From 7de055138766971c33b736b322cbf8a883a61734 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20Sch=C3=A4fer?= <david.schaefer@ufz.de>
Date: Thu, 30 Mar 2023 21:28:21 +0200
Subject: [PATCH] Allow function arguments in config syntax

---
 CHANGELOG.md              |  3 ++-
 saqc/parsing/visitor.py   | 43 +++++++++++++++++++++++++++------------
 tests/core/test_reader.py | 22 +++++++++++++++++++-
 3 files changed, 53 insertions(+), 15 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c36faca39..871d5797b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,7 +10,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
 [List of commits](https://git.ufz.de/rdm-software/saqc/-/compare/v2.3.0...develop)
 ### Added
 - Methods `logicalAnd` and `logicalOr`
-- `Flags` supports slicing and column selection with `list` or a `pd.Index`.
+- `Flags` supports slicing and column selection with `list` or a `pd.Index`
 ### Changed
 - Deprecate `interpolate`, `linear` and `shift` in favor of `align`
 - Rename `interplateInvalid` to `interpolate`
@@ -18,6 +18,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
 ### Removed
 - Parameter `limit` from `align`
 ### Fixed
+- `func` arguments in text configurations were not parsed correctly
 - fail on duplicated arguments to test methods
 
 ## [2.3.0](https://git.ufz.de/rdm-software/saqc/-/tags/v2.3.0) - 2023-01-17
diff --git a/saqc/parsing/visitor.py b/saqc/parsing/visitor.py
index 91f086e2f..7224e85a0 100644
--- a/saqc/parsing/visitor.py
+++ b/saqc/parsing/visitor.py
@@ -7,6 +7,9 @@
 # -*- coding: utf-8 -*-
 
 import ast
+import importlib
+
+import numpy as np
 
 from saqc.core.register import FUNC_MAP
 from saqc.parsing.environ import ENVIRONMENT
@@ -127,18 +130,33 @@ class ConfigFunctionParser(ast.NodeVisitor):
         key, value = node.arg, node.value
         check_tree = True
 
+        imports = {}
         if key == "func":
-            visitor = ConfigExpressionParser(value)
-            args = ast.arguments(
-                posonlyargs=[],
-                kwonlyargs=[],
-                kw_defaults=[],
-                defaults=[],
-                args=[ast.arg(arg=a, annotation=None) for a in visitor.args],
-                kwarg=None,
-                vararg=None,
-            )
-            value = ast.Lambda(args=args, body=value)
+            if (isinstance(value, ast.Name) and value.id in ENVIRONMENT) or (
+                isinstance(value, ast.Constant) and value.value in ENVIRONMENT
+            ):
+                func = ENVIRONMENT[
+                    value.id if isinstance(value, ast.Name) else value.value
+                ]
+                # handle the missing attribute for numpy.ufunc
+                module = getattr(func, "__module__", "numpy")
+                if module.startswith("saqc"):
+                    # if it's an saqc function, we need to import the top level package first
+                    imports["saqc"] = importlib.import_module("saqc")
+                imports[module] = importlib.import_module(module)
+                value = ast.parse(f"{module}.{func.__name__}").body[0].value
+            else:
+                visitor = ConfigExpressionParser(value)
+                args = ast.arguments(
+                    posonlyargs=[],
+                    kwonlyargs=[],
+                    kw_defaults=[],
+                    defaults=[],
+                    args=[ast.arg(arg=a, annotation=None) for a in visitor.args],
+                    kwarg=None,
+                    vararg=None,
+                )
+                value = ast.Lambda(args=args, body=value)
             # NOTE:
             # don't pass the generated functions down
             # to the checks implemented in this class...
@@ -159,8 +177,7 @@ class ConfigFunctionParser(ast.NodeVisitor):
             mode="single",
         )
         # NOTE: only pass a copy to not clutter the ENVIRONMENT
-        # try:
-        exec(co, {**ENVIRONMENT}, self.kwargs)
+        exec(co, {**ENVIRONMENT, **imports}, self.kwargs)
 
         # let's do some more validity checks
         if check_tree:
diff --git a/tests/core/test_reader.py b/tests/core/test_reader.py
index c3de90c16..a7d0f2d66 100644
--- a/tests/core/test_reader.py
+++ b/tests/core/test_reader.py
@@ -9,7 +9,9 @@
 import numpy as np
 import pytest
 
-from saqc.core import DictOfSeries, Flags, flagging
+import saqc.lib.ts_operators as ts_ops
+from saqc.core import DictOfSeries, Flags, SaQC, flagging
+from saqc.parsing.environ import ENVIRONMENT
 from saqc.parsing.reader import fromConfig, readFile
 from tests.common import initData, writeIO
 
@@ -155,3 +157,21 @@ def test_supportedArguments(data):
     for test in tests:
         fobj = writeIO(header + "\n" + test)
         fromConfig(fobj, data)
+
+
+@pytest.mark.parametrize(
+    "func_string", [k for k, v in ENVIRONMENT.items() if callable(v)]
+)
+def test_funtionArguments(data, func_string):
+    @flagging()
+    def testFunction(saqc, field, func, **kwargs):
+        assert func is ENVIRONMENT[func_string]
+        return saqc
+
+    config = f"""
+    varname ; test
+    {data.columns[0]} ; testFunction(func={func_string})
+    {data.columns[0]} ; testFunction(func="{func_string}")
+    """
+
+    fromConfig(writeIO(config), data)
-- 
GitLab