Coverage for src/_griffe/models.py: 79.40%
779 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-16 15:54 +0200
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-16 15:54 +0200
1# This module contains our models definitions,
2# to represent Python objects (and other aspects of Python APIs)... in Python.
4from __future__ import annotations
6import inspect
7from collections import defaultdict
8from contextlib import suppress
9from pathlib import Path
10from textwrap import dedent
11from typing import TYPE_CHECKING, Any, Callable, Sequence, Union, cast
13from _griffe.c3linear import c3linear_merge
14from _griffe.docstrings.parsers import DocstringStyle, parse
15from _griffe.enumerations import Kind, ParameterKind, Parser
16from _griffe.exceptions import AliasResolutionError, BuiltinModuleError, CyclicAliasError, NameResolutionError
17from _griffe.expressions import ExprCall, ExprName
18from _griffe.logger import logger
19from _griffe.mixins import ObjectAliasMixin
21if TYPE_CHECKING:
22 from _griffe.collections import LinesCollection, ModulesCollection
23 from _griffe.docstrings.models import DocstringSection
24 from _griffe.expressions import Expr
26from functools import cached_property
29class Decorator:
30 """This class represents decorators."""
32 def __init__(self, value: str | Expr, *, lineno: int | None, endlineno: int | None) -> None:
33 """Initialize the decorator.
35 Parameters:
36 value: The decorator code.
37 lineno: The starting line number.
38 endlineno: The ending line number.
39 """
40 self.value: str | Expr = value
41 """The decorator value (as a Griffe expression or string)."""
42 self.lineno: int | None = lineno
43 """The starting line number of the decorator."""
44 self.endlineno: int | None = endlineno
45 """The ending line number of the decorator."""
47 @property
48 def callable_path(self) -> str:
49 """The path of the callable used as decorator."""
50 value = self.value.function if isinstance(self.value, ExprCall) else self.value
51 return value if isinstance(value, str) else value.canonical_path
53 def as_dict(self, **kwargs: Any) -> dict[str, Any]: # noqa: ARG002
54 """Return this decorator's data as a dictionary.
56 Parameters:
57 **kwargs: Additional serialization options.
59 Returns:
60 A dictionary.
61 """
62 return {
63 "value": self.value,
64 "lineno": self.lineno,
65 "endlineno": self.endlineno,
66 }
69class Docstring:
70 """This class represents docstrings."""
72 def __init__(
73 self,
74 value: str,
75 *,
76 lineno: int | None = None,
77 endlineno: int | None = None,
78 parent: Object | None = None,
79 parser: DocstringStyle | Parser | None = None,
80 parser_options: dict[str, Any] | None = None,
81 ) -> None:
82 """Initialize the docstring.
84 Parameters:
85 value: The docstring value.
86 lineno: The starting line number.
87 endlineno: The ending line number.
88 parent: The parent object on which this docstring is attached.
89 parser: The docstring parser to use. By default, no parsing is done.
90 parser_options: Additional docstring parsing options.
91 """
92 self.value: str = inspect.cleandoc(value.rstrip())
93 """The original value of the docstring, cleaned by `inspect.cleandoc`."""
94 self.lineno: int | None = lineno
95 """The starting line number of the docstring."""
96 self.endlineno: int | None = endlineno
97 """The ending line number of the docstring."""
98 self.parent: Object | None = parent
99 """The object this docstring is attached to."""
100 self.parser: DocstringStyle | Parser | None = parser
101 """The selected docstring parser."""
102 self.parser_options: dict[str, Any] = parser_options or {}
103 """The configured parsing options."""
105 @property
106 def lines(self) -> list[str]:
107 """The lines of the docstring."""
108 return self.value.split("\n")
110 @property
111 def source(self) -> str:
112 """The original, uncleaned value of the docstring as written in the source."""
113 if self.parent is None:
114 raise ValueError("Cannot get original docstring without parent object")
115 if isinstance(self.parent.filepath, list):
116 raise ValueError("Cannot get original docstring for namespace package") # noqa: TRY004
117 if self.lineno is None or self.endlineno is None:
118 raise ValueError("Cannot get original docstring without line numbers")
119 return "\n".join(self.parent.lines_collection[self.parent.filepath][self.lineno - 1 : self.endlineno])
121 @cached_property
122 def parsed(self) -> list[DocstringSection]:
123 """The docstring sections, parsed into structured data."""
124 return self.parse()
126 def parse(
127 self,
128 parser: DocstringStyle | Parser | None = None,
129 **options: Any,
130 ) -> list[DocstringSection]:
131 """Parse the docstring into structured data.
133 Parameters:
134 parser: The docstring parser to use.
135 In order: use the given parser, or the self parser, or no parser (return a single text section).
136 **options: Additional docstring parsing options.
138 Returns:
139 The parsed docstring as a list of sections.
140 """
141 return parse(self, parser or self.parser, **(options or self.parser_options))
143 def as_dict(
144 self,
145 *,
146 full: bool = False,
147 **kwargs: Any, # noqa: ARG002
148 ) -> dict[str, Any]:
149 """Return this docstring's data as a dictionary.
151 Parameters:
152 full: Whether to return full info, or just base info.
153 **kwargs: Additional serialization options.
155 Returns:
156 A dictionary.
157 """
158 base: dict[str, Any] = {
159 "value": self.value,
160 "lineno": self.lineno,
161 "endlineno": self.endlineno,
162 }
163 if full:
164 base["parsed"] = self.parsed
165 return base
168class Parameter:
169 """This class represent a function parameter."""
171 def __init__(
172 self,
173 name: str,
174 *,
175 annotation: str | Expr | None = None,
176 kind: ParameterKind | None = None,
177 default: str | Expr | None = None,
178 docstring: Docstring | None = None,
179 ) -> None:
180 """Initialize the parameter.
182 Parameters:
183 name: The parameter name.
184 annotation: The parameter annotation, if any.
185 kind: The parameter kind.
186 default: The parameter default, if any.
187 docstring: The parameter docstring.
188 """
189 self.name: str = name
190 """The parameter name."""
191 self.annotation: str | Expr | None = annotation
192 """The parameter type annotation."""
193 self.kind: ParameterKind | None = kind
194 """The parameter kind."""
195 self.default: str | Expr | None = default
196 """The parameter default value."""
197 self.docstring: Docstring | None = docstring
198 """The parameter docstring."""
199 # The parent function is set in `Function.__init__`,
200 # when the parameters are assigned to the function.
201 self.function: Function | None = None
202 """The parent function of the parameter."""
204 def __str__(self) -> str:
205 param = f"{self.name}: {self.annotation} = {self.default}"
206 if self.kind:
207 return f"[{self.kind.value}] {param}"
208 return param
210 def __repr__(self) -> str:
211 return f"Parameter(name={self.name!r}, annotation={self.annotation!r}, kind={self.kind!r}, default={self.default!r})"
213 def __eq__(self, __value: object) -> bool:
214 """Parameters are equal if all their attributes except `docstring` and `function` are equal."""
215 if not isinstance(__value, Parameter): 215 ↛ 216line 215 didn't jump to line 216 because the condition on line 215 was never true
216 return NotImplemented
217 return (
218 self.name == __value.name
219 and self.annotation == __value.annotation
220 and self.kind == __value.kind
221 and self.default == __value.default
222 )
224 @property
225 def required(self) -> bool:
226 """Whether this parameter is required."""
227 return self.default is None
229 def as_dict(self, *, full: bool = False, **kwargs: Any) -> dict[str, Any]: # noqa: ARG002
230 """Return this parameter's data as a dictionary.
232 Parameters:
233 **kwargs: Additional serialization options.
235 Returns:
236 A dictionary.
237 """
238 base: dict[str, Any] = {
239 "name": self.name,
240 "annotation": self.annotation,
241 "kind": self.kind,
242 "default": self.default,
243 }
244 if self.docstring:
245 base["docstring"] = self.docstring.as_dict(full=full)
246 return base
249class Parameters:
250 """This class is a container for parameters.
252 It allows to get parameters using their position (index) or their name:
254 ```pycon
255 >>> parameters = Parameters(Parameter("hello"))
256 >>> parameters[0] is parameters["hello"]
257 True
258 ```
259 """
261 def __init__(self, *parameters: Parameter) -> None:
262 """Initialize the parameters container.
264 Parameters:
265 *parameters: The initial parameters to add to the container.
266 """
267 self._parameters_list: list[Parameter] = []
268 self._parameters_dict: dict[str, Parameter] = {}
269 for parameter in parameters:
270 self.add(parameter)
272 def __repr__(self) -> str:
273 return f"Parameters({', '.join(repr(param) for param in self._parameters_list)})"
275 def __getitem__(self, name_or_index: int | str) -> Parameter:
276 """Get a parameter by index or name."""
277 if isinstance(name_or_index, int):
278 return self._parameters_list[name_or_index]
279 return self._parameters_dict[name_or_index.lstrip("*")]
281 def __len__(self):
282 """The number of parameters."""
283 return len(self._parameters_list)
285 def __iter__(self):
286 """Iterate over the parameters, in order."""
287 return iter(self._parameters_list)
289 def __contains__(self, param_name: str):
290 """Whether a parameter with the given name is present."""
291 return param_name.lstrip("*") in self._parameters_dict
293 def add(self, parameter: Parameter) -> None:
294 """Add a parameter to the container.
296 Parameters:
297 parameter: The function parameter to add.
299 Raises:
300 ValueError: When a parameter with the same name is already present.
301 """
302 if parameter.name not in self._parameters_dict: 302 ↛ 306line 302 didn't jump to line 306 because the condition on line 302 was always true
303 self._parameters_dict[parameter.name] = parameter
304 self._parameters_list.append(parameter)
305 else:
306 raise ValueError(f"parameter {parameter.name} already present")
309class Object(ObjectAliasMixin):
310 """An abstract class representing a Python object."""
312 kind: Kind
313 """The object kind."""
314 is_alias: bool = False
315 """Always false for objects."""
316 is_collection: bool = False
317 """Always false for objects."""
318 inherited: bool = False
319 """Always false for objects.
321 Only aliases can be marked as inherited.
322 """
324 def __init__(
325 self,
326 name: str,
327 *,
328 lineno: int | None = None,
329 endlineno: int | None = None,
330 runtime: bool = True,
331 docstring: Docstring | None = None,
332 parent: Module | Class | None = None,
333 lines_collection: LinesCollection | None = None,
334 modules_collection: ModulesCollection | None = None,
335 ) -> None:
336 """Initialize the object.
338 Parameters:
339 name: The object name, as declared in the code.
340 lineno: The object starting line, or None for modules. Lines start at 1.
341 endlineno: The object ending line (inclusive), or None for modules.
342 runtime: Whether this object is present at runtime or not.
343 docstring: The object docstring.
344 parent: The object parent.
345 lines_collection: A collection of source code lines.
346 modules_collection: A collection of modules.
347 """
348 self.name: str = name
349 """The object name."""
351 self.lineno: int | None = lineno
352 """The starting line number of the object."""
354 self.endlineno: int | None = endlineno
355 """The ending line number of the object."""
357 self.docstring: Docstring | None = docstring
358 """The object docstring."""
360 self.parent: Module | Class | None = parent
361 """The parent of the object (none if top module)."""
363 self.members: dict[str, Object | Alias] = {}
364 """The object members (modules, classes, functions, attributes)."""
366 self.labels: set[str] = set()
367 """The object labels (`property`, `dataclass`, etc.)."""
369 self.imports: dict[str, str] = {}
370 """The other objects imported by this object.
372 Keys are the names within the object (`from ... import ... as AS_NAME`),
373 while the values are the actual names of the objects (`from ... import REAL_NAME as ...`).
374 """
376 self.exports: set[str] | list[str | ExprName] | None = None
377 """The names of the objects exported by this (module) object through the `__all__` variable.
379 Exports can contain string (object names) or resolvable names,
380 like other lists of exports coming from submodules:
382 ```python
383 from .submodule import __all__ as submodule_all
385 __all__ = ["hello", *submodule_all]
386 ```
388 Exports get expanded by the loader before it expands wildcards and resolves aliases.
389 """
391 self.aliases: dict[str, Alias] = {}
392 """The aliases pointing to this object."""
394 self.runtime: bool = runtime
395 """Whether this object is available at runtime.
397 Typically, type-guarded objects (under an `if TYPE_CHECKING` condition)
398 are not available at runtime.
399 """
401 self.extra: dict[str, dict[str, Any]] = defaultdict(dict)
402 """Namespaced dictionaries storing extra metadata for this object, used by extensions."""
404 self.public: bool | None = None
405 """Whether this object is public."""
407 self.deprecated: bool | str | None = None
408 """Whether this object is deprecated (boolean or deprecation message)."""
410 self._lines_collection: LinesCollection | None = lines_collection
411 self._modules_collection: ModulesCollection | None = modules_collection
413 # attach the docstring to this object
414 if docstring:
415 docstring.parent = self
417 def __repr__(self) -> str:
418 return f"{self.__class__.__name__}({self.name!r}, {self.lineno!r}, {self.endlineno!r})"
420 # Prevent using `__len__`.
421 def __bool__(self) -> bool:
422 """An object is always true-ish."""
423 return True
425 def __len__(self) -> int:
426 """The number of members in this object, recursively."""
427 return len(self.members) + sum(len(member) for member in self.members.values())
429 @property
430 def has_docstring(self) -> bool:
431 """Whether this object has a docstring (empty or not)."""
432 return bool(self.docstring)
434 # NOTE: (pawamoy) I'm not happy with `has_docstrings`.
435 # It currently recurses into submodules, but that doesn't make sense
436 # if downstream projects use it to know if they should render an init module
437 # while not rendering submodules too: the property could tell them there are
438 # docstrings, but they could be in submodules, not in the init module.
439 # Maybe we should derive it into new properties: `has_local_docstrings`,
440 # `has_docstrings`, `has_public_docstrings`... Maybe we should make it a function?`
441 # For now it's used in mkdocstrings-python so we must be careful with changes.
442 @property
443 def has_docstrings(self) -> bool:
444 """Whether this object or any of its members has a docstring (empty or not).
446 Inherited members are not considered. Imported members are not considered,
447 unless they are also public.
448 """
449 if self.has_docstring:
450 return True
451 for member in self.members.values():
452 try:
453 if (not member.is_imported or member.is_public) and member.has_docstrings:
454 return True
455 except AliasResolutionError:
456 continue
457 return False
459 def is_kind(self, kind: str | Kind | set[str | Kind]) -> bool:
460 """Tell if this object is of the given kind.
462 Parameters:
463 kind: An instance or set of kinds (strings or enumerations).
465 Raises:
466 ValueError: When an empty set is given as argument.
468 Returns:
469 True or False.
470 """
471 if isinstance(kind, set):
472 if not kind:
473 raise ValueError("kind must not be an empty set")
474 return self.kind in (knd if isinstance(knd, Kind) else Kind(knd) for knd in kind)
475 if isinstance(kind, str):
476 kind = Kind(kind)
477 return self.kind is kind
479 @cached_property
480 def inherited_members(self) -> dict[str, Alias]:
481 """Members that are inherited from base classes.
483 This method is part of the consumer API:
484 do not use when producing Griffe trees!
485 """
486 if not isinstance(self, Class):
487 return {}
488 try:
489 mro = self.mro()
490 except ValueError as error:
491 logger.debug(error)
492 return {}
493 inherited_members = {}
494 for base in reversed(mro):
495 for name, member in base.members.items():
496 if name not in self.members:
497 inherited_members[name] = Alias(name, member, parent=self, inherited=True)
498 return inherited_members
500 @property
501 def is_module(self) -> bool:
502 """Whether this object is a module."""
503 return self.kind is Kind.MODULE
505 @property
506 def is_class(self) -> bool:
507 """Whether this object is a class."""
508 return self.kind is Kind.CLASS
510 @property
511 def is_function(self) -> bool:
512 """Whether this object is a function."""
513 return self.kind is Kind.FUNCTION
515 @property
516 def is_attribute(self) -> bool:
517 """Whether this object is an attribute."""
518 return self.kind is Kind.ATTRIBUTE
520 @property
521 def is_init_module(self) -> bool:
522 """Whether this object is an `__init__.py` module."""
523 return False
525 @property
526 def is_package(self) -> bool:
527 """Whether this object is a package (top module)."""
528 return False
530 @property
531 def is_subpackage(self) -> bool:
532 """Whether this object is a subpackage."""
533 return False
535 @property
536 def is_namespace_package(self) -> bool:
537 """Whether this object is a namespace package (top folder, no `__init__.py`)."""
538 return False
540 @property
541 def is_namespace_subpackage(self) -> bool:
542 """Whether this object is a namespace subpackage."""
543 return False
545 def has_labels(self, *labels: str) -> bool:
546 """Tell if this object has all the given labels.
548 Parameters:
549 *labels: Labels that must be present.
551 Returns:
552 True or False.
553 """
554 return set(labels).issubset(self.labels)
556 def filter_members(self, *predicates: Callable[[Object | Alias], bool]) -> dict[str, Object | Alias]:
557 """Filter and return members based on predicates.
559 Parameters:
560 *predicates: A list of predicates, i.e. callables accepting a member as argument and returning a boolean.
562 Returns:
563 A dictionary of members.
564 """
565 if not predicates:
566 return self.members
567 members: dict[str, Object | Alias] = {}
568 for name, member in self.members.items():
569 if all(predicate(member) for predicate in predicates):
570 members[name] = member
571 return members
573 @property
574 def module(self) -> Module:
575 """The parent module of this object.
577 Examples:
578 >>> import griffe
579 >>> markdown = griffe.load("markdown")
580 >>> markdown["core.Markdown.references"].module
581 Module(PosixPath('~/project/.venv/lib/python3.11/site-packages/markdown/core.py'))
582 >>> # The `module` of a module is itself.
583 >>> markdown["core"].module
584 Module(PosixPath('~/project/.venv/lib/python3.11/site-packages/markdown/core.py'))
586 Raises:
587 ValueError: When the object is not a module and does not have a parent.
588 """
589 if isinstance(self, Module):
590 return self
591 if self.parent is not None:
592 return self.parent.module
593 raise ValueError(f"Object {self.name} does not have a parent module")
595 @property
596 def package(self) -> Module:
597 """The absolute top module (the package) of this object.
599 Examples:
600 >>> import griffe
601 >>> markdown = griffe.load("markdown")
602 >>> markdown["core.Markdown.references"].package
603 Module(PosixPath('~/project/.venv/lib/python3.11/site-packages/markdown/__init__.py'))
604 """
605 module = self.module
606 while module.parent:
607 module = module.parent # type: ignore[assignment] # always a module
608 return module
610 @property
611 def filepath(self) -> Path | list[Path]:
612 """The file path (or directory list for namespace packages) where this object was defined.
614 Examples:
615 >>> import griffe
616 >>> markdown = griffe.load("markdown")
617 >>> markdown.filepath
618 PosixPath('~/project/.venv/lib/python3.11/site-packages/markdown/__init__.py')
619 """
620 return self.module.filepath
622 @property
623 def relative_package_filepath(self) -> Path:
624 """The file path where this object was defined, relative to the top module path.
626 Raises:
627 ValueError: When the relative path could not be computed.
628 """
629 package_path = self.package.filepath
631 # Current "module" is a namespace package.
632 if isinstance(self.filepath, list): 632 ↛ 634line 632 didn't jump to line 634 because the condition on line 632 was never true
633 # Current package is a namespace package.
634 if isinstance(package_path, list):
635 for pkg_path in package_path:
636 for self_path in self.filepath:
637 with suppress(ValueError):
638 return self_path.relative_to(pkg_path.parent)
640 # Current package is a regular package.
641 # NOTE: Technically it makes no sense to have a namespace package
642 # under a non-namespace one, so we should never enter this branch.
643 else:
644 for self_path in self.filepath:
645 with suppress(ValueError):
646 return self_path.relative_to(package_path.parent.parent)
647 raise ValueError
649 # Current package is a namespace package,
650 # and current module is a regular module or package.
651 if isinstance(package_path, list): 651 ↛ 652line 651 didn't jump to line 652 because the condition on line 651 was never true
652 for pkg_path in package_path:
653 with suppress(ValueError):
654 return self.filepath.relative_to(pkg_path.parent)
655 raise ValueError
657 # Current package is a regular package,
658 # and current module is a regular module or package,
659 # try to compute the path relative to the parent folder
660 # of the package (search path).
661 return self.filepath.relative_to(package_path.parent.parent)
663 @property
664 def relative_filepath(self) -> Path:
665 """The file path where this object was defined, relative to the current working directory.
667 If this object's file path is not relative to the current working directory, return its absolute path.
669 Raises:
670 ValueError: When the relative path could not be computed.
671 """
672 cwd = Path.cwd()
673 if isinstance(self.filepath, list): 673 ↛ 674line 673 didn't jump to line 674 because the condition on line 673 was never true
674 for self_path in self.filepath:
675 with suppress(ValueError):
676 return self_path.relative_to(cwd)
677 raise ValueError(f"No directory in {self.filepath!r} is relative to the current working directory {cwd}")
678 try:
679 return self.filepath.relative_to(cwd)
680 except ValueError:
681 return self.filepath
683 @property
684 def path(self) -> str:
685 """The dotted path of this object.
687 On regular objects (not aliases), the path is the canonical path.
689 Examples:
690 >>> import griffe
691 >>> markdown = griffe.load("markdown")
692 >>> markdown["core.Markdown.references"].path
693 'markdown.core.Markdown.references'
694 """
695 return self.canonical_path
697 @property
698 def canonical_path(self) -> str:
699 """The full dotted path of this object.
701 The canonical path is the path where the object was defined (not imported).
702 """
703 if self.parent is None:
704 return self.name
705 return ".".join((self.parent.path, self.name))
707 @property
708 def modules_collection(self) -> ModulesCollection:
709 """The modules collection attached to this object or its parents.
711 Raises:
712 ValueError: When no modules collection can be found in the object or its parents.
713 """
714 if self._modules_collection is not None:
715 return self._modules_collection
716 if self.parent is None: 716 ↛ 717line 716 didn't jump to line 717 because the condition on line 716 was never true
717 raise ValueError("no modules collection in this object or its parents")
718 return self.parent.modules_collection
720 @property
721 def lines_collection(self) -> LinesCollection:
722 """The lines collection attached to this object or its parents.
724 Raises:
725 ValueError: When no modules collection can be found in the object or its parents.
726 """
727 if self._lines_collection is not None:
728 return self._lines_collection
729 if self.parent is None: 729 ↛ 730line 729 didn't jump to line 730 because the condition on line 729 was never true
730 raise ValueError("no lines collection in this object or its parents")
731 return self.parent.lines_collection
733 @property
734 def lines(self) -> list[str]:
735 """The lines containing the source of this object."""
736 try:
737 filepath = self.filepath
738 except BuiltinModuleError:
739 return []
740 if isinstance(filepath, list): 740 ↛ 741line 740 didn't jump to line 741 because the condition on line 740 was never true
741 return []
742 try:
743 lines = self.lines_collection[filepath]
744 except KeyError:
745 return []
746 if self.is_module:
747 return lines
748 if self.lineno is None or self.endlineno is None:
749 return []
750 return lines[self.lineno - 1 : self.endlineno]
752 @property
753 def source(self) -> str:
754 """The source code of this object."""
755 return dedent("\n".join(self.lines))
757 def resolve(self, name: str) -> str:
758 """Resolve a name within this object's and parents' scope.
760 Parameters:
761 name: The name to resolve.
763 Raises:
764 NameResolutionError: When the name could not be resolved.
766 Returns:
767 The resolved name.
768 """
769 # TODO: Better match Python's own scoping rules?
770 # Also, maybe return regular paths instead of canonical ones?
772 # Name is a member this object.
773 if name in self.members:
774 if self.members[name].is_alias:
775 return self.members[name].target_path # type: ignore[union-attr]
776 return self.members[name].path
778 # Name was imported.
779 if name in self.imports: 779 ↛ 780line 779 didn't jump to line 780 because the condition on line 779 was never true
780 return self.imports[name]
782 # Name unknown and no more parent scope.
783 if self.parent is None:
784 # could be a built-in
785 raise NameResolutionError(f"{name} could not be resolved in the scope of {self.path}")
787 # Name is parent, non-module object.
788 # NOTE: possibly useless branch.
789 if name == self.parent.name and not self.parent.is_module: 789 ↛ 790line 789 didn't jump to line 790 because the condition on line 789 was never true
790 return self.parent.path
792 # Recurse in parent.
793 return self.parent.resolve(name)
795 def as_dict(self, *, full: bool = False, **kwargs: Any) -> dict[str, Any]:
796 """Return this object's data as a dictionary.
798 Parameters:
799 full: Whether to return full info, or just base info.
800 **kwargs: Additional serialization options.
802 Returns:
803 A dictionary.
804 """
805 base: dict[str, Any] = {
806 "kind": self.kind,
807 "name": self.name,
808 }
810 if full:
811 base.update(
812 {
813 "path": self.path,
814 "filepath": self.filepath,
815 "relative_filepath": self.relative_filepath,
816 "relative_package_filepath": self.relative_package_filepath,
817 },
818 )
820 if self.lineno is not None:
821 base["lineno"] = self.lineno
822 if self.endlineno is not None:
823 base["endlineno"] = self.endlineno
824 if self.docstring:
825 base["docstring"] = self.docstring
827 base["labels"] = self.labels
828 base["members"] = {name: member.as_dict(full=full, **kwargs) for name, member in self.members.items()}
830 return base
833class Alias(ObjectAliasMixin):
834 """This class represents an alias, or indirection, to an object declared in another module.
836 Aliases represent objects that are in the scope of a module or class,
837 but were imported from another module.
839 They behave almost exactly like regular objects, to a few exceptions:
841 - line numbers are those of the alias, not the target
842 - the path is the alias path, not the canonical one
843 - the name can be different from the target's
844 - if the target can be resolved, the kind is the target's kind
845 - if the target cannot be resolved, the kind becomes [Kind.ALIAS][griffe.Kind]
846 """
848 is_alias: bool = True
849 """Always true for aliases."""
850 is_collection: bool = False
851 """Always false for aliases."""
853 def __init__(
854 self,
855 name: str,
856 target: str | Object | Alias,
857 *,
858 lineno: int | None = None,
859 endlineno: int | None = None,
860 runtime: bool = True,
861 parent: Module | Class | Alias | None = None,
862 inherited: bool = False,
863 ) -> None:
864 """Initialize the alias.
866 Parameters:
867 name: The alias name.
868 target: If it's a string, the target resolution is delayed until accessing the target property.
869 If it's an object, or even another alias, the target is immediately set.
870 lineno: The alias starting line number.
871 endlineno: The alias ending line number.
872 runtime: Whether this alias is present at runtime or not.
873 parent: The alias parent.
874 inherited: Whether this alias wraps an inherited member.
875 """
876 self.name: str = name
877 """The alias name."""
879 self.alias_lineno: int | None = lineno
880 """The starting line number of the alias."""
882 self.alias_endlineno: int | None = endlineno
883 """The ending line number of the alias."""
885 self.runtime: bool = runtime
886 """Whether this alias is available at runtime."""
888 self.inherited: bool = inherited
889 """Whether this alias represents an inherited member."""
891 self.public: bool | None = None
892 """Whether this alias is public."""
894 self.deprecated: str | bool | None = None
895 """Whether this alias is deprecated (boolean or deprecation message)."""
897 self._parent: Module | Class | Alias | None = parent
898 self._passed_through: bool = False
900 self.target_path: str
901 """The path of this alias' target."""
903 if isinstance(target, str):
904 self._target: Object | Alias | None = None
905 self.target_path = target
906 else:
907 self._target = target
908 self.target_path = target.path
909 self._update_target_aliases()
911 def __repr__(self) -> str:
912 return f"Alias({self.name!r}, {self.target_path!r})"
914 # Prevent using `__len__`.
915 def __bool__(self) -> bool:
916 """An alias is always true-ish."""
917 return True
919 def __len__(self) -> int:
920 """The length of an alias is always 1."""
921 return 1
923 # SPECIAL PROXIES -------------------------------
924 # The following methods and properties exist on the target(s),
925 # but we must handle them in a special way.
927 @property
928 def kind(self) -> Kind:
929 """The target's kind, or `Kind.ALIAS` if the target cannot be resolved."""
930 # custom behavior to avoid raising exceptions
931 try:
932 return self.final_target.kind
933 except (AliasResolutionError, CyclicAliasError):
934 return Kind.ALIAS
936 @property
937 def has_docstring(self) -> bool:
938 """Whether this alias' target has a non-empty docstring."""
939 try:
940 return self.final_target.has_docstring
941 except (AliasResolutionError, CyclicAliasError):
942 return False
944 @property
945 def has_docstrings(self) -> bool:
946 """Whether this alias' target or any of its members has a non-empty docstring."""
947 try:
948 return self.final_target.has_docstrings
949 except (AliasResolutionError, CyclicAliasError):
950 return False
952 @property
953 def parent(self) -> Module | Class | Alias | None:
954 """The parent of this alias."""
955 return self._parent
957 @parent.setter
958 def parent(self, value: Module | Class | Alias) -> None:
959 self._parent = value
960 self._update_target_aliases()
962 @property
963 def path(self) -> str:
964 """The dotted path / import path of this object."""
965 return ".".join((self.parent.path, self.name)) # type: ignore[union-attr] # we assume there's always a parent
967 @property
968 def modules_collection(self) -> ModulesCollection:
969 """The modules collection attached to the alias parents."""
970 # no need to forward to the target
971 return self.parent.modules_collection # type: ignore[union-attr] # we assume there's always a parent
973 @cached_property
974 def members(self) -> dict[str, Object | Alias]:
975 """The target's members (modules, classes, functions, attributes)."""
976 final_target = self.final_target
978 # We recreate aliases to maintain a correct hierarchy,
979 # and therefore correct paths. The path of an alias member
980 # should be the path of the alias plus the member's name,
981 # not the original member's path.
982 return {
983 name: Alias(name, target=member, parent=self, inherited=False)
984 for name, member in final_target.members.items()
985 }
987 @cached_property
988 def inherited_members(self) -> dict[str, Alias]:
989 """Members that are inherited from base classes.
991 Each inherited member of the target will be wrapped in an alias,
992 to preserve correct object access paths.
994 This method is part of the consumer API:
995 do not use when producing Griffe trees!
996 """
997 final_target = self.final_target
999 # We recreate aliases to maintain a correct hierarchy,
1000 # and therefore correct paths. The path of an alias member
1001 # should be the path of the alias plus the member's name,
1002 # not the original member's path.
1003 return {
1004 name: Alias(name, target=member, parent=self, inherited=True)
1005 for name, member in final_target.inherited_members.items()
1006 }
1008 def as_json(self, *, full: bool = False, **kwargs: Any) -> str:
1009 """Return this target's data as a JSON string.
1011 Parameters:
1012 full: Whether to return full info, or just base info.
1013 **kwargs: Additional serialization options passed to encoder.
1015 Returns:
1016 A JSON string.
1017 """
1018 try:
1019 return self.final_target.as_json(full=full, **kwargs)
1020 except (AliasResolutionError, CyclicAliasError):
1021 return super().as_json(full=full, **kwargs)
1023 # GENERIC OBJECT PROXIES --------------------------------
1024 # The following methods and properties exist on the target(s).
1025 # We first try to reach the final target, triggering alias resolution errors
1026 # and cyclic aliases errors early. We avoid recursing in the alias chain.
1028 @property
1029 def extra(self) -> dict:
1030 """Namespaced dictionaries storing extra metadata for this object, used by extensions."""
1031 return self.final_target.extra
1033 @property
1034 def lineno(self) -> int | None:
1035 """The starting line number of the target object."""
1036 return self.final_target.lineno
1038 @property
1039 def endlineno(self) -> int | None:
1040 """The ending line number of the target object."""
1041 return self.final_target.endlineno
1043 @property
1044 def docstring(self) -> Docstring | None:
1045 """The target docstring."""
1046 return self.final_target.docstring
1048 @docstring.setter
1049 def docstring(self, docstring: Docstring | None) -> None:
1050 self.final_target.docstring = docstring
1052 @property
1053 def labels(self) -> set[str]:
1054 """The target labels (`property`, `dataclass`, etc.)."""
1055 return self.final_target.labels
1057 @property
1058 def imports(self) -> dict[str, str]:
1059 """The other objects imported by this alias' target.
1061 Keys are the names within the object (`from ... import ... as AS_NAME`),
1062 while the values are the actual names of the objects (`from ... import REAL_NAME as ...`).
1063 """
1064 return self.final_target.imports
1066 @property
1067 def exports(self) -> set[str] | list[str | ExprName] | None:
1068 """The names of the objects exported by this (module) object through the `__all__` variable.
1070 Exports can contain string (object names) or resolvable names,
1071 like other lists of exports coming from submodules:
1073 ```python
1074 from .submodule import __all__ as submodule_all
1076 __all__ = ["hello", *submodule_all]
1077 ```
1079 Exports get expanded by the loader before it expands wildcards and resolves aliases.
1080 """
1081 return self.final_target.exports
1083 @property
1084 def aliases(self) -> dict[str, Alias]:
1085 """The aliases pointing to this object."""
1086 return self.final_target.aliases
1088 def is_kind(self, kind: str | Kind | set[str | Kind]) -> bool:
1089 """Tell if this object is of the given kind.
1091 Parameters:
1092 kind: An instance or set of kinds (strings or enumerations).
1094 Raises:
1095 ValueError: When an empty set is given as argument.
1097 Returns:
1098 True or False.
1099 """
1100 return self.final_target.is_kind(kind)
1102 @property
1103 def is_module(self) -> bool:
1104 """Whether this object is a module."""
1105 return self.final_target.is_module
1107 @property
1108 def is_class(self) -> bool:
1109 """Whether this object is a class."""
1110 return self.final_target.is_class
1112 @property
1113 def is_function(self) -> bool:
1114 """Whether this object is a function."""
1115 return self.final_target.is_function
1117 @property
1118 def is_attribute(self) -> bool:
1119 """Whether this object is an attribute."""
1120 return self.final_target.is_attribute
1122 def has_labels(self, *labels: str) -> bool:
1123 """Tell if this object has all the given labels.
1125 Parameters:
1126 *labels: Labels that must be present.
1128 Returns:
1129 True or False.
1130 """
1131 return self.final_target.has_labels(*labels)
1133 def filter_members(self, *predicates: Callable[[Object | Alias], bool]) -> dict[str, Object | Alias]:
1134 """Filter and return members based on predicates.
1136 Parameters:
1137 *predicates: A list of predicates, i.e. callables accepting a member as argument and returning a boolean.
1139 Returns:
1140 A dictionary of members.
1141 """
1142 return self.final_target.filter_members(*predicates)
1144 @property
1145 def module(self) -> Module:
1146 """The parent module of this object.
1148 Raises:
1149 ValueError: When the object is not a module and does not have a parent.
1150 """
1151 return self.final_target.module
1153 @property
1154 def package(self) -> Module:
1155 """The absolute top module (the package) of this object."""
1156 return self.final_target.package
1158 @property
1159 def filepath(self) -> Path | list[Path]:
1160 """The file path (or directory list for namespace packages) where this object was defined."""
1161 return self.final_target.filepath
1163 @property
1164 def relative_filepath(self) -> Path:
1165 """The file path where this object was defined, relative to the current working directory.
1167 If this object's file path is not relative to the current working directory, return its absolute path.
1169 Raises:
1170 ValueError: When the relative path could not be computed.
1171 """
1172 return self.final_target.relative_filepath
1174 @property
1175 def relative_package_filepath(self) -> Path:
1176 """The file path where this object was defined, relative to the top module path.
1178 Raises:
1179 ValueError: When the relative path could not be computed.
1180 """
1181 return self.final_target.relative_package_filepath
1183 @property
1184 def canonical_path(self) -> str:
1185 """The full dotted path of this object.
1187 The canonical path is the path where the object was defined (not imported).
1188 """
1189 return self.final_target.canonical_path
1191 @property
1192 def lines_collection(self) -> LinesCollection:
1193 """The lines collection attached to this object or its parents.
1195 Raises:
1196 ValueError: When no modules collection can be found in the object or its parents.
1197 """
1198 return self.final_target.lines_collection
1200 @property
1201 def lines(self) -> list[str]:
1202 """The lines containing the source of this object."""
1203 return self.final_target.lines
1205 @property
1206 def source(self) -> str:
1207 """The source code of this object."""
1208 return self.final_target.source
1210 def resolve(self, name: str) -> str:
1211 """Resolve a name within this object's and parents' scope.
1213 Parameters:
1214 name: The name to resolve.
1216 Raises:
1217 NameResolutionError: When the name could not be resolved.
1219 Returns:
1220 The resolved name.
1221 """
1222 return self.final_target.resolve(name)
1224 # SPECIFIC MODULE/CLASS/FUNCTION/ATTRIBUTE PROXIES ---------------
1225 # These methods and properties exist on targets of specific kind.
1226 # We first try to reach the final target, triggering alias resolution errors
1227 # and cyclic aliases errors early. We avoid recursing in the alias chain.
1229 @property
1230 def _filepath(self) -> Path | list[Path] | None:
1231 return cast(Module, self.final_target)._filepath
1233 @property
1234 def bases(self) -> list[Expr | str]:
1235 """The class bases."""
1236 return cast(Class, self.final_target).bases
1238 @property
1239 def decorators(self) -> list[Decorator]:
1240 """The class/function decorators."""
1241 return cast(Union[Class, Function], self.target).decorators
1243 @property
1244 def imports_future_annotations(self) -> bool:
1245 """Whether this module import future annotations."""
1246 return cast(Module, self.final_target).imports_future_annotations
1248 @property
1249 def is_init_module(self) -> bool:
1250 """Whether this module is an `__init__.py` module."""
1251 return cast(Module, self.final_target).is_init_module
1253 @property
1254 def is_package(self) -> bool:
1255 """Whether this module is a package (top module)."""
1256 return cast(Module, self.final_target).is_package
1258 @property
1259 def is_subpackage(self) -> bool:
1260 """Whether this module is a subpackage."""
1261 return cast(Module, self.final_target).is_subpackage
1263 @property
1264 def is_namespace_package(self) -> bool:
1265 """Whether this module is a namespace package (top folder, no `__init__.py`)."""
1266 return cast(Module, self.final_target).is_namespace_package
1268 @property
1269 def is_namespace_subpackage(self) -> bool:
1270 """Whether this module is a namespace subpackage."""
1271 return cast(Module, self.final_target).is_namespace_subpackage
1273 @property
1274 def overloads(self) -> dict[str, list[Function]] | list[Function] | None:
1275 """The overloaded signatures declared in this class/module or for this function."""
1276 return cast(Union[Module, Class, Function], self.final_target).overloads
1278 @overloads.setter
1279 def overloads(self, overloads: list[Function] | None) -> None:
1280 cast(Union[Module, Class, Function], self.final_target).overloads = overloads
1282 @property
1283 def parameters(self) -> Parameters:
1284 """The parameters of the current function or `__init__` method for classes.
1286 This property can fetch inherited members,
1287 and therefore is part of the consumer API:
1288 do not use when producing Griffe trees!
1289 """
1290 return cast(Union[Class, Function], self.final_target).parameters
1292 @property
1293 def returns(self) -> str | Expr | None:
1294 """The function return type annotation."""
1295 return cast(Function, self.final_target).returns
1297 @returns.setter
1298 def returns(self, returns: str | Expr | None) -> None:
1299 cast(Function, self.final_target).returns = returns
1301 @property
1302 def setter(self) -> Function | None:
1303 """The setter linked to this function (property)."""
1304 return cast(Attribute, self.final_target).setter
1306 @property
1307 def deleter(self) -> Function | None:
1308 """The deleter linked to this function (property)."""
1309 return cast(Attribute, self.final_target).deleter
1311 @property
1312 def value(self) -> str | Expr | None:
1313 """The attribute value."""
1314 return cast(Attribute, self.final_target).value
1316 @property
1317 def annotation(self) -> str | Expr | None:
1318 """The attribute type annotation."""
1319 return cast(Attribute, self.final_target).annotation
1321 @annotation.setter
1322 def annotation(self, annotation: str | Expr | None) -> None:
1323 cast(Attribute, self.final_target).annotation = annotation
1325 @property
1326 def resolved_bases(self) -> list[Object]:
1327 """Resolved class bases.
1329 This method is part of the consumer API:
1330 do not use when producing Griffe trees!
1331 """
1332 return cast(Class, self.final_target).resolved_bases
1334 def mro(self) -> list[Class]:
1335 """Return a list of classes in order corresponding to Python's MRO."""
1336 return cast(Class, self.final_target).mro()
1338 # SPECIFIC ALIAS METHOD AND PROPERTIES -----------------
1339 # These methods and properties do not exist on targets,
1340 # they are specific to aliases.
1342 @property
1343 def target(self) -> Object | Alias:
1344 """The resolved target (actual object), if possible.
1346 Upon accessing this property, if the target is not already resolved,
1347 a lookup is done using the modules collection to find the target.
1348 """
1349 if not self.resolved:
1350 self.resolve_target()
1351 return self._target # type: ignore[return-value] # cannot return None, exception is raised
1353 @target.setter
1354 def target(self, value: Object | Alias) -> None:
1355 if value is self or value.path == self.path:
1356 raise CyclicAliasError([self.target_path])
1357 self._target = value
1358 self.target_path = value.path
1359 if self.parent is not None:
1360 self._target.aliases[self.path] = self
1362 @property
1363 def final_target(self) -> Object:
1364 """The final, resolved target, if possible.
1366 This will iterate through the targets until a non-alias object is found.
1367 """
1368 # Here we quickly iterate on the alias chain,
1369 # remembering which path we've seen already to detect cycles.
1371 # The cycle detection is needed because alias chains can be created
1372 # as already resolved, and can contain cycles.
1374 # using a dict as an ordered set
1375 paths_seen: dict[str, None] = {}
1376 target = self
1377 while target.is_alias:
1378 if target.path in paths_seen: 1378 ↛ 1379line 1378 didn't jump to line 1379 because the condition on line 1378 was never true
1379 raise CyclicAliasError([*paths_seen, target.path])
1380 paths_seen[target.path] = None
1381 target = target.target # type: ignore[assignment]
1382 return target # type: ignore[return-value]
1384 def resolve_target(self) -> None:
1385 """Resolve the target.
1387 Raises:
1388 AliasResolutionError: When the target cannot be resolved.
1389 It happens when the target does not exist,
1390 or could not be loaded (unhandled dynamic object?),
1391 or when the target is from a module that was not loaded
1392 and added to the collection.
1393 CyclicAliasError: When the resolved target is the alias itself.
1394 """
1395 # Here we try to resolve the whole alias chain recursively.
1396 # We detect cycles by setting a "passed through" state variable
1397 # on each alias as we pass through it. Passing a second time
1398 # through an alias will raise a CyclicAliasError.
1400 # If a single link of the chain cannot be resolved,
1401 # the whole chain stays unresolved. This prevents
1402 # bad surprises later, in code that checks if
1403 # an alias is resolved by checking only
1404 # the first link of the chain.
1405 if self._passed_through: 1405 ↛ 1406line 1405 didn't jump to line 1406 because the condition on line 1405 was never true
1406 raise CyclicAliasError([self.target_path])
1407 self._passed_through = True
1408 try:
1409 self._resolve_target()
1410 finally:
1411 self._passed_through = False
1413 def _resolve_target(self) -> None:
1414 try:
1415 resolved = self.modules_collection.get_member(self.target_path)
1416 except KeyError as error:
1417 raise AliasResolutionError(self) from error
1418 if resolved is self: 1418 ↛ 1419line 1418 didn't jump to line 1419 because the condition on line 1418 was never true
1419 raise CyclicAliasError([self.target_path])
1420 if resolved.is_alias and not resolved.resolved:
1421 try:
1422 resolved.resolve_target()
1423 except CyclicAliasError as error:
1424 raise CyclicAliasError([self.target_path, *error.chain]) from error
1425 self._target = resolved
1426 if self.parent is not None: 1426 ↛ exitline 1426 didn't return from function '_resolve_target' because the condition on line 1426 was always true
1427 self._target.aliases[self.path] = self # type: ignore[union-attr] # we just set the target
1429 def _update_target_aliases(self) -> None:
1430 with suppress(AttributeError, AliasResolutionError, CyclicAliasError):
1431 self._target.aliases[self.path] = self # type: ignore[union-attr]
1433 @property
1434 def resolved(self) -> bool:
1435 """Whether this alias' target is resolved."""
1436 return self._target is not None
1438 @property
1439 def wildcard(self) -> str | None:
1440 """The module on which the wildcard import is performed (if any)."""
1441 if self.name.endswith("/*"):
1442 return self.target_path
1443 return None
1445 def as_dict(self, *, full: bool = False, **kwargs: Any) -> dict[str, Any]: # noqa: ARG002
1446 """Return this alias' data as a dictionary.
1448 Parameters:
1449 full: Whether to return full info, or just base info.
1450 **kwargs: Additional serialization options.
1452 Returns:
1453 A dictionary.
1454 """
1455 base: dict[str, Any] = {
1456 "kind": Kind.ALIAS,
1457 "name": self.name,
1458 "target_path": self.target_path,
1459 }
1461 if full:
1462 base["path"] = self.path
1464 if self.alias_lineno: 1464 ↛ 1466line 1464 didn't jump to line 1466 because the condition on line 1464 was always true
1465 base["lineno"] = self.alias_lineno
1466 if self.alias_endlineno: 1466 ↛ 1469line 1466 didn't jump to line 1469 because the condition on line 1466 was always true
1467 base["endlineno"] = self.alias_endlineno
1469 return base
1472class Module(Object):
1473 """The class representing a Python module."""
1475 kind = Kind.MODULE
1477 def __init__(self, *args: Any, filepath: Path | list[Path] | None = None, **kwargs: Any) -> None:
1478 """Initialize the module.
1480 Parameters:
1481 *args: See [`griffe.Object`][].
1482 filepath: The module file path (directory for namespace [sub]packages, none for builtin modules).
1483 **kwargs: See [`griffe.Object`][].
1484 """
1485 super().__init__(*args, **kwargs)
1486 self._filepath: Path | list[Path] | None = filepath
1487 self.overloads: dict[str, list[Function]] = defaultdict(list)
1488 """The overloaded signatures declared in this module."""
1490 def __repr__(self) -> str:
1491 try:
1492 return f"Module({self.filepath!r})"
1493 except BuiltinModuleError:
1494 return f"Module({self.name!r})"
1496 @property
1497 def filepath(self) -> Path | list[Path]:
1498 """The file path of this module.
1500 Raises:
1501 BuiltinModuleError: When the instance filepath is None.
1502 """
1503 if self._filepath is None:
1504 raise BuiltinModuleError(self.name)
1505 return self._filepath
1507 @property
1508 def imports_future_annotations(self) -> bool:
1509 """Whether this module import future annotations."""
1510 return (
1511 "annotations" in self.members
1512 and self.members["annotations"].is_alias
1513 and self.members["annotations"].target_path == "__future__.annotations" # type: ignore[union-attr]
1514 )
1516 @property
1517 def is_init_module(self) -> bool:
1518 """Whether this module is an `__init__.py` module."""
1519 if isinstance(self.filepath, list): 1519 ↛ 1520line 1519 didn't jump to line 1520 because the condition on line 1519 was never true
1520 return False
1521 try:
1522 return self.filepath.name.split(".", 1)[0] == "__init__"
1523 except BuiltinModuleError:
1524 return False
1526 @property
1527 def is_package(self) -> bool:
1528 """Whether this module is a package (top module)."""
1529 return not bool(self.parent) and self.is_init_module
1531 @property
1532 def is_subpackage(self) -> bool:
1533 """Whether this module is a subpackage."""
1534 return bool(self.parent) and self.is_init_module
1536 @property
1537 def is_namespace_package(self) -> bool:
1538 """Whether this module is a namespace package (top folder, no `__init__.py`)."""
1539 try:
1540 return self.parent is None and isinstance(self.filepath, list)
1541 except BuiltinModuleError:
1542 return False
1544 @property
1545 def is_namespace_subpackage(self) -> bool:
1546 """Whether this module is a namespace subpackage."""
1547 try:
1548 return (
1549 self.parent is not None
1550 and isinstance(self.filepath, list)
1551 and (
1552 cast(Module, self.parent).is_namespace_package or cast(Module, self.parent).is_namespace_subpackage
1553 )
1554 )
1555 except BuiltinModuleError:
1556 return False
1558 def as_dict(self, **kwargs: Any) -> dict[str, Any]:
1559 """Return this module's data as a dictionary.
1561 Parameters:
1562 **kwargs: Additional serialization options.
1564 Returns:
1565 A dictionary.
1566 """
1567 base = super().as_dict(**kwargs)
1568 if isinstance(self._filepath, list): 1568 ↛ 1569line 1568 didn't jump to line 1569 because the condition on line 1568 was never true
1569 base["filepath"] = [str(path) for path in self._filepath]
1570 elif self._filepath: 1570 ↛ 1573line 1570 didn't jump to line 1573 because the condition on line 1570 was always true
1571 base["filepath"] = str(self._filepath)
1572 else:
1573 base["filepath"] = None
1574 return base
1577class Class(Object):
1578 """The class representing a Python class."""
1580 kind = Kind.CLASS
1582 def __init__(
1583 self,
1584 *args: Any,
1585 bases: Sequence[Expr | str] | None = None,
1586 decorators: list[Decorator] | None = None,
1587 **kwargs: Any,
1588 ) -> None:
1589 """Initialize the class.
1591 Parameters:
1592 *args: See [`griffe.Object`][].
1593 bases: The list of base classes, if any.
1594 decorators: The class decorators, if any.
1595 **kwargs: See [`griffe.Object`][].
1596 """
1597 super().__init__(*args, **kwargs)
1598 self.bases: list[Expr | str] = list(bases) if bases else []
1599 """The class bases."""
1600 self.decorators: list[Decorator] = decorators or []
1601 """The class decorators."""
1602 self.overloads: dict[str, list[Function]] = defaultdict(list)
1603 """The overloaded signatures declared in this class."""
1605 @property
1606 def parameters(self) -> Parameters:
1607 """The parameters of this class' `__init__` method, if any.
1609 This property fetches inherited members,
1610 and therefore is part of the consumer API:
1611 do not use when producing Griffe trees!
1612 """
1613 try:
1614 return self.all_members["__init__"].parameters # type: ignore[union-attr]
1615 except KeyError:
1616 return Parameters()
1618 @cached_property
1619 def resolved_bases(self) -> list[Object]:
1620 """Resolved class bases.
1622 This method is part of the consumer API:
1623 do not use when producing Griffe trees!
1624 """
1625 resolved_bases = []
1626 for base in self.bases:
1627 base_path = base if isinstance(base, str) else base.canonical_path
1628 try:
1629 resolved_base = self.modules_collection[base_path]
1630 if resolved_base.is_alias:
1631 resolved_base = resolved_base.final_target
1632 except (AliasResolutionError, CyclicAliasError, KeyError):
1633 logger.debug(f"Base class {base_path} is not loaded, or not static, it cannot be resolved")
1634 else:
1635 resolved_bases.append(resolved_base)
1636 return resolved_bases
1638 def _mro(self, seen: tuple[str, ...] = ()) -> list[Class]:
1639 seen = (*seen, self.path)
1640 bases: list[Class] = [base for base in self.resolved_bases if base.is_class] # type: ignore[misc]
1641 if not bases:
1642 return [self]
1643 for base in bases:
1644 if base.path in seen:
1645 cycle = " -> ".join(seen) + f" -> {base.path}"
1646 raise ValueError(f"Cannot compute C3 linearization, inheritance cycle detected: {cycle}")
1647 return [self, *c3linear_merge(*[base._mro(seen) for base in bases], bases)]
1649 def mro(self) -> list[Class]:
1650 """Return a list of classes in order corresponding to Python's MRO."""
1651 return self._mro()[1:] # remove self
1653 def as_dict(self, **kwargs: Any) -> dict[str, Any]:
1654 """Return this class' data as a dictionary.
1656 Parameters:
1657 **kwargs: Additional serialization options.
1659 Returns:
1660 A dictionary.
1661 """
1662 base = super().as_dict(**kwargs)
1663 base["bases"] = self.bases
1664 base["decorators"] = [dec.as_dict(**kwargs) for dec in self.decorators]
1665 return base
1668class Function(Object):
1669 """The class representing a Python function."""
1671 kind = Kind.FUNCTION
1673 def __init__(
1674 self,
1675 *args: Any,
1676 parameters: Parameters | None = None,
1677 returns: str | Expr | None = None,
1678 decorators: list[Decorator] | None = None,
1679 **kwargs: Any,
1680 ) -> None:
1681 """Initialize the function.
1683 Parameters:
1684 *args: See [`griffe.Object`][].
1685 parameters: The function parameters.
1686 returns: The function return annotation.
1687 decorators: The function decorators, if any.
1688 **kwargs: See [`griffe.Object`][].
1689 """
1690 super().__init__(*args, **kwargs)
1691 self.parameters: Parameters = parameters or Parameters()
1692 """The function parameters."""
1693 self.returns: str | Expr | None = returns
1694 """The function return type annotation."""
1695 self.decorators: list[Decorator] = decorators or []
1696 """The function decorators."""
1697 self.overloads: list[Function] | None = None
1698 """The overloaded signatures of this function."""
1700 for parameter in self.parameters:
1701 parameter.function = self
1703 @property
1704 def annotation(self) -> str | Expr | None:
1705 """The type annotation of the returned value."""
1706 return self.returns
1708 def as_dict(self, **kwargs: Any) -> dict[str, Any]:
1709 """Return this function's data as a dictionary.
1711 Parameters:
1712 **kwargs: Additional serialization options.
1714 Returns:
1715 A dictionary.
1716 """
1717 base = super().as_dict(**kwargs)
1718 base["decorators"] = [dec.as_dict(**kwargs) for dec in self.decorators]
1719 base["parameters"] = [param.as_dict(**kwargs) for param in self.parameters]
1720 base["returns"] = self.returns
1721 return base
1724class Attribute(Object):
1725 """The class representing a Python module/class/instance attribute."""
1727 kind = Kind.ATTRIBUTE
1729 def __init__(
1730 self,
1731 *args: Any,
1732 value: str | Expr | None = None,
1733 annotation: str | Expr | None = None,
1734 **kwargs: Any,
1735 ) -> None:
1736 """Initialize the function.
1738 Parameters:
1739 *args: See [`griffe.Object`][].
1740 value: The attribute value, if any.
1741 annotation: The attribute annotation, if any.
1742 **kwargs: See [`griffe.Object`][].
1743 """
1744 super().__init__(*args, **kwargs)
1745 self.value: str | Expr | None = value
1746 """The attribute value."""
1747 self.annotation: str | Expr | None = annotation
1748 """The attribute type annotation."""
1749 self.setter: Function | None = None
1750 """The setter linked to this property."""
1751 self.deleter: Function | None = None
1752 """The deleter linked to this property."""
1754 def as_dict(self, **kwargs: Any) -> dict[str, Any]:
1755 """Return this function's data as a dictionary.
1757 Parameters:
1758 **kwargs: Additional serialization options.
1760 Returns:
1761 A dictionary.
1762 """
1763 base = super().as_dict(**kwargs)
1764 if self.value is not None: 1764 ↛ 1766line 1764 didn't jump to line 1766 because the condition on line 1764 was always true
1765 base["value"] = self.value
1766 if self.annotation is not None: 1766 ↛ 1767line 1766 didn't jump to line 1767 because the condition on line 1766 was never true
1767 base["annotation"] = self.annotation
1768 return base