Copilot commented on code in PR #4268:
URL: https://github.com/apache/texera/pull/4268#discussion_r2907501231


##########
amber/src/main/python/pytexera/udf/udf_operator.py:
##########
@@ -16,12 +16,76 @@
 # under the License.
 
 from abc import abstractmethod
-from typing import Iterator, Optional, Union
-
-from pyamber import *
+from typing import Any, Dict, Iterator, Optional, Union
 
+import functools
 
-class UDFOperatorV2(TupleOperatorV2):
+from pyamber import *
+from core.models.schema.attribute_type import AttributeType, 
FROM_STRING_PARSER_MAPPING
+
+class _UiParameterSupport:
+    _ui_parameter_injected_values: Dict[str, Any] = {}
+    _ui_parameter_name_types: Dict[str, AttributeType] = {}
+
+    # Reserved hook name. Backend injector will generate this in the user's 
class.
+    def _texera_injected_ui_parameters(self) -> Dict[str, Any]:
+        return {}
+
+    def _texera_apply_injected_ui_parameters(self) -> None:
+        values = self._texera_injected_ui_parameters()
+        # Write to base class storage (not cls) because UiParameter reads from 
_UiParameterSupport directly
+        _UiParameterSupport._ui_parameter_injected_values = dict(values or {})
+        _UiParameterSupport._ui_parameter_name_types = {}

Review Comment:
   `_ui_parameter_injected_values` and `_ui_parameter_name_types` are 
class-level dictionaries on `_UiParameterSupport` (lines 27-28). This means all 
subclass instances share the same state. If multiple UDF operator instances run 
concurrently in the same Python worker process (e.g., due to parallelism), one 
operator's `_texera_apply_injected_ui_parameters()` call could overwrite 
another operator's injected values, causing incorrect parameter values to be 
read. Each operator instance should have its own storage, for example by using 
instance-level variables instead of class-level ones.



##########
frontend/src/app/workspace/service/code-editor/ui-udf-parameters-parser.service.spec.ts:
##########
@@ -0,0 +1,79 @@
+/**
+ * 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 { UiUdfParametersParserService } from 
"./ui-udf-parameters-parser.service";
+
+describe("UiUdfParametersParserService", () => {
+  let service: UiUdfParametersParserService;
+
+  beforeEach(() => {
+    service = new UiUdfParametersParserService();
+  });
+
+  it("should parse positional and name-based arguments", () => {
+    const code = `
+      class ProcessTupleOperator(UDFOperatorV2):
+          def open(self):
+              self.UiParameter(AttributeType.INT, "count")
+              self.UiParameter(type=AttributeType.STRING, name="name")
+              self.UiParameter(name="age", type=AttributeType.LONG)
+              self.UiParameter(AttributeType.DOUBLE, name="score")
+              self.UiParameter("created_at", type=AttributeType.TIMESTAMP)
+    `;

Review Comment:
   There is a mismatch between the test code calling convention and the actual 
Python `UiParameter` signature. The test uses 
`self.UiParameter(AttributeType.INT, "count")` — type first, name second. 
However, the Python `UiParameter.__init__` signature is `def __init__(self, 
name: str, type: AttributeType)` (name first, type second). If a user follows 
this positional call pattern from the test, calling 
`self.UiParameter(AttributeType.INT, "count")` in Python would fail with a 
`TypeError` since `AttributeType.INT` would be passed as `name` and `"count"` 
as `type`. While the frontend parser correctly handles both orderings (since it 
detects positional tokens by pattern matching), the test inadvertently 
documents an unsupported Python call pattern. The test should use the correct 
Python positional call convention, or strictly use keyword arguments.



##########
frontend/src/app/workspace/component/ui-udf-parameters/ui-udf-parameters.component.html:
##########
@@ -0,0 +1,55 @@
+<!--
+ 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.
+-->
+<div
+  class="ui-udf-param-list"
+  *ngIf="(model?.length ?? 0) > 0">
+  <!-- Optional header row -->
+  <div class="ui-udf-param-row header">
+    <div class="field-cell"><span class="col-title">Value</span></div>
+    <div class="field-cell"><span class="col-title">Name</span></div>
+    <div class="field-cell"><span class="col-title">Type</span></div>
+  </div>
+
+  <div
+    class="ui-udf-param-row"
+    *ngFor="let param of (model || []); let i = index; trackBy: 
trackByParamName">
+    <ng-container *ngIf="field.fieldGroup?.[i] as rowField">
+      <!-- Value -->
+      <div class="field-cell">
+        <ng-container *ngIf="getValueField(rowField) as valueField">
+          <formly-field [field]="valueField"></formly-field>
+        </ng-container>
+      </div>
+
+      <!-- Name -->
+      <div class="field-cell">
+        <ng-container *ngIf="getNameField(rowField) as nameField">
+          <formly-field [field]="nameField"></formly-field>
+        </ng-container>
+      </div>
+
+      <!-- Type -->
+      <div class="field-cell">
+        <ng-container *ngIf="getTypeField(rowField) as typeField">
+          <formly-field [field]="typeField"></formly-field>
+        </ng-container>
+      </div>
+    </ng-container>
+  </div>
+</div>

Review Comment:
   The column order in the UI template renders "Value", "Name", "Type" (left to 
right) across both the header row and data rows. However, the PR description 
screenshot shows the column order as "Name", "Type", "Value". This is a 
discrepancy between the PR description and the actual code. The PR description 
screenshot likely shows the intended UX (Name and Type are read-only attributes 
that describe the parameter, while Value is what users edit). The current code 
places the editable "Value" column first, which may be confusing since users 
typically scan left-to-right and seeing Value before knowing the Name/Type of 
the parameter is less intuitive.



##########
frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.ts:
##########
@@ -47,11 +47,12 @@ import "@codingame/monaco-vscode-python-default-extension";
 import "@codingame/monaco-vscode-r-default-extension";
 import "@codingame/monaco-vscode-java-default-extension";
 import { isDefined } from "../../../common/util/predicate";
-import { filter, switchMap } from "rxjs/operators";
+import { debounceTime, filter, switchMap } from "rxjs/operators";

Review Comment:
   `debounceTime` is imported but never used in this file. The commented-out 
`pythonCodeChangeSubject` field on line 106 suggests this was intended for 
throttling code change events, but the implementation was changed to use 
`yCode.observe(handler)` directly without debouncing. The unused import should 
be removed to keep the code clean.



##########
frontend/src/app/common/formly/collab-wrapper/collab-wrapper/collab-wrapper.component.css:
##########
@@ -25,7 +25,7 @@
   outline: transparent;
   overflow: visible;
   position: relative;
-  : 1pt;
+  :1pt;

Review Comment:
   The CSS rule on line 28 is invalid: `:1pt;` is not a valid CSS declaration — 
it has no property name. This appears to be a stray/broken rule. The original 
code before this change was `  : 1pt;` which was also invalid, but the 
refactoring of removing the leading space didn't fix it. This line should 
either be removed entirely or corrected to a valid CSS property (e.g., `border: 
1pt;`).



##########
common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUdfUiParameterInjector.scala:
##########
@@ -0,0 +1,196 @@
+/*
+ * 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.
+ */
+package org.apache.texera.amber.operator.udf.python
+
+import org.apache.texera.amber.pybuilder.PythonTemplateBuilder
+import 
org.apache.texera.amber.pybuilder.PythonTemplateBuilder.PythonTemplateBuilderStringContext
+
+import scala.util.matching.Regex
+
+object PythonUdfUiParameterInjector {
+
+  private val ReservedHookMethod = "_texera_injected_ui_parameters"
+
+  // Match user-facing UDF classes (the ones users write)
+  private val SupportedUserClassRegex: Regex =
+    """(?m)^([ 
\t]*)class\s+(ProcessTupleOperator|ProcessBatchOperator|ProcessTableOperator|GenerateOperator)\s*\([^)]*\)\s*:\s*(?:#.*)?$""".r
+
+  private def validate(uiParameters: List[UiUDFParameter]): Unit = {
+    uiParameters.foreach { parameter =>
+      if (parameter.attribute == null) {
+        throw new RuntimeException("UiParameter attribute is required.")
+      }
+    }
+
+    val grouped = uiParameters.groupBy(_.attribute.getName)
+    grouped.foreach {
+      case (key, values) =>
+        val typeSet = values.map(_.attribute.getType).toSet
+        if (typeSet.size > 1) {
+          throw new RuntimeException(
+            s"UiParameter key '$key' has multiple types: 
${typeSet.map(_.name()).mkString(",")}."
+          )
+        }
+    }
+  }
+
+  private def buildInjectedParametersMap(
+      uiParameters: List[UiUDFParameter]
+  ): PythonTemplateBuilder = {
+    val entries = uiParameters.map { parameter =>
+      pyb"'${parameter.attribute.getName()}': ${parameter.value}"
+    }
+
+    entries.reduceOption((acc, entry) => acc + pyb", " + 
entry).getOrElse(pyb"")
+  }
+
+  private def buildInjectedHookMethod(uiParameters: List[UiUDFParameter]): 
String = {
+    val injectedParametersMap = buildInjectedParametersMap(uiParameters)
+
+    // unindented method; we indent it when inserting into the class body
+    (pyb"""|@overrides
+            |def """ + pyb"$ReservedHookMethod" + pyb"""(self) -> 
typing.Dict[str, typing.Any]:
+                                                       |    return {""" +
+      injectedParametersMap +
+      pyb"""}
+             |""").encode
+  }
+
+  private def ensureTypingImport(encodedUserCode: String): String = {
+    val alreadyImported = encodedUserCode
+      .split("\n", -1)
+      .exists(_.trim == "import typing")
+
+    if (alreadyImported) {
+      encodedUserCode
+    } else {
+      "import typing\n" + encodedUserCode
+    }
+  }
+
+  private def indentBlock(block: String, indent: String): String = {
+    block
+      .split("\n", -1)
+      .map { line =>
+        if (line.nonEmpty) indent + line else line
+      }
+      .mkString("\n")
+  }
+
+  private def lineEndIndex(text: String, from: Int): Int = {
+    val idx = text.indexOf('\n', from)
+    if (idx < 0) text.length else idx
+  }
+
+  private def detectClassBlockEnd(code: String, classHeaderStart: Int, 
classIndent: String): Int = {
+    val classLineEnd = lineEndIndex(code, classHeaderStart)
+    var pos = if (classLineEnd < code.length) classLineEnd + 1 else code.length
+
+    while (pos < code.length) {
+      val end = lineEndIndex(code, pos)
+      val line = code.substring(pos, end)
+
+      val trimmed = line.trim
+      val isBlank = trimmed.isEmpty
+
+      // a top-level (or same/lower-indented) non-blank line ends the class 
block
+      val currentIndentLen = line.prefixLength(ch => ch == ' ' || ch == '\t')
+      val classIndentLen = classIndent.length
+
+      if (!isBlank && currentIndentLen <= classIndentLen) {
+        return pos
+      }
+
+      pos = if (end < code.length) end + 1 else code.length
+    }
+
+    code.length
+  }
+
+  private def containsReservedHook(classBlock: String): Boolean = {
+    val hookRegex = ("""(?m)^[ \t]+def\s+""" + Regex.quote(ReservedHookMethod) 
+ """\s*\(""").r
+    hookRegex.findFirstIn(classBlock).isDefined
+  }
+
+  private def findInsertionPointInsideClass(classBlock: String, classIndent: 
String): Int = {
+    // Insert before the first method definition in the class body.
+    // This preserves existing open() and also preserves class docstrings if 
present.
+    val methodRegex = """(?m)^[ \t]+def\s+\w+\s*\(""".r
+    
methodRegex.findFirstMatchIn(classBlock).map(_.start).getOrElse(classBlock.length)
+  }

Review Comment:
   The `findInsertionPointInsideClass` method is defined on line 131 but is 
never called anywhere in the file. The `injectHookIntoUserClass` method instead 
always appends the injected hook at the end of the class block 
(`detectClassBlockEnd` is used) rather than before the first method. This dead 
method should be removed to avoid confusion.



##########
common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonUdfUiParameterInjectorSpec.scala:
##########
@@ -0,0 +1,181 @@
+/*
+ * 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.
+ */
+package org.apache.texera.amber.operator.udf.python
+
+import org.apache.texera.amber.core.tuple.{Attribute, AttributeType}
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+class PythonUdfUiParameterInjectorSpec extends AnyFlatSpec with Matchers {
+
+  private def createParameter(
+      key: String,
+      attributeType: AttributeType,
+      value: String
+  ): UiUDFParameter = {
+    val parameter = new UiUDFParameter
+    parameter.attribute = new Attribute(key, attributeType)
+    parameter.value = value
+    parameter
+  }
+
+  private val baseUdfCode: String =
+    """from pytexera import *
+      |
+      |class ProcessTupleOperator(UDFOperatorV2):
+      |    @overrides
+      |    def open(self):
+      |        print("open")
+      |
+      |    @overrides
+      |    def process_tuple(self, tuple_: Tuple, port: int):
+      |        yield tuple_
+      |""".stripMargin
+
+  it should "return encoded user code unchanged when there are no ui 
parameters" in {
+    val injectedCode = PythonUdfUiParameterInjector.inject(baseUdfCode, Nil)
+
+    injectedCode should include("class ProcessTupleOperator(UDFOperatorV2):")
+    injectedCode should include("""print("open")""")
+    injectedCode should not include ("_texera_injected_ui_parameters")
+    injectedCode should not include ("self.decode_python_template")
+  }
+
+  it should "inject ui parameter hook into supported UDF class" in {
+    val injectedCode = PythonUdfUiParameterInjector.inject(
+      baseUdfCode,
+      List(
+        createParameter("date", AttributeType.TIMESTAMP, 
"2024-01-01T00:00:00Z")
+      )
+    )
+
+    injectedCode should include("class ProcessTupleOperator(UDFOperatorV2):")
+    injectedCode should include("def _texera_injected_ui_parameters(self):")
+    injectedCode should include("return {")
+    injectedCode should include("self.decode_python_template")
+    injectedCode should include("""print("open")""")
+  }
+
+  it should "inject the reserved hook before the first method definition" in {
+    val injectedCode = PythonUdfUiParameterInjector.inject(
+      baseUdfCode,
+      List(createParameter("k", AttributeType.STRING, "v"))
+    )
+
+    val hookIndex = injectedCode.indexOf("def 
_texera_injected_ui_parameters(self):")
+    val openIndex = injectedCode.indexOf("def open(self):")
+
+    hookIndex should be >= 0
+    openIndex should be > hookIndex
+  }

Review Comment:
   The test has two issues:
   
   1. `hookIndex = injectedCode.indexOf("def 
_texera_injected_ui_parameters(self):")` will always return `-1` because the 
injected method signature includes a return type annotation: `def 
_texera_injected_ui_parameters(self) -> typing.Dict[str, typing.Any]:`. The 
substring `(self):` is not present. The assertion `hookIndex should be >= 0` 
will fail.
   
   2. Even if the string search were fixed, `injectHookIntoUserClass` appends 
the hook at `classBlockEnd` (end of class body), placing it *after* `def 
open(self):`. This means `hookIndex > openIndex`, so the assertion `openIndex 
should be > hookIndex` would also fail. The dead method 
`findInsertionPointInsideClass` (line 131-136) was apparently intended to solve 
this by inserting before the first method, but it is never called.



##########
amber/src/main/python/pytexera/udf/udf_operator.py:
##########
@@ -16,12 +16,76 @@
 # under the License.
 
 from abc import abstractmethod
-from typing import Iterator, Optional, Union
-
-from pyamber import *
+from typing import Any, Dict, Iterator, Optional, Union
 
+import functools
 
-class UDFOperatorV2(TupleOperatorV2):
+from pyamber import *
+from core.models.schema.attribute_type import AttributeType, 
FROM_STRING_PARSER_MAPPING
+
+class _UiParameterSupport:
+    _ui_parameter_injected_values: Dict[str, Any] = {}
+    _ui_parameter_name_types: Dict[str, AttributeType] = {}
+
+    # Reserved hook name. Backend injector will generate this in the user's 
class.
+    def _texera_injected_ui_parameters(self) -> Dict[str, Any]:
+        return {}
+
+    def _texera_apply_injected_ui_parameters(self) -> None:
+        values = self._texera_injected_ui_parameters()
+        # Write to base class storage (not cls) because UiParameter reads from 
_UiParameterSupport directly
+        _UiParameterSupport._ui_parameter_injected_values = dict(values or {})
+        _UiParameterSupport._ui_parameter_name_types = {}
+
+    def __init_subclass__(cls, **kwargs):
+        super().__init_subclass__(**kwargs)
+
+        # Wrap only methods defined on this class (not inherited ones)
+        original_open = getattr(cls, "open", None)
+        if original_open is None:
+            return
+
+        # Avoid double wrapping
+        if getattr(original_open, "__texera_ui_params_wrapped__", False):
+            return
+
+        @functools.wraps(original_open)
+        def wrapped_open(self, *args, **kwargs):
+            self._texera_apply_injected_ui_parameters()
+            return original_open(self, *args, **kwargs)
+
+        setattr(wrapped_open, "__texera_ui_params_wrapped__", True)
+        cls.open = wrapped_open
+
+    class UiParameter:
+        def __init__(self, name: str, type: AttributeType):
+            if not isinstance(type, AttributeType):
+                raise TypeError(
+                    f"UiParameter.type must be an AttributeType, got {type!r}."
+                )
+
+            existing_type = 
_UiParameterSupport._ui_parameter_name_types.get(name)
+            if existing_type is not None and existing_type != type:
+                raise ValueError(
+                    f"Duplicate UiParameter name '{name}' with conflicting 
types: "
+                    f"{existing_type.name} vs {type.name}."
+                )
+
+            _UiParameterSupport._ui_parameter_name_types[name] = type
+            raw_value = 
_UiParameterSupport._ui_parameter_injected_values.get(name)
+            self.name = name
+            self.type = type
+            self.value = _UiParameterSupport._parse(raw_value, type)

Review Comment:
   The `UiParameter.__init__` method uses `type` as a parameter name (line 61), 
which shadows the Python built-in `type()` function. While this works correctly 
inside the function, it is a Python anti-pattern that can lead to subtle bugs 
if developers modify this code and inadvertently try to use `type()`. A clearer 
name like `attr_type` would be more consistent with `_parse`'s parameter name 
`attr_type` used on line 81.



##########
common/workflow-core/src/main/scala/org/apache/texera/amber/core/tuple/Attribute.java:
##########
@@ -21,6 +21,10 @@
 
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.texera.amber.pybuilder.EncodableStringAnnotation;
+import org.apache.texera.amber.pybuilder.PyStringTypes;
+import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableStringFactory$;

Review Comment:
   Two unused imports were added: `import 
org.apache.texera.amber.pybuilder.PyStringTypes;` (line 25) and `import 
org.apache.texera.amber.pybuilder.PyStringTypes.EncodableStringFactory$;` (line 
26). Neither `PyStringTypes` nor `EncodableStringFactory$` are referenced 
anywhere in the file body — only `@EncodableStringAnnotation` from line 24's 
import is used. These unused imports should be removed.



##########
amber/src/main/python/pytexera/udf/udf_operator.py:
##########
@@ -16,12 +16,76 @@
 # under the License.
 
 from abc import abstractmethod
-from typing import Iterator, Optional, Union
-
-from pyamber import *
+from typing import Any, Dict, Iterator, Optional, Union
 
+import functools
 
-class UDFOperatorV2(TupleOperatorV2):
+from pyamber import *
+from core.models.schema.attribute_type import AttributeType, 
FROM_STRING_PARSER_MAPPING
+
+class _UiParameterSupport:
+    _ui_parameter_injected_values: Dict[str, Any] = {}
+    _ui_parameter_name_types: Dict[str, AttributeType] = {}
+
+    # Reserved hook name. Backend injector will generate this in the user's 
class.
+    def _texera_injected_ui_parameters(self) -> Dict[str, Any]:
+        return {}
+
+    def _texera_apply_injected_ui_parameters(self) -> None:
+        values = self._texera_injected_ui_parameters()
+        # Write to base class storage (not cls) because UiParameter reads from 
_UiParameterSupport directly
+        _UiParameterSupport._ui_parameter_injected_values = dict(values or {})
+        _UiParameterSupport._ui_parameter_name_types = {}
+
+    def __init_subclass__(cls, **kwargs):
+        super().__init_subclass__(**kwargs)
+
+        # Wrap only methods defined on this class (not inherited ones)
+        original_open = getattr(cls, "open", None)
+        if original_open is None:
+            return
+
+        # Avoid double wrapping
+        if getattr(original_open, "__texera_ui_params_wrapped__", False):
+            return
+
+        @functools.wraps(original_open)
+        def wrapped_open(self, *args, **kwargs):
+            self._texera_apply_injected_ui_parameters()
+            return original_open(self, *args, **kwargs)
+
+        setattr(wrapped_open, "__texera_ui_params_wrapped__", True)
+        cls.open = wrapped_open
+
+    class UiParameter:
+        def __init__(self, name: str, type: AttributeType):
+            if not isinstance(type, AttributeType):
+                raise TypeError(
+                    f"UiParameter.type must be an AttributeType, got {type!r}."
+                )
+
+            existing_type = 
_UiParameterSupport._ui_parameter_name_types.get(name)
+            if existing_type is not None and existing_type != type:
+                raise ValueError(
+                    f"Duplicate UiParameter name '{name}' with conflicting 
types: "
+                    f"{existing_type.name} vs {type.name}."
+                )
+
+            _UiParameterSupport._ui_parameter_name_types[name] = type
+            raw_value = 
_UiParameterSupport._ui_parameter_injected_values.get(name)
+            self.name = name
+            self.type = type
+            self.value = _UiParameterSupport._parse(raw_value, type)
+
+    @staticmethod
+    def _parse(value: Any, attr_type: AttributeType) -> Any:
+        if value is None:
+            return None
+
+        py_type = FROM_STRING_PARSER_MAPPING.get(attr_type)
+        return py_type(value)
+

Review Comment:
   The new `_UiParameterSupport` class and `UiParameter` inner class in 
`udf_operator.py` have no corresponding Python unit tests. The surrounding 
codebase has thorough test coverage (e.g., `test_echo_operator.py`, 
`test_count_batch_operator.py`). Tests should be added to cover at minimum: 
parameter value injection via `_texera_apply_injected_ui_parameters`, duplicate 
name detection with conflicting types in `UiParameter`, the `_parse` method for 
all supported attribute types, and the `__init_subclass__` wrapping behavior.



##########
frontend/src/app/workspace/component/code-editor-dialog/code-editor.component.ts:
##########
@@ -102,6 +103,7 @@ export class CodeEditorComponent implements AfterViewInit, 
SafeStyle, OnDestroy
   private isMultipleVariables: boolean = false;
   public codeDebuggerComponent!: Type<any> | null;
   public editorToPass!: MonacoEditor;
+  // private readonly pythonCodeChangeSubject = new Subject<string>();

Review Comment:
   The commented-out field `// private readonly pythonCodeChangeSubject = new 
Subject<string>();` on line 106 appears to be leftover dead code from a 
previous implementation approach. It should be removed to keep the file clean.



##########
frontend/src/app/workspace/service/code-editor/ui-udf-parameters-sync.service.ts:
##########
@@ -0,0 +1,131 @@
+/**
+ * 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 { Injectable } from "@angular/core";
+import { isEqual } from "lodash-es";
+import { ReplaySubject } from "rxjs";
+import { Subject } from "rxjs";

Review Comment:
   The `Subject` import from `"rxjs"` on line 22 is unused. Only 
`ReplaySubject` is actually used in this file. This unused import should be 
removed.



##########
amber/src/main/python/core/models/schema/attribute_type.py:
##########
@@ -78,6 +77,17 @@ class AttributeType(Enum):
 }
 
 
+FROM_STRING_PARSER_MAPPING = {
+    AttributeType.STRING: str,
+    AttributeType.INT: int,
+    AttributeType.LONG: int,
+    AttributeType.DOUBLE: float,
+    AttributeType.BOOL: lambda v: str(v).strip().lower() in ("True", "true", 
"1", "yes"),

Review Comment:
   The boolean parser in `FROM_STRING_PARSER_MAPPING` for `AttributeType.BOOL` 
includes `"True"` in the truthy set (line 85), but this is redundant since the 
lambda already calls `str(v).strip().lower()` — after `.lower()`, no string can 
equal `"True"` (capital T). The `"True"` entry in the tuple is dead code and 
the set effectively only checks for `"true"`, `"1"`, and `"yes"`.



##########
amber/src/main/python/pytexera/udf/udf_operator.py:
##########
@@ -16,12 +16,76 @@
 # under the License.
 
 from abc import abstractmethod
-from typing import Iterator, Optional, Union
-
-from pyamber import *
+from typing import Any, Dict, Iterator, Optional, Union
 
+import functools
 
-class UDFOperatorV2(TupleOperatorV2):
+from pyamber import *
+from core.models.schema.attribute_type import AttributeType, 
FROM_STRING_PARSER_MAPPING
+
+class _UiParameterSupport:
+    _ui_parameter_injected_values: Dict[str, Any] = {}
+    _ui_parameter_name_types: Dict[str, AttributeType] = {}
+
+    # Reserved hook name. Backend injector will generate this in the user's 
class.
+    def _texera_injected_ui_parameters(self) -> Dict[str, Any]:
+        return {}
+
+    def _texera_apply_injected_ui_parameters(self) -> None:
+        values = self._texera_injected_ui_parameters()
+        # Write to base class storage (not cls) because UiParameter reads from 
_UiParameterSupport directly
+        _UiParameterSupport._ui_parameter_injected_values = dict(values or {})
+        _UiParameterSupport._ui_parameter_name_types = {}
+
+    def __init_subclass__(cls, **kwargs):
+        super().__init_subclass__(**kwargs)
+
+        # Wrap only methods defined on this class (not inherited ones)
+        original_open = getattr(cls, "open", None)
+        if original_open is None:
+            return
+
+        # Avoid double wrapping
+        if getattr(original_open, "__texera_ui_params_wrapped__", False):
+            return
+
+        @functools.wraps(original_open)
+        def wrapped_open(self, *args, **kwargs):
+            self._texera_apply_injected_ui_parameters()
+            return original_open(self, *args, **kwargs)
+
+        setattr(wrapped_open, "__texera_ui_params_wrapped__", True)
+        cls.open = wrapped_open
+
+    class UiParameter:
+        def __init__(self, name: str, type: AttributeType):
+            if not isinstance(type, AttributeType):
+                raise TypeError(
+                    f"UiParameter.type must be an AttributeType, got {type!r}."
+                )
+
+            existing_type = 
_UiParameterSupport._ui_parameter_name_types.get(name)
+            if existing_type is not None and existing_type != type:
+                raise ValueError(
+                    f"Duplicate UiParameter name '{name}' with conflicting 
types: "
+                    f"{existing_type.name} vs {type.name}."
+                )
+
+            _UiParameterSupport._ui_parameter_name_types[name] = type
+            raw_value = 
_UiParameterSupport._ui_parameter_injected_values.get(name)
+            self.name = name
+            self.type = type
+            self.value = _UiParameterSupport._parse(raw_value, type)
+
+    @staticmethod
+    def _parse(value: Any, attr_type: AttributeType) -> Any:
+        if value is None:
+            return None
+
+        py_type = FROM_STRING_PARSER_MAPPING.get(attr_type)
+        return py_type(value)

Review Comment:
   The `_parse` method in `_UiParameterSupport` (lines 81-86) does not handle 
the case where `FROM_STRING_PARSER_MAPPING.get(attr_type)` returns `None` 
(i.e., when the `attr_type` is not in the mapping). If `py_type` is `None`, 
calling `py_type(value)` on line 86 will raise a `TypeError: 'NoneType' object 
is not callable`, giving an unhelpful error to the user.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to