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

1# This module contains utilities to merge stubs data and concrete data. 

2 

3from __future__ import annotations 

4 

5from contextlib import suppress 

6from typing import TYPE_CHECKING 

7 

8from _griffe.exceptions import AliasResolutionError, CyclicAliasError 

9from _griffe.logger import logger 

10 

11if TYPE_CHECKING: 

12 from _griffe.models import Attribute, Class, Function, Module, Object 

13 

14 

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) 

19 

20 

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) 

25 

26 

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 

33 

34 

35def _merge_attribute_stubs(attribute: Attribute, stubs: Attribute) -> None: 

36 _merge_stubs_docstring(attribute, stubs) 

37 attribute.annotation = stubs.annotation 

38 

39 

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 

43 

44 

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] 

51 

52 

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) 

85 

86 

87def merge_stubs(mod1: Module, mod2: Module) -> Module: 

88 """Merge stubs into a module. 

89 

90 Parameters: 

91 mod1: A regular module or stubs module. 

92 mod2: A regular module or stubs module. 

93 

94 Raises: 

95 ValueError: When both modules are regular modules (no stubs is passed). 

96 

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