Coverage for src/_griffe/stats.py: 42.86%

94 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-15 16:47 +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.enumerations import Kind 

11 

12if TYPE_CHECKING: 

13 from _griffe.loader import GriffeLoader 

14 from _griffe.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 } 

50 """Number of objects by kind.""" 

51 

52 self.packages = len(top_modules) 

53 """Number of packages.""" 

54 

55 self.modules_by_extension = modules_by_extension 

56 """Number of modules by extension.""" 

57 

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

59 """Total number of lines.""" 

60 

61 self.time_spent_visiting = 0 

62 """Time spent visiting modules.""" 

63 

64 self.time_spent_inspecting = 0 

65 """Time spent inspecting modules.""" 

66 

67 self.time_spent_serializing = 0 

68 """Time spent serializing objects.""" 

69 

70 for module in top_modules: 

71 self._itercount(module) 

72 

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

74 if root.is_alias: 

75 return 

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

77 if root.is_module: 

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

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

80 elif root.filepath is None: 

81 self.modules_by_extension[""] += 1 

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

83 self._itercount(member) 

84 

85 def as_text(self) -> str: 

86 """Format the statistics as text. 

87 

88 Returns: 

89 Text stats. 

90 """ 

91 lines = [] 

92 packages = self.packages 

93 modules = self.by_kind[Kind.MODULE] 

94 classes = self.by_kind[Kind.CLASS] 

95 functions = self.by_kind[Kind.FUNCTION] 

96 attributes = self.by_kind[Kind.ATTRIBUTE] 

97 objects = sum((modules, classes, functions, attributes)) 

98 lines.append("Statistics") 

99 lines.append("---------------------") 

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

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

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

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

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

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

106 per_ext = self.modules_by_extension 

107 builtin = per_ext[""] 

108 regular = per_ext[".py"] 

109 stubs = per_ext[".pyi"] 

110 compiled = modules - builtin - regular - stubs 

111 lines.append("") 

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

113 lines.append("") 

114 lines.append("Modules") 

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

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

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

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

119 lines.append(" Per extension:") 

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

121 if ext: 

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

123 

124 visit_time = self.time_spent_visiting / 1000 

125 inspect_time = self.time_spent_inspecting / 1000 

126 total_time = visit_time + inspect_time 

127 visit_percent = visit_time / total_time * 100 

128 inspect_percent = inspect_time / total_time * 100 

129 

130 force_inspection = self.loader.force_inspection 

131 visited_modules = 0 if force_inspection else regular 

132 try: 

133 visit_time_per_module = visit_time / visited_modules 

134 except ZeroDivisionError: 

135 visit_time_per_module = 0 

136 

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

138 try: 

139 inspect_time_per_module = inspect_time / inspected_modules 

140 except ZeroDivisionError: 

141 inspect_time_per_module = 0 

142 

143 lines.append("") 

144 lines.append( 

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

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

147 ) 

148 lines.append( 

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

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

151 ) 

152 

153 serialize_time = self.time_spent_serializing / 1000 

154 serialize_time_per_module = serialize_time / modules 

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

156 

157 return "\n".join(lines)