Coverage for tests/test_api.py: 91.06%

93 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-05 17:33 +0200

1"""Tests for our own API exposition.""" 

2 

3from __future__ import annotations 

4 

5from collections import defaultdict 

6from pathlib import Path 

7from typing import TYPE_CHECKING 

8 

9import griffe 

10import pytest 

11from mkdocstrings import Inventory 

12 

13import griffe_public_redundant_aliases 

14 

15if TYPE_CHECKING: 

16 from collections.abc import Iterator 

17 

18 

19@pytest.fixture(name="loader", scope="module") 

20def _fixture_loader() -> griffe.GriffeLoader: 

21 loader = griffe.GriffeLoader() 

22 loader.load("griffe_public_redundant_aliases") 

23 loader.resolve_aliases() 

24 return loader 

25 

26 

27@pytest.fixture(name="internal_api", scope="module") 

28def _fixture_internal_api(loader: griffe.GriffeLoader) -> griffe.Module: 

29 return loader.modules_collection["griffe_public_redundant_aliases._internal"] 

30 

31 

32@pytest.fixture(name="public_api", scope="module") 

33def _fixture_public_api(loader: griffe.GriffeLoader) -> griffe.Module: 

34 return loader.modules_collection["griffe_public_redundant_aliases"] 

35 

36 

37def _yield_public_objects( 

38 obj: griffe.Module | griffe.Class, 

39 *, 

40 modules: bool = False, 

41 modulelevel: bool = True, 

42 inherited: bool = False, 

43 special: bool = False, 

44) -> Iterator[griffe.Object | griffe.Alias]: 

45 for member in obj.all_members.values() if inherited else obj.members.values(): 

46 try: 

47 if member.is_module: 

48 if member.is_alias or not member.is_public: 

49 continue 

50 if modules: 50 ↛ 51line 50 didn't jump to line 51 because the condition on line 50 was never true

51 yield member 

52 yield from _yield_public_objects( 

53 member, # type: ignore[arg-type] 

54 modules=modules, 

55 modulelevel=modulelevel, 

56 inherited=inherited, 

57 special=special, 

58 ) 

59 elif member.is_public and (special or not member.is_special): 

60 yield member 

61 else: 

62 continue 

63 if member.is_class and not modulelevel: 

64 yield from _yield_public_objects( 

65 member, # type: ignore[arg-type] 

66 modules=modules, 

67 modulelevel=False, 

68 inherited=inherited, 

69 special=special, 

70 ) 

71 except (griffe.AliasResolutionError, griffe.CyclicAliasError): 

72 continue 

73 

74 

75@pytest.fixture(name="modulelevel_internal_objects", scope="module") 

76def _fixture_modulelevel_internal_objects(internal_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: 

77 return list(_yield_public_objects(internal_api, modulelevel=True)) 

78 

79 

80@pytest.fixture(name="internal_objects", scope="module") 

81def _fixture_internal_objects(internal_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: 

82 return list(_yield_public_objects(internal_api, modulelevel=False, special=True)) 

83 

84 

85@pytest.fixture(name="public_objects", scope="module") 

86def _fixture_public_objects(public_api: griffe.Module) -> list[griffe.Object | griffe.Alias]: 

87 return list(_yield_public_objects(public_api, modulelevel=False, inherited=True, special=True)) 

88 

89 

90@pytest.fixture(name="inventory", scope="module") 

91def _fixture_inventory() -> Inventory: 

92 inventory_file = Path(__file__).parent.parent / "site" / "objects.inv" 

93 if not inventory_file.exists(): 93 ↛ 94line 93 didn't jump to line 94 because the condition on line 93 was never true

94 pytest.skip("The objects inventory is not available.") # ty: ignore[call-non-callable] 

95 with inventory_file.open("rb") as file: 

96 return Inventory.parse_sphinx(file) 

97 

98 

99def test_exposed_objects(modulelevel_internal_objects: list[griffe.Object | griffe.Alias]) -> None: 

100 """All public objects in the internal API are exposed under `griffe_public_redundant_aliases`.""" 

101 not_exposed = [ 

102 obj.path 

103 for obj in modulelevel_internal_objects 

104 if obj.name not in griffe_public_redundant_aliases.__all__ 

105 or not hasattr(griffe_public_redundant_aliases, obj.name) 

106 ] 

107 assert not not_exposed, "Objects not exposed:\n" + "\n".join(sorted(not_exposed)) 

108 

109 

110def test_unique_names(modulelevel_internal_objects: list[griffe.Object | griffe.Alias]) -> None: 

111 """All internal objects have unique names.""" 

112 names_to_paths = defaultdict(list) 

113 for obj in modulelevel_internal_objects: 

114 names_to_paths[obj.name].append(obj.path) 

115 non_unique = [paths for paths in names_to_paths.values() if len(paths) > 1] 

116 assert not non_unique, "Non-unique names:\n" + "\n".join(str(paths) for paths in non_unique) 

117 

118 

119def test_single_locations(public_api: griffe.Module) -> None: 

120 """All objects have a single public location.""" 

121 

122 def _public_path(obj: griffe.Object | griffe.Alias) -> bool: 

123 return obj.is_public and (obj.parent is None or _public_path(obj.parent)) 

124 

125 multiple_locations = {} 

126 for obj_name in griffe_public_redundant_aliases.__all__: 

127 obj = public_api[obj_name] 

128 if obj.aliases and ( 128 ↛ 131line 128 didn't jump to line 131 because the condition on line 128 was never true

129 public_aliases := [path for path, alias in obj.aliases.items() if path != obj.path and _public_path(alias)] 

130 ): 

131 multiple_locations[obj.path] = public_aliases 

132 assert not multiple_locations, "Multiple public locations:\n" + "\n".join( 

133 f"{path}: {aliases}" for path, aliases in multiple_locations.items() 

134 ) 

135 

136 

137def test_api_matches_inventory(inventory: Inventory, public_objects: list[griffe.Object | griffe.Alias]) -> None: 

138 """All public objects are added to the inventory.""" 

139 ignore_names = {"__getattr__", "__init__", "__repr__", "__str__", "__post_init__"} 

140 not_in_inventory = [ 

141 f"{obj.relative_filepath}:{obj.lineno}: {obj.path}" 

142 for obj in public_objects 

143 if obj.name not in ignore_names and obj.path not in inventory 

144 ] 

145 msg = "Objects not in the inventory (try running `make run mkdocs build`):\n{paths}" 

146 assert not not_in_inventory, msg.format(paths="\n".join(sorted(not_in_inventory))) 

147 

148 

149def test_inventory_matches_api( 

150 inventory: Inventory, 

151 public_objects: list[griffe.Object | griffe.Alias], 

152 loader: griffe.GriffeLoader, 

153) -> None: 

154 """The inventory doesn't contain any additional Python object.""" 

155 not_in_api = [] 

156 public_api_paths = {obj.path for obj in public_objects} 

157 public_api_paths.add("griffe_public_redundant_aliases") 

158 for item in inventory.values(): 

159 if ( 159 ↛ 158line 159 didn't jump to line 158 because the condition on line 159 was always true

160 item.domain == "py" 

161 and "(" not in item.name 

162 and ( 

163 item.name == "griffe_public_redundant_aliases" 

164 or item.name.startswith("griffe_public_redundant_aliases.") 

165 ) 

166 ): 

167 obj = loader.modules_collection[item.name] 

168 if obj.path not in public_api_paths and not any(path in public_api_paths for path in obj.aliases): 168 ↛ 169line 168 didn't jump to line 169 because the condition on line 168 was never true

169 not_in_api.append(item.name) 

170 msg = "Inventory objects not in public API (try running `make run mkdocs build`):\n{paths}" 

171 assert not not_in_api, msg.format(paths="\n".join(sorted(not_in_api))) 

172 

173 

174def test_no_module_docstrings_in_internal_api(internal_api: griffe.Module) -> None: 

175 """No module docstrings should be written in our internal API. 

176 

177 The reasoning is that docstrings are addressed to users of the public API, 

178 but internal modules are not exposed to users, so they should not have docstrings. 

179 """ 

180 

181 def _modules(obj: griffe.Module) -> Iterator[griffe.Module]: 

182 for member in obj.modules.values(): 

183 yield member 

184 yield from _modules(member) 

185 

186 for obj in _modules(internal_api): 

187 assert not obj.docstring