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
« 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
7import numpy as np
10def convert_value(value):
11 """Function to convert values in python native types"""
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
27def concat_lists_unique(
28 list1: list,
29 list2: list,
30):
31 return list(dict.fromkeys(list1 + list2))
34# From ChatGPT
35def get_self_method_calls(cls, method_name="__call__"):
36 """Get list of functions called in a specific class"""
38 method = getattr(cls, method_name, None)
39 if method is None:
40 return []
42 source = inspect.getsource(method)
43 source = textwrap.dedent(source)
44 tree = ast.parse(source)
46 called_methods = []
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)
55 SelfCallVisitor().visit(tree)
56 return called_methods
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)
69 # Parse the AST
70 tree = ast.parse(source)
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
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
82 call = stmt.value
83 func = call.func
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
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
99 # If it's neither of the above, reject
100 return False
102 return True
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
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
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 = []
133 # Get and clean source code
134 source = inspect.getsource(method)
135 source = textwrap.dedent(source)
136 tree = ast.parse(source)
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"):
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)
153 # Continue visiting child nodes
154 self.generic_visit(node)
156 OutputKeyVisitor().visit(tree)
158 return keys