1
+ import argparse
1
2
import inspect
2
3
import json
3
4
import logging
4
5
import os
5
6
from typing import Any , Dict , Type
6
7
7
- import pydantic
8
8
from pydantic import BaseModel , ConfigDict
9
9
from typing_extensions import Callable
10
10
@@ -48,15 +48,40 @@ def get_user_set_parameters(remove: bool = False) -> Dict[str, JsonParameter]:
48
48
return parameters
49
49
50
50
51
+ def return_json_parameters (params : Dict [str , Any ]) -> Dict [str , Any ]:
52
+ """
53
+ Returns the parameters as a JSON serializable dictionary.
54
+
55
+ Args:
56
+ params (dict): The parameters to serialize.
57
+
58
+ Returns:
59
+ dict: The JSON serializable dictionary.
60
+ """
61
+ return_params = {}
62
+ for key , value in params .items ():
63
+ if isinstance (value , ObjectParameter ):
64
+ continue
65
+
66
+ return_params [key ] = value .get_value ()
67
+ return return_params
68
+
69
+
51
70
def filter_arguments_for_func (
52
71
func : Callable [..., Any ],
53
72
params : Dict [str , Any ],
54
73
map_variable : MapVariableType = None ,
55
74
) -> Dict [str , Any ]:
56
75
"""
57
76
Inspects the function to be called as part of the pipeline to find the arguments of the function.
58
- Matches the function arguments to the parameters available either by command line or by up stream steps.
77
+ Matches the function arguments to the parameters available either by static parameters or by up stream steps.
59
78
79
+ The function "func" signature could be:
80
+ - def my_function(arg1: int, arg2: str, arg3: float):
81
+ - def my_function(arg1: int, arg2: str, arg3: float, **kwargs):
82
+ in this case, we would need to send in remaining keyword arguments as a dictionary.
83
+ - def my_function(arg1: int, arg2: str, arg3: float, args: argparse.Namespace):
84
+ In this case, we need to send the rest of the parameters as attributes of the args object.
60
85
61
86
Args:
62
87
func (Callable): The function to inspect
@@ -72,63 +97,109 @@ def filter_arguments_for_func(
72
97
params [key ] = JsonParameter (kind = "json" , value = v )
73
98
74
99
bound_args = {}
75
- unassigned_params = set (params .keys ())
76
- # Check if VAR_KEYWORD is used, it is we send back everything
77
- # If **kwargs is present in the function signature, we send back everything
78
- for name , value in function_args .items ():
79
- if value .kind != inspect .Parameter .VAR_KEYWORD :
80
- continue
81
- # Found VAR_KEYWORD, we send back everything as found
82
- for key , value in params .items ():
83
- bound_args [key ] = params [key ].get_value ()
100
+ missing_required_args : list [str ] = []
101
+ var_keyword_param = None
102
+ namespace_param = None
84
103
85
- return bound_args
86
-
87
- # Lets return what is asked for then!!
104
+ # First pass: Handle regular parameters and identify special parameters
88
105
for name , value in function_args .items ():
89
106
# Ignore any *args
90
107
if value .kind == inspect .Parameter .VAR_POSITIONAL :
91
108
logger .warning (f"Ignoring parameter { name } as it is VAR_POSITIONAL" )
92
109
continue
93
110
94
- if name not in params :
95
- # No parameter of this name was provided
96
- if value .default == inspect .Parameter .empty :
97
- # No default value is given in the function signature. error as parameter is required.
98
- raise ValueError (
99
- f"Parameter { name } is required for { func .__name__ } but not provided"
100
- )
101
- # default value is given in the function signature, nothing further to do.
111
+ # Check for **kwargs parameter
112
+ if value .kind == inspect .Parameter .VAR_KEYWORD :
113
+ var_keyword_param = name
102
114
continue
103
115
104
- param_value = params [name ]
105
-
106
- if type (value .annotation ) in [
107
- BaseModel ,
108
- pydantic ._internal ._model_construction .ModelMetaclass ,
109
- ] and not isinstance (param_value , ObjectParameter ):
110
- # Even if the annotation is a pydantic model, it can be passed as an object parameter
111
- # We try to cast it as a pydantic model if asked
112
- named_param = params [name ].get_value ()
113
-
114
- if not isinstance (named_param , dict ):
115
- # A case where the parameter is a one attribute model
116
- named_param = {name : named_param }
117
-
118
- bound_model = bind_args_for_pydantic_model (named_param , value .annotation )
119
- bound_args [name ] = bound_model
116
+ # Check for argparse.Namespace parameter
117
+ if value .annotation == argparse .Namespace :
118
+ namespace_param = name
119
+ continue
120
120
121
- elif value .annotation in [str , int , float , bool ]:
122
- # Cast it if its a primitive type. Ensure the type matches the annotation.
123
- bound_args [name ] = value .annotation (params [name ].get_value ())
121
+ # Handle regular parameters
122
+ if name not in params :
123
+ if value .default != inspect .Parameter .empty :
124
+ # Default value is given in the function signature, we can use it
125
+ bound_args [name ] = value .default
126
+ else :
127
+ # This is a required parameter that's missing
128
+ missing_required_args .append (name )
124
129
else :
125
- bound_args [name ] = params [name ].get_value ()
126
-
127
- unassigned_params .remove (name )
128
-
129
- params = {
130
- key : params [key ] for key in unassigned_params
131
- } # remove keys from params if they are assigned
130
+ # We have a parameter of this name, lets bind it
131
+ param_value = params [name ]
132
+
133
+ if (
134
+ inspect .isclass (value .annotation )
135
+ and issubclass (value .annotation , BaseModel )
136
+ ) and not isinstance (param_value , ObjectParameter ):
137
+ # Even if the annotation is a pydantic model, it can be passed as an object parameter
138
+ # We try to cast it as a pydantic model if asked
139
+ named_param = params [name ].get_value ()
140
+
141
+ if not isinstance (named_param , dict ):
142
+ # A case where the parameter is a one attribute model
143
+ named_param = {name : named_param }
144
+
145
+ bound_model = bind_args_for_pydantic_model (
146
+ named_param , value .annotation
147
+ )
148
+ bound_args [name ] = bound_model
149
+
150
+ elif value .annotation in [str , int , float , bool ] and callable (
151
+ value .annotation
152
+ ):
153
+ # Cast it if its a primitive type. Ensure the type matches the annotation.
154
+ try :
155
+ bound_args [name ] = value .annotation (params [name ].get_value ())
156
+ except (ValueError , TypeError ) as e :
157
+ raise ValueError (
158
+ f"Cannot cast parameter '{ name } ' to { value .annotation .__name__ } : { e } "
159
+ )
160
+ else :
161
+ # We do not know type of parameter, we send the value as found
162
+ bound_args [name ] = params [name ].get_value ()
163
+
164
+ # Find extra parameters (parameters in params but not consumed by regular function parameters)
165
+ consumed_param_names = set (bound_args .keys ()) | set (missing_required_args )
166
+ extra_params = {k : v for k , v in params .items () if k not in consumed_param_names }
167
+
168
+ # Second pass: Handle **kwargs and argparse.Namespace parameters
169
+ if var_keyword_param is not None :
170
+ # Function accepts **kwargs - add all extra parameters directly to bound_args
171
+ for param_name , param_value in extra_params .items ():
172
+ bound_args [param_name ] = param_value .get_value ()
173
+ elif namespace_param is not None :
174
+ # Function accepts argparse.Namespace - create namespace with extra parameters
175
+ args_namespace = argparse .Namespace ()
176
+ for param_name , param_value in extra_params .items ():
177
+ setattr (args_namespace , param_name , param_value .get_value ())
178
+ bound_args [namespace_param ] = args_namespace
179
+ elif extra_params :
180
+ # Function doesn't accept **kwargs or namespace, but we have extra parameters
181
+ # This should only be an error if we also have missing required parameters
182
+ # or if the function truly can't handle the extra parameters
183
+ if missing_required_args :
184
+ # We have both missing required and extra parameters - this is an error
185
+ raise ValueError (
186
+ f"Function { func .__name__ } has parameters { missing_required_args } that are not present in the parameters"
187
+ )
188
+ # If we only have extra parameters and no missing required ones, we just ignore the extras
189
+ # This allows for more flexible parameter passing
190
+
191
+ # Check for missing required parameters
192
+ if missing_required_args :
193
+ if var_keyword_param is None and namespace_param is None :
194
+ # No way to handle missing parameters
195
+ raise ValueError (
196
+ f"Function { func .__name__ } has parameters { missing_required_args } that are not present in the parameters"
197
+ )
198
+ # If we have **kwargs or namespace, missing parameters might be handled there
199
+ # But if they're truly required (no default), we should still error
200
+ raise ValueError (
201
+ f"Function { func .__name__ } has parameters { missing_required_args } that are not present in the parameters"
202
+ )
132
203
133
204
return bound_args
134
205
0 commit comments