Coverage for src/griffe/_internal/stats.py: 41.07%

96 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-11 13:44 +0200

1# This module contains utilities to compute loading statistics, 

2# like time spent visiting modules statically or dynamically. 

3 

4from __future__ import annotations 

5 

6from collections import defaultdict 

7from pathlib import Path 

8from typing import TYPE_CHECKING 

9 

10from griffe._internal.enumerations import Kind 

11 

12if TYPE_CHECKING: 

13 from griffe._internal.loader import GriffeLoader 

14 from griffe._internal.models import Alias, Object 

15 

16 

17class Stats: 

18 """Load statistics for a Griffe loader.""" 

19 

20 def __init__(self, loader: GriffeLoader) -> None: 

21 """Initialiwe the stats object. 

22 

23 Parameters: 

24 loader: The loader to compute stats for. 

25 """ 

26 self.loader = loader 

27 """The loader to compute stats for.""" 

28 

29 modules_by_extension = defaultdict( 

30 int, 

31 { 

32 "": 0, 

33 ".py": 0, 

34 ".pyi": 0, 

35 ".pyc": 0, 

36 ".pyo": 0, 

37 ".pyd": 0, 

38 ".so": 0, 

39 }, 

40 ) 

41 

42 top_modules = loader.modules_collection.members.values() 

43 

44 self.by_kind = { 

45 Kind.MODULE: 0, 

46 Kind.CLASS: 0, 

47 Kind.FUNCTION: 0, 

48 Kind.ATTRIBUTE: 0, 

49 Kind.TYPE_ALIAS: 0, 

50 } 

51 """Number of objects by kind.""" 

52 

53 self.packages = len(top_modules) 

54 """Number of packages.""" 

55 

56 self.modules_by_extension = modules_by_extension 

57 """Number of modules by extension.""" 

58 

59 self.lines = sum(len(lines) for lines in loader.lines_collection.values()) 

60 """Total number of lines.""" 

61 

62 self.time_spent_visiting = 0 

63 """Time spent visiting modules.""" 

64 

65 self.time_spent_inspecting = 0 

66 """Time spent inspecting modules.""" 

67 

68 self.time_spent_serializing = 0 

69 """Time spent serializing objects.""" 

70 

71 for module in top_modules: 

72 self._itercount(module) 

73 

74 def _itercount(self, root: Object | Alias) -> None: 

75 if root.is_alias: 

76 return 

77 self.by_kind[root.kind] += 1 

78 if root.is_module: 

79 if isinstance(root.filepath, Path): 79 ↛ 81line 79 didn't jump to line 81 because the condition on line 79 was always true

80 self.modules_by_extension[root.filepath.suffix] += 1 

81 elif root.filepath is None: 

82 self.modules_by_extension[""] += 1 

83 for member in root.members.values(): 

84 self._itercount(member) 

85 

86 def as_text(self) -> str: 

87 """Format the statistics as text. 

88 

89 Returns: 

90 Text stats. 

91 """ 

92 lines = [] 

93 packages = self.packages 

94 modules = self.by_kind[Kind.MODULE] 

95 classes = self.by_kind[Kind.CLASS] 

96 functions = self.by_kind[Kind.FUNCTION] 

97 attributes = self.by_kind[Kind.ATTRIBUTE] 

98 type_aliases = self.by_kind[Kind.TYPE_ALIAS] 

99 objects = sum((modules, classes, functions, attributes, type_aliases)) 

100 lines.append("Statistics") 

101 lines.append("---------------------") 

102 lines.append("Number of loaded objects") 

103 lines.append(f" Modules: {modules}") 

104 lines.append(f" Classes: {classes}") 

105 lines.append(f" Functions: {functions}") 

106 lines.append(f" Attributes: {attributes}") 

107 lines.append(f" Type aliases: {type_aliases}") 

108 lines.append(f" Total: {objects} across {packages} packages") 

109 per_ext = self.modules_by_extension 

110 builtin = per_ext[""] 

111 regular = per_ext[".py"] 

112 stubs = per_ext[".pyi"] 

113 compiled = modules - builtin - regular - stubs 

114 lines.append("") 

115 lines.append(f"Total number of lines: {self.lines}") 

116 lines.append("") 

117 lines.append("Modules") 

118 lines.append(f" Builtin: {builtin}") 

119 lines.append(f" Compiled: {compiled}") 

120 lines.append(f" Regular: {regular}") 

121 lines.append(f" Stubs: {stubs}") 

122 lines.append(" Per extension:") 

123 for ext, number in sorted(per_ext.items()): 

124 if ext: 

125 lines.append(f" {ext}: {number}") 

126 

127 visit_time = self.time_spent_visiting / 1000 

128 inspect_time = self.time_spent_inspecting / 1000 

129 total_time = visit_time + inspect_time 

130 visit_percent = visit_time / total_time * 100 

131 inspect_percent = inspect_time / total_time * 100 

132 

133 force_inspection = self.loader.force_inspection 

134 visited_modules = 0 if force_inspection else regular 

135 try: 

136 visit_time_per_module = visit_time / visited_modules 

137 except ZeroDivisionError: 

138 visit_time_per_module = 0 

139 

140 inspected_modules = builtin + compiled + (regular if force_inspection else 0) 

141 try: 

142 inspect_time_per_module = inspect_time / inspected_modules 

143 except ZeroDivisionError: 

144 inspect_time_per_module = 0 

145 

146 lines.append("") 

147 lines.append( 

148 f"Time spent visiting modules ({visited_modules}): " 

149 f"{visit_time}ms, {visit_time_per_module:.02f}ms/module ({visit_percent:.02f}%)", 

150 ) 

151 lines.append( 

152 f"Time spent inspecting modules ({inspected_modules}): " 

153 f"{inspect_time}ms, {inspect_time_per_module:.02f}ms/module ({inspect_percent:.02f}%)", 

154 ) 

155 

156 serialize_time = self.time_spent_serializing / 1000 

157 serialize_time_per_module = serialize_time / modules 

158 lines.append(f"Time spent serializing: {serialize_time}ms, {serialize_time_per_module:.02f}ms/module") 

159 

160 return "\n".join(lines)