This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/main/pr-5603-429be110a72ae9967f39dcc41efa75321467c328
in repository https://gitbox.apache.org/repos/asf/texera.git

commit bcf9e014fee7e742c8d2229424fc2a186c2040ad
Author: carloea2 <[email protected]>
AuthorDate: Fri Jul 24 21:44:44 2026 -0700

    feat(pyamber): support Python UDF UI parameters (#5603)
    
    ### What changes were proposed in this PR?
    
    This PR adds Python runtime support for Python UDF UI parameters.
    
    It introduces:
    
    | Area | Change |
    | --- | --- |
    | PyTexera runtime API | Adds `self.UiParameter(...)` support on Python
    UDF operator base classes. |
    | Runtime injection bridge | Adds the `_texera_injected_ui_parameters`
    base hook that the Scala injector overrides. |
    | Typed parameter parsing | Converts injected UI parameter values into
    Python values using `AttributeType`. |
    | PyTexera exports | Exports `Dict`, `Any`, and `AttributeType` through
    `from pytexera import *`, so generated code from the Scala injector
    loads correctly. |
    | Attribute type compatibility | Adds Python enum aliases for
    `AttributeType.INTEGER` and `AttributeType.BOOLEAN`, matching the
    frontend parser’s accepted tokens. |
    | Follow-up cleanup from PR #5141 | Removes the temporary generated-code
    comment that described this runtime PR as a future dependency. |
    | Test coverage | Adds PyAmber tests for injected values, parsing,
    duplicate declarations, unsupported types, enum aliases, and
    instance-local state. |
    
    This PR is stacked after the merged frontend foundation PR #5043 and
    Scala backend injection PR #5141. It does not wire UI parameters into
    operator execution end to end; that integration is handled by the next
    PR in the stack.
    
    ### Any related issues, documentation, discussions?
    
    Part of the Python UDF UI parameter feature split from
    `feat/ui-parameter`.
    
    Related tracking issue / stack: #5044
    
    Stack order:
    
    1. Frontend UI parameter building blocks: #5043
    2. Scala backend injection model: #5141
    3. Python runtime support: this PR
    4. End-to-end integration
    
    ### How was this PR tested?
    
    Commands run:
    
    ```bash
    cd amber
    ruff check src/main/python/core/models/schema/attribute_type.py 
src/main/python/pytexera/udf/udf_operator.py 
src/test/python/pytexera/udf/test_udf_operator.py
    pytest src/test/python/pytexera/udf/test_udf_operator.py -q
    pytest src/test/python/pytexera/udf -q
    
    ```
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Me
---
 .../python/core/models/schema/attribute_type.py    |  44 +++
 amber/src/main/python/pytexera/__init__.py         |   6 +-
 amber/src/main/python/pytexera/udf/udf_operator.py | 168 +++++++++-
 .../test/python/pytexera/udf/test_udf_operator.py  | 353 +++++++++++++++++++++
 .../udf/python/PythonUdfUiParameterInjector.scala  |   3 +-
 .../python/PythonUdfUiParameterInjectorSpec.scala  |   3 -
 6 files changed, 566 insertions(+), 11 deletions(-)

diff --git a/amber/src/main/python/core/models/schema/attribute_type.py 
b/amber/src/main/python/core/models/schema/attribute_type.py
index 0511937336..0887816c82 100644
--- a/amber/src/main/python/core/models/schema/attribute_type.py
+++ b/amber/src/main/python/core/models/schema/attribute_type.py
@@ -33,8 +33,11 @@ class AttributeType(Enum):
 
     STRING = 1
     INT = 2
+    # Attribute access needs Java names; RAW_TYPE_MAPPING handles raw schema 
strings.
+    INTEGER = 2
     LONG = 3
     BOOL = 4
+    BOOLEAN = 4
     DOUBLE = 5
     TIMESTAMP = 6
     BINARY = 7
@@ -78,6 +81,47 @@ FROM_ARROW_MAPPING = {
 }
 
 
+def _is_empty_value(v):
+    return v is None or (isinstance(v, str) and v.strip() == "")
+
+
+def _parse_bool(v):
+    if _is_empty_value(v):
+        return False
+
+    normalized_value = str(v).strip().lower()
+    if normalized_value == "true":
+        return True
+    if normalized_value == "false":
+        return False
+    return float(normalized_value) != 0
+
+
+def _parse_timestamp(v):
+    if _is_empty_value(v):
+        return datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
+
+    normalized_value = str(v)
+    if normalized_value.endswith("Z"):
+        normalized_value = normalized_value[:-1] + "+00:00"
+    parsed_value = datetime.datetime.fromisoformat(normalized_value)
+    if (
+        parsed_value.tzinfo is None
+        or parsed_value.tzinfo.utcoffset(parsed_value) is None
+    ):
+        return parsed_value.replace(tzinfo=datetime.timezone.utc)
+    return parsed_value
+
+
+FROM_STRING_PARSER_MAPPING = {
+    AttributeType.STRING: str,
+    AttributeType.INT: lambda v: 0 if _is_empty_value(v) else int(v),
+    AttributeType.LONG: lambda v: 0 if _is_empty_value(v) else int(v),
+    AttributeType.DOUBLE: lambda v: 0.0 if _is_empty_value(v) else float(v),
+    AttributeType.BOOL: _parse_bool,
+    AttributeType.TIMESTAMP: _parse_timestamp,
+}
+
 # Only single-directional mapping.
 TO_PYOBJECT_MAPPING = {
     AttributeType.STRING: str,
diff --git a/amber/src/main/python/pytexera/__init__.py 
b/amber/src/main/python/pytexera/__init__.py
index c600166738..4c5db21491 100644
--- a/amber/src/main/python/pytexera/__init__.py
+++ b/amber/src/main/python/pytexera/__init__.py
@@ -17,7 +17,7 @@
 
 from loguru import logger
 from overrides import overrides
-from typing import Iterator, Optional, Union
+from typing import Iterator, Optional, Union, Dict, Any
 
 from core.models.operator import LoopStartOperator, LoopEndOperator
 from pyamber import *
@@ -31,6 +31,7 @@ from .udf.udf_operator import (
     UDFSourceOperator,
 )
 from core.models.type.large_binary import largebinary
+from core.models.schema.attribute_type import AttributeType
 
 __all__ = [
     "State",
@@ -56,4 +57,7 @@ __all__ = [
     "Iterator",
     "Optional",
     "Union",
+    "Dict",
+    "Any",
+    "AttributeType",
 ]
diff --git a/amber/src/main/python/pytexera/udf/udf_operator.py 
b/amber/src/main/python/pytexera/udf/udf_operator.py
index 003225c75c..7c0fd239c7 100644
--- a/amber/src/main/python/pytexera/udf/udf_operator.py
+++ b/amber/src/main/python/pytexera/udf/udf_operator.py
@@ -16,12 +16,170 @@
 # under the License.
 
 from abc import abstractmethod
-from typing import Iterator, Optional, Union
+from dataclasses import dataclass
+import functools
+from typing import Any, Dict, Iterator, Optional, Set, Union
 
 from pyamber import *
+from core.models.schema.attribute_type import AttributeType, 
FROM_STRING_PARSER_MAPPING
+from loguru import logger
 
 
-class UDFOperatorV2(TupleOperatorV2):
+@dataclass(frozen=True)
+class _UiParameterValue:
+    name: str
+    type: AttributeType
+    value: Any
+
+
+class _UiParameterSupport:
+    _ui_parameter_injected_values: Dict[str, Any]
+    _ui_parameter_name_types: Dict[str, AttributeType]
+    _ui_parameter_used_names: Set[str]
+    _unsupported_ui_parameter_types = {
+        AttributeType.BINARY,
+        AttributeType.LARGE_BINARY,
+    }
+
+    # Reserved hook name. Backend injector will generate this in the user's 
class.
+    def _texera_injected_ui_parameters(self) -> Dict[str, Any]:
+        return {}
+
+    def _ensure_ui_parameter_state(self) -> None:
+        if "_ui_parameter_injected_values" not in self.__dict__:
+            self._ui_parameter_injected_values = {}
+        if "_ui_parameter_name_types" not in self.__dict__:
+            self._ui_parameter_name_types = {}
+        if "_ui_parameter_used_names" not in self.__dict__:
+            self._ui_parameter_used_names = set()
+
+    def _texera_apply_injected_ui_parameters(self) -> None:
+        self._ensure_ui_parameter_state()
+        values = self._texera_injected_ui_parameters()
+        self._ui_parameter_injected_values = dict(values or {})
+        self._ui_parameter_name_types = {}
+        self._ui_parameter_used_names = set()
+
+    def _warn_unused_injected_ui_parameters(self) -> None:
+        unused_names = sorted(
+            set(self._ui_parameter_injected_values) - 
self._ui_parameter_used_names
+        )
+        if unused_names:
+            logger.warning(
+                "Injected UI parameter value(s) were not used: {}.",
+                ", ".join(unused_names),
+            )
+
+    def __init_subclass__(cls, **kwargs):
+        super().__init_subclass__(**kwargs)
+
+        original_open = getattr(cls, "open", None)
+        if original_open is None:
+            return
+
+        if getattr(original_open, "__texera_ui_params_wrapped__", False):
+            return
+
+        @functools.wraps(original_open)
+        def wrapped_open(self, *args, **kwargs):
+            """Apply injected UI parameters once before the outermost 
open()."""
+            if getattr(self, "_ui_parameter_open_in_progress", False):
+                return original_open(self, *args, **kwargs)
+
+            self._ui_parameter_open_in_progress = True
+            try:
+                self._texera_apply_injected_ui_parameters()
+                result = original_open(self, *args, **kwargs)
+                self._warn_unused_injected_ui_parameters()
+                return result
+            finally:
+                self._ui_parameter_open_in_progress = False
+
+        setattr(wrapped_open, "__texera_ui_params_wrapped__", True)
+        cls.open = wrapped_open
+
+    def UiParameter(
+        self, name: str, attr_type: Optional[AttributeType] = None, **kwargs: 
Any
+    ) -> _UiParameterValue:
+        """
+        Return the current UI parameter value parsed as attr_type.
+
+        Re-reading the same name with the same type is idempotent. Reusing a
+        name with a different type is rejected because the parsed value would 
be
+        ambiguous.
+        """
+        if "type" in kwargs:
+            if attr_type is not None:
+                raise TypeError("UiParameter.type was provided multiple 
times.")
+            attr_type = kwargs.pop("type")
+
+        if kwargs:
+            unexpected_arguments = ", ".join(sorted(kwargs))
+            raise TypeError(
+                f"UiParameter got unexpected keyword argument(s): "
+                f"{unexpected_arguments}."
+            )
+
+        if attr_type is None:
+            raise TypeError("UiParameter.type is required.")
+
+        if not isinstance(attr_type, AttributeType):
+            raise TypeError(
+                f"UiParameter.type must be an AttributeType, got 
{attr_type!r}."
+            )
+
+        self._ensure_ui_parameter_state()
+        existing_type = self._ui_parameter_name_types.get(name)
+        if existing_type is not None and existing_type != attr_type:
+            raise ValueError(
+                f"Duplicate UiParameter name '{name}' with conflicting types: "
+                f"{existing_type.name} vs {attr_type.name}."
+            )
+
+        self._ui_parameter_name_types[name] = attr_type
+        if name in self._ui_parameter_injected_values:
+            self._ui_parameter_used_names.add(name)
+            raw_value = self._ui_parameter_injected_values[name]
+        else:
+            logger.warning(
+                "No injected UI parameter value found for name '{}'.",
+                name,
+            )
+            raw_value = None
+
+        return _UiParameterValue(
+            name=name,
+            type=attr_type,
+            value=self._parse(raw_value, attr_type),
+        )
+
+    @staticmethod
+    def _parse(value: Any, attr_type: AttributeType) -> Any:
+        if attr_type in _UiParameterSupport._unsupported_ui_parameter_types:
+            raise ValueError(
+                f"UiParameter does not support {attr_type.name} values. "
+                "Use a supported type instead."
+            )
+
+        parser = FROM_STRING_PARSER_MAPPING.get(attr_type)
+        if parser is None:
+            raise TypeError(
+                f"UiParameter.type {attr_type!r} is not supported for parsing."
+            )
+
+        if value is None:
+            return None
+
+        try:
+            return parser(value)
+        except Exception as e:
+            raise ValueError(
+                f"Failed to parse UiParameter value {value!r} as 
{attr_type.name}. "
+                f"Please provide a valid {attr_type.name.lower()} value."
+            ) from e
+
+
+class UDFOperatorV2(_UiParameterSupport, TupleOperatorV2):
     """
     Base class for tuple-oriented user-defined operators. A concrete 
implementation must
     be provided upon using.
@@ -65,7 +223,7 @@ class UDFOperatorV2(TupleOperatorV2):
         pass
 
 
-class UDFSourceOperator(SourceOperator):
+class UDFSourceOperator(_UiParameterSupport, SourceOperator):
     def open(self) -> None:
         """
         Open a context of the operator. Usually can be used for 
loading/initiating some
@@ -90,7 +248,7 @@ class UDFSourceOperator(SourceOperator):
         pass
 
 
-class UDFTableOperator(TableOperator):
+class UDFTableOperator(_UiParameterSupport, TableOperator):
     """
     Base class for table-oriented user-defined operators. A concrete 
implementation must
     be provided upon using.
@@ -123,7 +281,7 @@ class UDFTableOperator(TableOperator):
         pass
 
 
-class UDFBatchOperator(BatchOperator):
+class UDFBatchOperator(_UiParameterSupport, BatchOperator):
     """
     Base class for batch-oriented user-defined operators. A concrete 
implementation must
     be provided upon using.
diff --git a/amber/src/test/python/pytexera/udf/test_udf_operator.py 
b/amber/src/test/python/pytexera/udf/test_udf_operator.py
new file mode 100644
index 0000000000..b96199d450
--- /dev/null
+++ b/amber/src/test/python/pytexera/udf/test_udf_operator.py
@@ -0,0 +1,353 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import datetime
+from typing import Iterator, Optional
+
+import pytest
+import pytexera.udf.udf_operator as udf_operator
+
+from pytexera import AttributeType, Tuple, TupleLike, UDFOperatorV2
+from pytexera.udf.udf_operator import _UiParameterSupport
+
+
+class InjectedParametersOperator(UDFOperatorV2):
+    def _texera_injected_ui_parameters(self):
+        return {
+            "count": "7",
+            "enabled": "1",
+            "created_at": "2024-01-01T00:00:00",
+        }
+
+    def open(self):
+        self.count_parameter = self.UiParameter("count", AttributeType.INT)
+        self.enabled_parameter = self.UiParameter(
+            name="enabled", type=AttributeType.BOOL
+        )
+        self.created_at_parameter = self.UiParameter(
+            "created_at", type=AttributeType.TIMESTAMP
+        )
+
+    def process_tuple(self, tuple_: Tuple, port: int) -> 
Iterator[Optional[TupleLike]]:
+        yield tuple_
+
+
+class ConflictingParameterOperator(UDFOperatorV2):
+    def _texera_injected_ui_parameters(self):
+        return {"duplicate": "1"}
+
+    def open(self):
+        self.UiParameter("duplicate", AttributeType.INT)
+        self.UiParameter("duplicate", AttributeType.STRING)
+
+    def process_tuple(self, tuple_: Tuple, port: int) -> 
Iterator[Optional[TupleLike]]:
+        yield tuple_
+
+
+class RepeatedParameterOperator(UDFOperatorV2):
+    def _texera_injected_ui_parameters(self):
+        return {"duplicate": "1"}
+
+    def open(self):
+        self.first_parameter = self.UiParameter("duplicate", AttributeType.INT)
+        self.second_parameter = self.UiParameter("duplicate", 
AttributeType.INT)
+
+    def process_tuple(self, tuple_: Tuple, port: int) -> 
Iterator[Optional[TupleLike]]:
+        yield tuple_
+
+
+class FirstIndependentParameterOperator(UDFOperatorV2):
+    def _texera_injected_ui_parameters(self):
+        return {"count": "1"}
+
+    def open(self):
+        self.count_parameter = self.UiParameter("count", AttributeType.INT)
+
+    def process_tuple(self, tuple_: Tuple, port: int) -> 
Iterator[Optional[TupleLike]]:
+        yield tuple_
+
+
+class SecondIndependentParameterOperator(UDFOperatorV2):
+    def _texera_injected_ui_parameters(self):
+        return {"count": "2"}
+
+    def open(self):
+        self.count_parameter = self.UiParameter("count", AttributeType.INT)
+
+    def process_tuple(self, tuple_: Tuple, port: int) -> 
Iterator[Optional[TupleLike]]:
+        yield tuple_
+
+
+class MissingParameterOperator(UDFOperatorV2):
+    def _texera_injected_ui_parameters(self):
+        return {"sent": "1"}
+
+    def open(self):
+        self.missing_parameter = self.UiParameter("missing", AttributeType.INT)
+
+    def process_tuple(self, tuple_: Tuple, port: int) -> 
Iterator[Optional[TupleLike]]:
+        yield tuple_
+
+
+class UnusedParameterOperator(UDFOperatorV2):
+    def _texera_injected_ui_parameters(self):
+        return {"used": "1", "unused": "2"}
+
+    def open(self):
+        self.used_parameter = self.UiParameter("used", AttributeType.INT)
+
+    def process_tuple(self, tuple_: Tuple, port: int) -> 
Iterator[Optional[TupleLike]]:
+        yield tuple_
+
+
+class SuperOpenParameterOperator(UDFOperatorV2):
+    def __init__(self):
+        self.hook_call_count = 0
+
+    def _texera_injected_ui_parameters(self):
+        self.hook_call_count += 1
+        return {"count": "3"}
+
+    def open(self):
+        super().open()
+        self.count_parameter = self.UiParameter("count", AttributeType.INT)
+
+    def process_tuple(self, tuple_: Tuple, port: int) -> 
Iterator[Optional[TupleLike]]:
+        yield tuple_
+
+
+class SuperOpenConflictingParameterOperator(UDFOperatorV2):
+    def _texera_injected_ui_parameters(self):
+        return {"duplicate": "1"}
+
+    def open(self):
+        self.UiParameter("duplicate", AttributeType.INT)
+        super().open()
+        self.UiParameter("duplicate", AttributeType.STRING)
+
+    def process_tuple(self, tuple_: Tuple, port: int) -> 
Iterator[Optional[TupleLike]]:
+        yield tuple_
+
+
+class TestUiParameterSupport:
+    def test_injected_values_are_applied_before_open(self):
+        operator = InjectedParametersOperator()
+
+        operator.open()
+
+        assert operator.count_parameter.value == 7
+        assert operator.enabled_parameter.value is True
+        assert operator.created_at_parameter.value == datetime.datetime(
+            2024, 1, 1, 0, 0, tzinfo=datetime.timezone.utc
+        )
+
+    def test_duplicate_parameter_names_with_conflicting_types_raise(self):
+        operator = ConflictingParameterOperator()
+
+        with pytest.raises(ValueError) as exc_info:
+            operator.open()
+
+        assert "Duplicate UiParameter name 'duplicate'" in str(exc_info.value)
+
+    def test_duplicate_parameter_names_with_same_type_succeed(self):
+        operator = RepeatedParameterOperator()
+
+        operator.open()
+
+        assert operator.first_parameter.value == 1
+        assert operator.second_parameter.value == 1
+        assert operator.first_parameter.type is AttributeType.INT
+        assert operator.second_parameter.type is AttributeType.INT
+
+    @pytest.mark.parametrize(
+        ("raw_value", "attr_type", "expected"),
+        [
+            ("hello", AttributeType.STRING, "hello"),
+            ("7", AttributeType.INT, 7),
+            ("99", AttributeType.LONG, 99),
+            ("3.14", AttributeType.DOUBLE, 3.14),
+            ("1", AttributeType.BOOL, True),
+            (
+                "2024-01-01T00:00:00",
+                AttributeType.TIMESTAMP,
+                datetime.datetime(2024, 1, 1, 0, 0, 
tzinfo=datetime.timezone.utc),
+            ),
+            (
+                "2024-01-01T00:00:00Z",
+                AttributeType.TIMESTAMP,
+                datetime.datetime(2024, 1, 1, 0, 0, 
tzinfo=datetime.timezone.utc),
+            ),
+        ],
+    )
+    def test_parse_supported_types(self, raw_value, attr_type, expected):
+        assert _UiParameterSupport._parse(raw_value, attr_type) == expected
+
+    @pytest.mark.parametrize(
+        ("raw_value", "attr_type", "expected"),
+        [
+            ("", AttributeType.INT, 0),
+            ("   ", AttributeType.LONG, 0),
+            ("", AttributeType.DOUBLE, 0.0),
+            (
+                "",
+                AttributeType.TIMESTAMP,
+                datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc),
+            ),
+        ],
+    )
+    def test_parse_empty_values(self, raw_value, attr_type, expected):
+        assert _UiParameterSupport._parse(raw_value, attr_type) == expected
+
+    def test_java_attribute_type_aliases_parse_like_python_names(self):
+        assert AttributeType.INTEGER is AttributeType.INT
+        assert AttributeType.BOOLEAN is AttributeType.BOOL
+        assert _UiParameterSupport._parse("7", AttributeType.INTEGER) == 7
+        assert _UiParameterSupport._parse("false", AttributeType.BOOLEAN) is 
False
+
+    @pytest.mark.parametrize(
+        ("raw_value", "expected"),
+        [
+            ("", False),
+            ("   ", False),
+            ("True", True),
+            ("true", True),
+            ("1", True),
+            ("1.0", True),
+            ("2", True),
+            ("-1", True),
+            ("False", False),
+            ("false", False),
+            ("0", False),
+            ("0.0", False),
+        ],
+    )
+    def test_parse_bool_string_values(self, raw_value, expected):
+        assert _UiParameterSupport._parse(raw_value, AttributeType.BOOL) is 
expected
+
+    @pytest.mark.parametrize(
+        ("raw_value", "attr_type", "expected_message"),
+        [
+            (
+                "payload",
+                AttributeType.BINARY,
+                "UiParameter does not support BINARY values",
+            ),
+            (
+                "s3://bucket/path/to/object",
+                AttributeType.LARGE_BINARY,
+                "UiParameter does not support LARGE_BINARY values",
+            ),
+            (
+                None,
+                AttributeType.BINARY,
+                "UiParameter does not support BINARY values",
+            ),
+        ],
+    )
+    def test_parse_binary_types_raise_helpful_error(
+        self, raw_value, attr_type, expected_message
+    ):
+        with pytest.raises(ValueError, match=expected_message):
+            _UiParameterSupport._parse(raw_value, attr_type)
+
+    def test_parse_unsupported_type_raises_helpful_error(self):
+        with pytest.raises(TypeError, match="UiParameter.type .* is not 
supported"):
+            _UiParameterSupport._parse("value", object())
+
+    def test_missing_injected_name_returns_none_and_warns(self, monkeypatch):
+        operator = MissingParameterOperator()
+        warning_calls = []
+        monkeypatch.setattr(
+            udf_operator.logger,
+            "warning",
+            lambda msg, *args, **kwargs: 
warning_calls.append(msg.format(*args)),
+        )
+
+        operator.open()
+
+        assert operator.missing_parameter.value is None
+        assert any(
+            "No injected UI parameter value found for name 'missing'" in call
+            for call in warning_calls
+        )
+
+    def test_unused_injected_name_warns(self, monkeypatch):
+        operator = UnusedParameterOperator()
+        warning_calls = []
+        monkeypatch.setattr(
+            udf_operator.logger,
+            "warning",
+            lambda msg, *args, **kwargs: 
warning_calls.append(msg.format(*args)),
+        )
+
+        operator.open()
+
+        assert operator.used_parameter.value == 1
+        assert warning_calls == [
+            "Injected UI parameter value(s) were not used: unused."
+        ]
+
+    def test_ui_parameter_argument_errors(self):
+        operator = MissingParameterOperator()
+
+        with pytest.raises(TypeError, match="provided multiple times"):
+            operator.UiParameter("count", AttributeType.INT, 
type=AttributeType.INT)
+        with pytest.raises(TypeError, match="unexpected keyword argument"):
+            operator.UiParameter("count", AttributeType.INT, value="1")
+        with pytest.raises(TypeError, match="UiParameter.type is required"):
+            operator.UiParameter("count")
+        with pytest.raises(TypeError, match="must be an AttributeType"):
+            operator.UiParameter("count", object())
+
+    def test_super_open_applies_injected_values_once(self):
+        operator = SuperOpenParameterOperator()
+
+        operator.open()
+
+        assert operator.hook_call_count == 1
+        assert operator.count_parameter.value == 3
+
+    def test_super_open_does_not_reset_duplicate_tracking(self):
+        operator = SuperOpenConflictingParameterOperator()
+
+        with pytest.raises(ValueError, match="Duplicate UiParameter name"):
+            operator.open()
+
+    def test_wrapped_open_uses_instance_local_state(self):
+        assert (
+            getattr(
+                FirstIndependentParameterOperator.open,
+                "__texera_ui_params_wrapped__",
+                False,
+            )
+            is True
+        )
+
+        first_operator = FirstIndependentParameterOperator()
+        second_operator = SecondIndependentParameterOperator()
+
+        first_operator.open()
+        second_operator.open()
+
+        assert first_operator.count_parameter.value == 1
+        assert second_operator.count_parameter.value == 2
+        assert first_operator._ui_parameter_injected_values == {"count": "1"}
+        assert second_operator._ui_parameter_injected_values == {"count": "2"}
+        assert (
+            first_operator._ui_parameter_injected_values
+            is not second_operator._ui_parameter_injected_values
+        )
diff --git 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUdfUiParameterInjector.scala
 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUdfUiParameterInjector.scala
index 13c25a436e..7b26761290 100644
--- 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUdfUiParameterInjector.scala
+++ 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUdfUiParameterInjector.scala
@@ -85,8 +85,7 @@ object PythonUdfUiParameterInjector {
   private def buildInjectedHookMethod(uiParameters: List[UiUDFParameter]): 
String = {
     val injectedParametersMap = buildInjectedParametersMap(uiParameters)
 
-    (pyb"""|# Follow-up runtime support exports Dict/Any and defines the base 
hook that @overrides targets.
-           |@overrides
+    (pyb"""|@overrides
            |$InjectedUiParametersHookMethodHeader
            |    return {""" +
       injectedParametersMap +
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonUdfUiParameterInjectorSpec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonUdfUiParameterInjectorSpec.scala
index d5a2534758..a741dfbf03 100644
--- 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonUdfUiParameterInjectorSpec.scala
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonUdfUiParameterInjectorSpec.scala
@@ -104,9 +104,6 @@ class PythonUdfUiParameterInjectorSpec extends AnyFlatSpec 
with Matchers {
     val injectedCode = inject(uiParameter("date", AttributeType.TIMESTAMP, 
"2024-01-01T00:00:00Z"))
 
     injectedCode should include("class ProcessTupleOperator(UDFOperatorV2):")
-    injectedCode should include(
-      "# Follow-up runtime support exports Dict/Any and defines the base hook 
that @overrides targets."
-    )
     injectedCode should include("def _texera_injected_ui_parameters(self) -> 
Dict[str, Any]:")
     injectedCode should include("return {")
     injectedCode should include("self.decode_python_template")

Reply via email to