@@ -259,10 +259,101 @@ def wrapper_body(**kwargs):
259259
260260 return create_dynamically_parameterized_wrapper (test_func , wrapper_body , test_param_names )
261261
262- wrapper = create_wrapper_with_signature ()
263- wrapper = pytest . mark . parametrize ( test_param_names , param_tuples )( wrapper )
264- wrapper . original_evaluation_test_func = test_func
262+ # Create the pytest wrapper
263+ pytest_wrapper = create_wrapper_with_signature ( )
264+ pytest_wrapper = pytest . mark . parametrize ( test_param_names , param_tuples )( pytest_wrapper )
265265
266- return wrapper
266+ def create_dual_mode_wrapper () -> Callable :
267+ """
268+ Creates a wrapper that supports both pytest parameterized execution and direct function calls.
269+
270+ This wrapper enables the decorated evaluation test function to be used in two ways:
271+ 1. As a pytest test (via pytest.mark.parametrize) with full parameterization
272+ 2. As a direct function call with EvaluationRow data for programmatic use
273+
274+ The wrapper automatically detects the calling pattern and routes to the appropriate
275+ execution path, ensuring consistent behavior regardless of how the function is invoked.
276+
277+ Returns:
278+ A callable that can handle both pytest test execution and direct function calls
279+ """
280+ import asyncio
281+
282+ # Check if the test function is async
283+ is_async = asyncio .iscoroutinefunction (test_func )
284+
285+ if is_async :
286+
287+ async def dual_mode_wrapper (* args , ** kwargs ):
288+ # Check if this is a direct call with the expected signature
289+ if mode == "pointwise" :
290+ # For pointwise mode, check if called with a single row argument
291+ if len (args ) == 1 and isinstance (args [0 ], EvaluationRow ) and not kwargs :
292+ return await test_func (row = args [0 ])
293+ else :
294+ # For batch mode, check if called with rows argument
295+ if (
296+ len (args ) == 1
297+ and isinstance (args [0 ], list )
298+ and all (isinstance (r , EvaluationRow ) for r in args [0 ])
299+ and not kwargs
300+ ):
301+ return await test_func (rows = args [0 ])
302+ # Also check if called with keyword argument 'rows'
303+ if (
304+ len (args ) == 0
305+ and "rows" in kwargs
306+ and isinstance (kwargs ["rows" ], list )
307+ and all (isinstance (r , EvaluationRow ) for r in kwargs ["rows" ])
308+ ):
309+ return await test_func (** kwargs )
310+
311+ # If not a direct call, use the pytest wrapper
312+ return pytest_wrapper (* args , ** kwargs )
313+
314+ else :
315+
316+ def dual_mode_wrapper (* args , ** kwargs ):
317+ # Check if this is a direct call with the expected signature
318+ if mode == "pointwise" :
319+ # For pointwise mode, check if called with a single row argument
320+ if len (args ) == 1 and isinstance (args [0 ], EvaluationRow ) and not kwargs :
321+ return test_func (row = args [0 ])
322+
323+ if len (args ) == 0 and "row" in kwargs and isinstance (kwargs ["row" ], EvaluationRow ):
324+ return test_func (** kwargs )
325+ else :
326+ # For batch mode, check if called with rows argument
327+ if (
328+ len (args ) == 1
329+ and isinstance (args [0 ], list )
330+ and all (isinstance (r , EvaluationRow ) for r in args [0 ])
331+ and not kwargs
332+ ):
333+ return test_func (rows = args [0 ])
334+ # Also check if called with keyword argument 'rows'
335+ if (
336+ len (args ) == 0
337+ and "rows" in kwargs
338+ and isinstance (kwargs ["rows" ], list )
339+ and all (isinstance (r , EvaluationRow ) for r in kwargs ["rows" ])
340+ ):
341+ return test_func (** kwargs )
342+
343+ # If not a direct call, use the pytest wrapper
344+ return pytest_wrapper (* args , ** kwargs )
345+
346+ # Copy all attributes from the pytest wrapper to our dual mode wrapper
347+ import functools
348+
349+ functools .update_wrapper (dual_mode_wrapper , pytest_wrapper )
350+ dual_mode_wrapper .original_evaluation_test_func = test_func
351+
352+ return dual_mode_wrapper
353+
354+ # Create the dual mode wrapper
355+ dual_mode_wrapper = create_dual_mode_wrapper ()
356+
357+ return dual_mode_wrapper
267358
268359 return decorator
0 commit comments