Coverage for nuremics\core\utils.py: 91%

86 statements  

« prev     ^ index     » next       coverage.py v7.9.0, created at 2025-06-12 16:54 +0200

1import attrs 

2import ast 

3import inspect 

4import textwrap 

5from pathlib import Path 

6 

7import numpy as np 

8 

9 

10def convert_value(value): 

11 """Function to convert values in python native types""" 

12 

13 if value == "NA": 

14 return None 

15 elif isinstance(value, (bool, np.bool_)): 

16 return bool(value) 

17 elif isinstance(value, (int, np.int64)): 

18 return int(value) 

19 elif isinstance(value, (float, np.float64)): 

20 return float(value) 

21 elif isinstance(value, str): 

22 return str(value) 

23 else: 

24 return value 

25 

26 

27def concat_lists_unique( 

28 list1: list, 

29 list2: list, 

30): 

31 return list(dict.fromkeys(list1 + list2)) 

32 

33 

34# From ChatGPT 

35def get_self_method_calls(cls, method_name="__call__"): 

36 """Get list of functions called in a specific class""" 

37 

38 method = getattr(cls, method_name, None) 

39 if method is None: 

40 return [] 

41 

42 source = inspect.getsource(method) 

43 source = textwrap.dedent(source) 

44 tree = ast.parse(source) 

45 

46 called_methods = [] 

47 

48 class SelfCallVisitor(ast.NodeVisitor): 

49 def visit_Call(self, node): 

50 if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name): 

51 if node.func.value.id == "self": 

52 called_methods.append(node.func.attr) 

53 self.generic_visit(node) 

54 

55 SelfCallVisitor().visit(tree) 

56 return called_methods 

57 

58 

59# From ChatGPT 

60def only_function_calls(method, allowed_methods): 

61 """ 

62 Checks that the method contains only function calls, 

63 and that all calls are either super().__call__() or self.<allowed_method>(). 

64 """ 

65 # Get and dedent source code 

66 source = inspect.getsource(method) 

67 source = textwrap.dedent(source) 

68 

69 # Parse the AST 

70 tree = ast.parse(source) 

71 

72 # Expect a FunctionDef node at top level 

73 func_def = tree.body[0] 

74 if not isinstance(func_def, ast.FunctionDef): 

75 return False 

76 

77 for stmt in func_def.body: 

78 # Each statement must be a simple expression (Expr) containing a Call 

79 if not isinstance(stmt, ast.Expr) or not isinstance(stmt.value, ast.Call): 

80 return False 

81 

82 call = stmt.value 

83 func = call.func 

84 

85 # Allow super().__call__() 

86 if isinstance(func, ast.Attribute) and isinstance(func.value, ast.Call): 

87 if ( 

88 isinstance(func.value.func, ast.Name) 

89 and func.value.func.id == 'super' 

90 and func.attr == '__call__' 

91 ): 

92 continue 

93 

94 # Allow self.<allowed_method>() 

95 if isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name): 

96 if func.value.id == 'self' and func.attr in allowed_methods: 

97 continue 

98 

99 # If it's neither of the above, reject 

100 return False 

101 

102 return True 

103 

104 

105# From ChatGPT 

106def extract_inputs_and_types(obj) -> dict: 

107 params = {} 

108 for field in attrs.fields(obj.__class__): 

109 if field.metadata.get("input", False): 

110 params[field.name] = field.type 

111 return params 

112 

113 

114def extract_analysis(obj) -> dict: 

115 analysis = [] 

116 settings = {} 

117 for field in attrs.fields(obj.__class__): 

118 if field.metadata.get("analysis", False): 

119 analysis.append(field.name) 

120 if field.metadata.get("settings", False): 

121 settings[field.name] = field.metadata.get("settings") 

122 return analysis, settings 

123 

124 

125# From ChatGPT 

126def extract_self_output_keys(method): 

127 """ 

128 Extracts all dictionary keys used in self.output_paths[...] from a method. 

129 Returns a list of key names as strings. 

130 """ 

131 keys = [] 

132 

133 # Get and clean source code 

134 source = inspect.getsource(method) 

135 source = textwrap.dedent(source) 

136 tree = ast.parse(source) 

137 

138 class OutputKeyVisitor(ast.NodeVisitor): 

139 def visit_Subscript(self, node): 

140 # Check if it's self.output_paths[...] 

141 if (isinstance(node.value, ast.Attribute) and 

142 isinstance(node.value.value, ast.Name) and 

143 node.value.value.id == "self" and 

144 node.value.attr == "output_paths"): 

145 

146 # Extract the key if it's a constant (string) 

147 if isinstance(node.slice, ast.Constant): 

148 keys.append(node.slice.value) 

149 # Compatibility with Python <3.9: slice is an Index node 

150 elif isinstance(node.slice, ast.Index) and isinstance(node.slice.value, ast.Constant): 

151 keys.append(node.slice.value.value) 

152 

153 # Continue visiting child nodes 

154 self.generic_visit(node) 

155 

156 OutputKeyVisitor().visit(tree) 

157 

158 return keys