Coverage for src/_griffe/merger.py: 88.76%
61 statements
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +0200
« prev ^ index » next coverage.py v7.6.2, created at 2024-10-12 01:34 +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.exceptions import AliasResolutionError, CyclicAliasError
9from _griffe.logger import logger
11if TYPE_CHECKING:
12 from _griffe.models import Attribute, Class, Function, Module, Object
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_members(class_, stubs)
27def _merge_function_stubs(function: Function, stubs: Function) -> None:
28 _merge_stubs_docstring(function, stubs)
29 for parameter in stubs.parameters:
30 with suppress(KeyError):
31 function.parameters[parameter.name].annotation = parameter.annotation
32 function.returns = stubs.returns
35def _merge_attribute_stubs(attribute: Attribute, stubs: Attribute) -> None:
36 _merge_stubs_docstring(attribute, stubs)
37 attribute.annotation = stubs.annotation
40def _merge_stubs_docstring(obj: Object, stubs: Object) -> None:
41 if not obj.docstring and stubs.docstring: 41 ↛ 42line 41 didn't jump to line 42 because the condition on line 41 was never true
42 obj.docstring = stubs.docstring
45def _merge_stubs_overloads(obj: Module | Class, stubs: Module | Class) -> None:
46 for function_name, overloads in list(stubs.overloads.items()):
47 if overloads:
48 with suppress(KeyError):
49 obj.get_member(function_name).overloads = overloads
50 del stubs.overloads[function_name]
53def _merge_stubs_members(obj: Module | Class, stubs: Module | Class) -> None:
54 for member_name, stub_member in stubs.members.items():
55 if member_name in obj.members:
56 # We don't merge imported stub objects that already exist in the concrete module.
57 # Stub objects must be defined where they are exposed in the concrete package,
58 # not be imported from other stub modules.
59 if stub_member.is_alias:
60 continue
61 obj_member = obj.get_member(member_name)
62 with suppress(AliasResolutionError, CyclicAliasError):
63 # An object's canonical location can differ from its equivalent stub location.
64 # Devs usually declare stubs at the public location of the corresponding object,
65 # not the canonical one. Therefore, we must allow merging stubs into the target of an alias,
66 # as long as the stub and target are of the same kind.
67 if obj_member.kind is not stub_member.kind: 67 ↛ 68line 67 didn't jump to line 68 because the condition on line 67 was never true
68 logger.debug(
69 "Cannot merge stubs for %s: kind %s != %s",
70 obj_member.path,
71 stub_member.kind.value,
72 obj_member.kind.value,
73 )
74 elif obj_member.is_module:
75 _merge_module_stubs(obj_member, stub_member) # type: ignore[arg-type]
76 elif obj_member.is_class:
77 _merge_class_stubs(obj_member, stub_member) # type: ignore[arg-type]
78 elif obj_member.is_function:
79 _merge_function_stubs(obj_member, stub_member) # type: ignore[arg-type]
80 elif obj_member.is_attribute: 80 ↛ 54line 80 didn't jump to line 54
81 _merge_attribute_stubs(obj_member, stub_member) # type: ignore[arg-type]
82 else:
83 stub_member.runtime = False
84 obj.set_member(member_name, stub_member)
87def merge_stubs(mod1: Module, mod2: Module) -> Module:
88 """Merge stubs into a module.
90 Parameters:
91 mod1: A regular module or stubs module.
92 mod2: A regular module or stubs module.
94 Raises:
95 ValueError: When both modules are regular modules (no stubs is passed).
97 Returns:
98 The regular module.
99 """
100 logger.debug("Trying to merge %s and %s", mod1.filepath, mod2.filepath)
101 if mod1.filepath.suffix == ".pyi": # type: ignore[union-attr] 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true
102 stubs = mod1
103 module = mod2
104 elif mod2.filepath.suffix == ".pyi": # type: ignore[union-attr] 104 ↛ 108line 104 didn't jump to line 108 because the condition on line 104 was always true
105 stubs = mod2
106 module = mod1
107 else:
108 raise ValueError("cannot merge regular (non-stubs) modules together")
109 _merge_module_stubs(module, stubs)
110 return module