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-8770-bb470cd77c2e191c17574a0722d15f315f29c11f
in repository https://gitbox.apache.org/repos/asf/texera.git

commit 23c0e5a1882de7c564cdbe093d5a89649a47e1b4
Author: Suryaa Charan <[email protected]>
AuthorDate: Thu Oct 15 18:22:40 2026 +0000

    feat(workflow-operator): make Python UDF templates work after uncommenting 
one class (#8770)
    
    ### What changes were proposed in this PR?
    
    The Python UDF, 2-in Python UDF and 1-out Python UDF templates were
    fully commented out, including `from pytexera import *`. They also never
    said to keep only one class. So uncommenting a class failed with
    `NameError`, and keeping several failed the one-Operator check.
    
    - `from pytexera import *` is now active.
    - A short header explains the rules:
    
    ```python
    # Keep exactly ONE class below: uncomment it and delete the others.
    # Its method must match its base class:
    #   UDFOperatorV2 -> process_tuple, UDFBatchOperator -> process_batch,
    #   UDFTableOperator -> process_table
    # Always `yield` results (never `return` a value). Put setup code in open();
    # if you override __init__, call super().__init__() first.
    ```
    
    - The 2-in template also says that port 0 (model) is read before port 1
    (tuples).
    - The UiParameter lines are kept.
    - The template copies in
    `docs/reference/operators/user-defined-functions/python/` are synced.
    
    Saved workflows keep their own code and are not affected.
    
    Before, every line started with `#`:
    
    ```python
    # Choose from the following templates:
    # ...
    # from pytexera import *
    #
    # class ProcessTupleOperator(UDFOperatorV2):
    # ...
    ```
    
    After, in the code editor:
    
    <img width="630" height="388" alt="template-after"
    
src="https://github.com/user-attachments/assets/d376eb72-a40e-4493-9018-21c2b089ca9e";
    />
    
    
    ### Any related issues, documentation, discussions?
    
    Part of #8736. Split out of #8737.
    
    ### How was this PR tested?
    
    - The specs for all three descriptors read the `defaultValue`. They
    check that the import is the only active line and the header is there,
    and that uncommenting any one class leaves exactly one active class.
    - New Python tests load the template shape and check that uncommenting
    each class loads without adding an import.
    - The template copy in the notebook-migration prompt is updated to
    match.
    
    ```
    sbt "WorkflowOperator/testOnly 
org.apache.texera.amber.operator.udf.python.*" "WorkflowOperator/scalafmtCheck" 
"WorkflowOperator/Test/scalafmtCheck"
    cd amber && pytest -m "not integration" 
src/test/python/core/architecture/managers/test_executor_manager.py
    cd frontend && yarn eslint 
src/app/workspace/service/notebook-migration/migration-prompts.ts
    ```
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Claude Opus 5.5)
    
    ---------
    
    Co-authored-by: suryaacharan <[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../architecture/managers/test_executor_manager.py | 64 ++++++++++++++++++++++
 .../python/DualInputPortsPythonUDFOpDescV2.scala   | 16 ++++--
 .../operator/udf/python/PythonUDFOpDescV2.scala    | 13 +++--
 .../python/source/PythonUDFSourceOpDescV2.scala    | 14 +++--
 .../DualInputPortsPythonUDFOpDescV2Spec.scala      | 28 ++++++++++
 .../udf/python/PythonUDFOpDescV2Spec.scala         | 27 +++++++++
 .../udf/python/PythonUdfTemplateTestUtils.scala    | 51 +++++++++++++++++
 .../source/PythonUDFSourceOpDescV2Spec.scala       | 24 ++++++++
 .../python/1-out-python-udf.md                     | 13 ++++-
 .../python/2-in-python-udf.md                      | 19 +++++--
 .../user-defined-functions/python/python-udf.md    | 16 ++++--
 .../notebook-migration/migration-prompts.ts        |  6 +-
 12 files changed, 263 insertions(+), 28 deletions(-)

diff --git 
a/amber/src/test/python/core/architecture/managers/test_executor_manager.py 
b/amber/src/test/python/core/architecture/managers/test_executor_manager.py
index 07ac054c06..d7b9d039cd 100644
--- a/amber/src/test/python/core/architecture/managers/test_executor_manager.py
+++ b/amber/src/test/python/core/architecture/managers/test_executor_manager.py
@@ -320,6 +320,70 @@ class TestExecutorManager:
         assert "tmp_dir" not in manager.__dict__
 
 
+# A reduced copy of the default Python UDF template (PythonUDFOpDescV2.
+# defaultValue): the import is active and every class is commented out. The
+# Scala specs check the real template text; this checks how it loads.
+DEFAULT_TEMPLATE_CODE = """# Keep exactly ONE class below: uncomment it and 
delete the others.
+
+from pytexera import *
+
+# class ProcessTupleOperator(UDFOperatorV2):
+#
+#     @overrides
+#     def process_tuple(self, tuple_: Tuple, port: int) -> 
Iterator[Optional[TupleLike]]:
+#         yield tuple_
+#
+# class ProcessBatchOperator(UDFBatchOperator):
+#     BATCH_SIZE = 10 # must be a positive integer
+#
+#     @overrides
+#     def process_batch(self, batch: Batch, port: int) -> 
Iterator[Optional[BatchLike]]:
+#         yield batch
+#
+# class ProcessTableOperator(UDFTableOperator):
+#
+#     @overrides
+#     def process_table(self, table: Table, port: int) -> 
Iterator[Optional[TableLike]]:
+#         yield table
+"""
+
+
+def uncomment_class(code: str, class_name: str) -> str:
+    """Uncomment one `# class <name>` block of a template, as a user would."""
+    lines, in_block = [], False
+    for line in code.splitlines():
+        if line.startswith("# class "):
+            in_block = line.startswith(f"# class {class_name}")
+        if in_block and line.startswith("#"):
+            line = line[1:].removeprefix(" ")
+        lines.append(line)
+    return "\n".join(lines) + "\n"
+
+
+class TestDefaultTemplate:
+    @pytest.fixture
+    def executor_manager(self):
+        manager = ExecutorManager()
+        yield manager
+        manager.close()
+
+    def test_untouched_template_reports_no_operator(self, executor_manager):
+        with pytest.raises(AssertionError) as exc_info:
+            executor_manager.load_executor_definition(DEFAULT_TEMPLATE_CODE)
+        assert "one and only one Operator" in str(exc_info.value)
+
+    @pytest.mark.parametrize(
+        "class_name",
+        ["ProcessTupleOperator", "ProcessBatchOperator", 
"ProcessTableOperator"],
+    )
+    def test_uncommenting_one_class_loads_without_adding_an_import(
+        self, executor_manager, class_name
+    ):
+        code = uncomment_class(DEFAULT_TEMPLATE_CODE, class_name)
+        executor = executor_manager.load_executor_definition(code)
+        assert executor.__name__ == class_name
+
+
 REPLACEMENT_OPERATOR_CODE = """
 from pytexera import *
 
diff --git 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2.scala
 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2.scala
index 14a35faabe..2bb49b00ce 100644
--- 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2.scala
+++ 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2.scala
@@ -33,15 +33,23 @@ class DualInputPortsPythonUDFOpDescV2 extends LogicalOp 
with PythonUdfUiParamete
   @JsonProperty(
     required = true,
     defaultValue =
-      "# Choose from the following templates:\n" +
+      "# Keep exactly ONE class below: uncomment it and delete the others.\n" +
+        "# Its method must match its base class:\n" +
+        "#   UDFOperatorV2 -> process_tuple, UDFBatchOperator -> 
process_batch,\n" +
+        "#   UDFTableOperator -> process_table\n" +
+        "# Always `yield` results (never `return` a value). Put setup code in 
open();\n" +
+        "# if you override __init__, call super().__init__() first.\n" +
+        "# \n" +
+        "# Port 0 (model) is read completely before port 1 (tuples) starts.\n" 
+
+        "# Use the `port` argument to tell them apart, e.g. `if port == 
0:`.\n" +
         "# \n" +
         "# Define UiParameter inside open() of ProcessTupleOperator, 
ProcessBatchOperator, or ProcessTableOperator.\n" +
         "# Example: self.count = self.UiParameter(\"count\", 
AttributeType.INT).value\n" +
         "# Add value=Resource.MODEL or Resource.DATASET to pick a version; the 
value is its mount directory.\n" +
         "# See the Python UDF operator documentation for supported types and 
behavior.\n" +
-        "# \n" +
-        "# from pytexera import *\n" +
-        "# \n" +
+        "\n" +
+        "from pytexera import *\n" +
+        "\n" +
         "# class ProcessTupleOperator(UDFOperatorV2):\n" +
         "#     \n" +
         "#     @overrides\n" +
diff --git 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2.scala
 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2.scala
index 0239ca9145..6094f28eae 100644
--- 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2.scala
+++ 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2.scala
@@ -35,15 +35,20 @@ class PythonUDFOpDescV2 extends LogicalOp with 
PythonUdfUiParameterSupport {
   @JsonProperty(
     required = true,
     defaultValue =
-      "# Choose from the following templates:\n" +
+      "# Keep exactly ONE class below: uncomment it and delete the others.\n" +
+        "# Its method must match its base class:\n" +
+        "#   UDFOperatorV2 -> process_tuple, UDFBatchOperator -> 
process_batch,\n" +
+        "#   UDFTableOperator -> process_table\n" +
+        "# Always `yield` results (never `return` a value). Put setup code in 
open();\n" +
+        "# if you override __init__, call super().__init__() first.\n" +
         "# \n" +
         "# Define UiParameter inside open() of ProcessTupleOperator, 
ProcessBatchOperator, or ProcessTableOperator.\n" +
         "# Example: self.count = self.UiParameter(\"count\", 
AttributeType.INT).value\n" +
         "# Add value=Resource.MODEL or Resource.DATASET to pick a version; the 
value is its mount directory.\n" +
         "# See the Python UDF operator documentation for supported types and 
behavior.\n" +
-        "# \n" +
-        "# from pytexera import *\n" +
-        "# \n" +
+        "\n" +
+        "from pytexera import *\n" +
+        "\n" +
         "# class ProcessTupleOperator(UDFOperatorV2):\n" +
         "#     \n" +
         "#     @overrides\n" +
diff --git 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2.scala
 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2.scala
index 5932c6643c..623b80f2d6 100644
--- 
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2.scala
+++ 
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2.scala
@@ -33,16 +33,22 @@ class PythonUDFSourceOpDescV2 extends 
SourceOperatorDescriptor with PythonUdfUiP
 
   @JsonProperty(
     required = true,
-    defaultValue = "# Define UiParameter inside GenerateOperator.open().\n" +
+    defaultValue = "# Uncomment the class below. Its method must match its 
base class:\n" +
+      "#   UDFSourceOperator -> produce\n" +
+      "# Always `yield` results (never `return` a value). Put setup code in 
open();\n" +
+      "# if you override __init__, call super().__init__() first.\n" +
+      "# Keep exactly ONE class in this script.\n" +
+      "# \n" +
+      "# Define UiParameter inside GenerateOperator.open().\n" +
       "# Example: self.count = self.UiParameter(\"count\", 
AttributeType.INT).value\n" +
       "# Add value=Resource.MODEL or Resource.DATASET to pick a version; the 
value is its mount directory.\n" +
       "# See the Python UDF operator documentation for supported types and 
behavior.\n" +
-      "# \n" +
-      "# from pytexera import *\n" +
+      "\n" +
+      "from pytexera import *\n" +
+      "\n" +
       "# class GenerateOperator(UDFSourceOperator):\n" +
       "# \n" +
       "#     @overrides\n" +
-      "#     \n" +
       "#     def produce(self) -> Iterator[Union[TupleLike, TableLike, 
None]]:\n" +
       "#         yield\n"
   )
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2Spec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2Spec.scala
index 3b4f6c3207..7945099d00 100644
--- 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2Spec.scala
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/DualInputPortsPythonUDFOpDescV2Spec.scala
@@ -209,4 +209,32 @@ class DualInputPortsPythonUDFOpDescV2Spec extends 
AnyFlatSpec with Matchers {
     r.uiParameters.map(_.attribute) shouldBe List(new Attribute("count", 
AttributeType.INTEGER))
     r.uiParameters.map(_.value) shouldBe List("7")
   }
+
+  private val defaultCode: String =
+    
PythonUdfTemplateTestUtils.defaultCode(classOf[DualInputPortsPythonUDFOpDescV2])
+
+  "DualInputPortsPythonUDFOpDescV2 default code" should "keep the pytexera 
import active and explain the rules" in {
+    PythonUdfTemplateTestUtils.activeLines(defaultCode) shouldBe List("from 
pytexera import *")
+    defaultCode should include("Keep exactly ONE class")
+    defaultCode should include("never `return` a value")
+    defaultCode should include("super().__init__()")
+    defaultCode should include("UDFOperatorV2 -> process_tuple")
+    defaultCode should include("UDFBatchOperator -> process_batch")
+    defaultCode should include("UDFTableOperator -> process_table")
+    defaultCode should include("Port 0 (model) is read completely before port 
1 (tuples)")
+    defaultCode should include("UiParameter")
+  }
+
+  it should "leave exactly one active class after uncommenting any one 
template class" in {
+    for (
+      className <- List("ProcessTupleOperator", "ProcessBatchOperator", 
"ProcessTableOperator")
+    ) {
+      val active = PythonUdfTemplateTestUtils.activeLines(
+        PythonUdfTemplateTestUtils.uncommentClass(defaultCode, className)
+      )
+      active.head shouldBe "from pytexera import *"
+      active.count(_.startsWith("class ")) shouldBe 1
+      active.exists(_.startsWith(s"class $className(")) shouldBe true
+    }
+  }
 }
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2Spec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2Spec.scala
index bc0f22ff1c..bf2d9da430 100644
--- 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2Spec.scala
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonUDFOpDescV2Spec.scala
@@ -304,4 +304,31 @@ class PythonUDFOpDescV2Spec extends AnyFlatSpec with 
Matchers {
       case other                   => fail(s"expected OpExecWithCode, got 
$other")
     }
   }
+
+  private val defaultCode: String =
+    PythonUdfTemplateTestUtils.defaultCode(classOf[PythonUDFOpDescV2])
+
+  "PythonUDFOpDescV2 default code" should "keep the pytexera import active and 
explain the rules" in {
+    PythonUdfTemplateTestUtils.activeLines(defaultCode) shouldBe List("from 
pytexera import *")
+    defaultCode should include("Keep exactly ONE class")
+    defaultCode should include("never `return` a value")
+    defaultCode should include("super().__init__()")
+    defaultCode should include("UDFOperatorV2 -> process_tuple")
+    defaultCode should include("UDFBatchOperator -> process_batch")
+    defaultCode should include("UDFTableOperator -> process_table")
+    defaultCode should include("UiParameter")
+  }
+
+  it should "leave exactly one active class after uncommenting any one 
template class" in {
+    for (
+      className <- List("ProcessTupleOperator", "ProcessBatchOperator", 
"ProcessTableOperator")
+    ) {
+      val active = PythonUdfTemplateTestUtils.activeLines(
+        PythonUdfTemplateTestUtils.uncommentClass(defaultCode, className)
+      )
+      active.head shouldBe "from pytexera import *"
+      active.count(_.startsWith("class ")) shouldBe 1
+      active.exists(_.startsWith(s"class $className(")) shouldBe true
+    }
+  }
 }
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonUdfTemplateTestUtils.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonUdfTemplateTestUtils.scala
new file mode 100644
index 0000000000..9b92924f18
--- /dev/null
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/PythonUdfTemplateTestUtils.scala
@@ -0,0 +1,51 @@
+/*
+ * 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 com.fasterxml.jackson.annotation.JsonProperty
+
+/**
+  * Shared helpers for the Python UDF template specs: read a descriptor's 
default
+  * code template and simulate a user uncommenting one of its classes.
+  */
+object PythonUdfTemplateTestUtils {
+
+  /** The `defaultValue` of the descriptor's `code` property, i.e. the 
template. */
+  def defaultCode(descriptorClass: Class[_]): String =
+    descriptorClass
+      .getDeclaredField("code")
+      .getAnnotation(classOf[JsonProperty])
+      .defaultValue()
+
+  /** Lines that are neither blank nor comments. */
+  def activeLines(code: String): List[String] =
+    code.linesIterator.map(_.trim).filterNot(l => l.isEmpty || 
l.startsWith("#")).toList
+
+  /** Uncomment the template lines from `# class <name>` up to the next 
commented class. */
+  def uncommentClass(code: String, className: String): String = {
+    var inBlock = false
+    code.linesIterator
+      .map { line =>
+        if (line.startsWith("# class ")) inBlock = line.startsWith(s"# class 
$className")
+        if (inBlock && line.startsWith("#")) 
line.stripPrefix("#").stripPrefix(" ") else line
+      }
+      .mkString("\n")
+  }
+}
diff --git 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2Spec.scala
 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2Spec.scala
index dcb648e282..b4d32dc0a5 100644
--- 
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2Spec.scala
+++ 
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/udf/python/source/PythonUDFSourceOpDescV2Spec.scala
@@ -26,6 +26,7 @@ import org.apache.texera.amber.operator.LogicalOp
 import org.apache.texera.amber.operator.metadata.OperatorGroupConstants
 import org.apache.texera.amber.operator.udf.python.UiUDFParameter
 import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.apache.texera.amber.operator.udf.python.PythonUdfTemplateTestUtils
 import org.scalatest.flatspec.AnyFlatSpec
 import org.scalatest.matchers.should.Matchers
 
@@ -159,4 +160,27 @@ class PythonUDFSourceOpDescV2Spec extends AnyFlatSpec with 
Matchers {
     p.uiParameters.map(_.attribute) shouldBe List(new Attribute("count", 
AttributeType.INTEGER))
     p.uiParameters.map(_.value) shouldBe List("7")
   }
+
+  private val defaultCode: String =
+    PythonUdfTemplateTestUtils.defaultCode(classOf[PythonUDFSourceOpDescV2])
+
+  "PythonUDFSourceOpDescV2 default code" should "keep the pytexera import 
active and explain the rules" in {
+    PythonUdfTemplateTestUtils.activeLines(defaultCode) shouldBe List("from 
pytexera import *")
+    defaultCode should include("Keep exactly ONE class")
+    defaultCode should include("never `return` a value")
+    defaultCode should include("super().__init__()")
+    defaultCode should include("UDFSourceOperator -> produce")
+    defaultCode should include("UiParameter")
+  }
+
+  it should "leave exactly one active class after uncommenting any one 
template class" in {
+    for (className <- List("GenerateOperator")) {
+      val active = PythonUdfTemplateTestUtils.activeLines(
+        PythonUdfTemplateTestUtils.uncommentClass(defaultCode, className)
+      )
+      active.head shouldBe "from pytexera import *"
+      active.count(_.startsWith("class ")) shouldBe 1
+      active.exists(_.startsWith(s"class $className(")) shouldBe true
+    }
+  }
 }
diff --git 
a/docs/reference/operators/user-defined-functions/python/1-out-python-udf.md 
b/docs/reference/operators/user-defined-functions/python/1-out-python-udf.md
index 69f68d25d8..bcfab91e6b 100644
--- a/docs/reference/operators/user-defined-functions/python/1-out-python-udf.md
+++ b/docs/reference/operators/user-defined-functions/python/1-out-python-udf.md
@@ -43,15 +43,22 @@ tags: [user-defined-functions, python]
 **Python script**
 
 ```python
+# Uncomment the class below. Its method must match its base class:
+#   UDFSourceOperator -> produce
+# Always `yield` results (never `return` a value). Put setup code in open();
+# if you override __init__, call super().__init__() first.
+# Keep exactly ONE class in this script.
+# 
 # Define UiParameter inside GenerateOperator.open().
 # Example: self.count = self.UiParameter("count", AttributeType.INT).value
+# Add value=Resource.MODEL or Resource.DATASET to pick a version; the value is 
its mount directory.
 # See the Python UDF operator documentation for supported types and behavior.
-#
-# from pytexera import *
+
+from pytexera import *
+
 # class GenerateOperator(UDFSourceOperator):
 # 
 #     @overrides
-#     
 #     def produce(self) -> Iterator[Union[TupleLike, TableLike, None]]:
 #         yield
 
diff --git 
a/docs/reference/operators/user-defined-functions/python/2-in-python-udf.md 
b/docs/reference/operators/user-defined-functions/python/2-in-python-udf.md
index 5c0c398a62..a1d9143c55 100644
--- a/docs/reference/operators/user-defined-functions/python/2-in-python-udf.md
+++ b/docs/reference/operators/user-defined-functions/python/2-in-python-udf.md
@@ -44,14 +44,23 @@ tags: [user-defined-functions, python]
 **Python script**
 
 ```python
-# Choose from the following templates:
-#
+# Keep exactly ONE class below: uncomment it and delete the others.
+# Its method must match its base class:
+#   UDFOperatorV2 -> process_tuple, UDFBatchOperator -> process_batch,
+#   UDFTableOperator -> process_table
+# Always `yield` results (never `return` a value). Put setup code in open();
+# if you override __init__, call super().__init__() first.
+# 
+# Port 0 (model) is read completely before port 1 (tuples) starts.
+# Use the `port` argument to tell them apart, e.g. `if port == 0:`.
+# 
 # Define UiParameter inside open() of ProcessTupleOperator, 
ProcessBatchOperator, or ProcessTableOperator.
 # Example: self.count = self.UiParameter("count", AttributeType.INT).value
+# Add value=Resource.MODEL or Resource.DATASET to pick a version; the value is 
its mount directory.
 # See the Python UDF operator documentation for supported types and behavior.
-# 
-# from pytexera import *
-# 
+
+from pytexera import *
+
 # class ProcessTupleOperator(UDFOperatorV2):
 #     
 #     @overrides
diff --git 
a/docs/reference/operators/user-defined-functions/python/python-udf.md 
b/docs/reference/operators/user-defined-functions/python/python-udf.md
index 3f1a1e7f85..1680c9f3b7 100644
--- a/docs/reference/operators/user-defined-functions/python/python-udf.md
+++ b/docs/reference/operators/user-defined-functions/python/python-udf.md
@@ -44,14 +44,20 @@ tags: [user-defined-functions, python]
 **Python script**
 
 ```python
-# Choose from the following templates:
-#
+# Keep exactly ONE class below: uncomment it and delete the others.
+# Its method must match its base class:
+#   UDFOperatorV2 -> process_tuple, UDFBatchOperator -> process_batch,
+#   UDFTableOperator -> process_table
+# Always `yield` results (never `return` a value). Put setup code in open();
+# if you override __init__, call super().__init__() first.
+# 
 # Define UiParameter inside open() of ProcessTupleOperator, 
ProcessBatchOperator, or ProcessTableOperator.
 # Example: self.count = self.UiParameter("count", AttributeType.INT).value
+# Add value=Resource.MODEL or Resource.DATASET to pick a version; the value is 
its mount directory.
 # See the Python UDF operator documentation for supported types and behavior.
-# 
-# from pytexera import *
-# 
+
+from pytexera import *
+
 # class ProcessTupleOperator(UDFOperatorV2):
 #     
 #     @overrides
diff --git 
a/frontend/src/app/workspace/service/notebook-migration/migration-prompts.ts 
b/frontend/src/app/workspace/service/notebook-migration/migration-prompts.ts
index 6a8ad9872b..d5d304aef9 100644
--- a/frontend/src/app/workspace/service/notebook-migration/migration-prompts.ts
+++ b/frontend/src/app/workspace/service/notebook-migration/migration-prompts.ts
@@ -68,9 +68,9 @@ Batch API consumes a batch of tuples at a time. Similar to 
Table, a Batch is als
   All three APIs can return an empty iterator by yield None.
 
   The template code for a Python UDF follows: MAKE SURE TO USE THE CLASS NAMES 
AND FUNCTIONS DEFINED, THIS IS A MUST FOR THE PROGRAM TO WORK. SELECT 1 OUT OF 
THE 3 PROCESSING OPERATOR FUNCTIONS TO BUILD DEPENDING ON THE CONTEXT OF CODE 
TRANSLATION.
-# Choose from the following templates:
-  #
-# from pytexera import *
+# Keep exactly ONE class below: uncomment it and delete the others.
+#
+from pytexera import *
 #
 # class ProcessTupleOperator(UDFOperatorV2):
 #

Reply via email to