Coverage for src/griffe/_internal/merger.py: 86.61%
76 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-11 13:44 +0200
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-11 13:44 +0200
1# This module contains utilities to merge stubs data and concrete data.
3from __future__ import annotations
5from contextlib import suppress
6from typing import TYPE_CHECKING
8from griffe._internal.exceptions import AliasResolutionError, CyclicAliasError
9from griffe._internal.logger import logger
11if TYPE_CHECKING:
12 from griffe._internal.models import Attribute, Class, Function, Module, Object, TypeAlias
15def _merge_module_stubs(module: Module, stubs: Module) -> None:
16 _merge_stubs_docstring(module, stubs)
17 _merge_stubs_overloads(module, stubs)
18 _merge_stubs_members(module, stubs)
21def _merge_class_stubs(class_: Class, stubs: Class) -> None:
22 _merge_stubs_docstring(class_, stubs)
23 _merge_stubs_overloads(class_, stubs)
24 _merge_stubs_type_parameters(class_, stubs)
25 _merge_stubs_members(class_, stubs)
28def _merge_function_stubs(function: Function, stubs: Function) -> None:
29 _merge_stubs_docstring(function, stubs)
30 for parameter in stubs.parameters:
31 with suppress(KeyError):
32 function.parameters[parameter.name].annotation = parameter.annotation
33 function.returns = stubs.returns
34 _merge_stubs_type_parameters(function, stubs)
37def _merge_attribute_stubs(attribute: Attribute, stubs: Attribute) -> None:
38 _merge_stubs_docstring(attribute, stubs)
39 attribute.annotation = stubs.annotation
40 if stubs.value not in (None, "..."):
41 attribute.value = stubs.value
44def _merge_type_alias_stubs(type_alias: TypeAlias, stubs: TypeAlias) -> None:
45 _merge_stubs_docstring(type_alias, stubs)
46 _merge_stubs_type_parameters(type_alias, stubs)
49def _merge_stubs_docstring(obj: Object, stubs: Object) -> None:
50 if not obj.docstring and stubs.docstring: 50 ↛ 51line 50 didn't jump to line 51 because the condition on line 50 was never true
51 obj.docstring = stubs.docstring
54def _merge_stubs_type_parameters(obj: Class | Function | TypeAlias, stubs: Class | Function | TypeAlias) -> None:
55 if not obj.type_parameters and stubs.type_parameters: 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true
56 obj.type_parameters = stubs.type_parameters
59def _merge_stubs_overloads(obj: Module | Class, stubs: Module | Class) -> None:
60 for function_name, overloads in list(stubs.overloads.items()):
61 if overloads:
62 with suppress(KeyError):
63 obj.get_member(function_name).overloads = overloads
64 del stubs.overloads[function_name]
67def _merge_stubs_members(obj: Module | Class, stubs: Module | Class) -> None:
68 # Merge imports to later know if objects coming from the stubs were imported.
69 obj.imports.update(stubs.imports)
71 # Override exports to later know if objects coming from the stubs were exported.
72 if stubs.exports is not None:
73 obj.exports = stubs.exports
75 for member_name, stub_member in stubs.members.items():
76 if member_name in obj.members:
77 # We don't merge imported stub objects that already exist in the concrete module.
78 # Stub objects must be defined where they are exposed in the concrete package,
79 # not be imported from other stub modules.
80 if stub_member.is_alias:
81 continue
82 obj_member = obj.get_member(member_name)
83 with suppress(AliasResolutionError, CyclicAliasError):
84 # An object's canonical location can differ from its equivalent stub location.
85 # Devs usually declare stubs at the public location of the corresponding object,
86 # not the canonical one. Therefore, we must allow merging stubs into the target of an alias,
87 # as long as the stub and target are of the same kind.
88 if obj_member.kind is not stub_member.kind: 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true
89 logger.debug(
90 "Cannot merge stubs for %s: kind %s != %s",
91 obj_member.path,
92 stub_member.kind.value,
93 obj_member.kind.value,
94 )
95 elif obj_member.is_module:
96 _merge_module_stubs(obj_member, stub_member) # type: ignore[arg-type]
97 elif obj_member.is_class:
98 _merge_class_stubs(obj_member, stub_member) # type: ignore[arg-type]
99 elif obj_member.is_function:
100 _merge_function_stubs(obj_member, stub_member) # type: ignore[arg-type]
101 elif obj_member.is_attribute: 101 ↛ 103line 101 didn't jump to line 103 because the condition on line 101 was always true
102 _merge_attribute_stubs(obj_member, stub_member) # type: ignore[arg-type]
103 elif obj_member.is_type_alias:
104 _merge_type_alias_stubs(obj_member, stub_member) # type: ignore[arg-type]
105 else:
106 stub_member.runtime = False
107 obj.set_member(member_name, stub_member)
110def merge_stubs(mod1: Module, mod2: Module) -> Module:
111 """Merge stubs into a module.
113 Parameters:
114 mod1: A regular module or stubs module.
115 mod2: A regular module or stubs module.
117 Raises:
118 ValueError: When both modules are regular modules (no stubs is passed).
120 Returns:
121 The regular module.
122 """
123 logger.debug("Trying to merge %s and %s", mod1.filepath, mod2.filepath)
124 if mod1.filepath.suffix == ".pyi": # type: ignore[union-attr]
125 stubs = mod1
126 module = mod2
127 elif mod2.filepath.suffix == ".pyi": # type: ignore[union-attr] 127 ↛ 131line 127 didn't jump to line 131 because the condition on line 127 was always true
128 stubs = mod2
129 module = mod1
130 else:
131 raise ValueError("cannot merge regular (non-stubs) modules together")
132 _merge_module_stubs(module, stubs)
133 return module