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

Yicong-Huang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new 2a23ba59d8 chore(pybuilder): aggregate PyBuilder at root and add API 
spec for non-macro pieces (#5024)
2a23ba59d8 is described below

commit 2a23ba59d8ebe584127aadb2cc41725e8671e276
Author: Yicong Huang <[email protected]>
AuthorDate: Sun May 10 23:40:36 2026 -0700

    chore(pybuilder): aggregate PyBuilder at root and add API spec for 
non-macro pieces (#5024)
    
    ### What changes were proposed in this PR?
    
    Three closely related changes scoped to `common/pybuilder`:
    
    1. **Add `PyBuilder` to the root `TexeraProject` `aggregate(...)`**,
    grouped under "common libraries" alongside `Auth`, `Config`, `DAO`,
    `WorkflowCore`, and `WorkflowOperator` (alphabetized within each group).
    Before this PR, `PyBuilder` was the only sbt project defined but not
    aggregated, so `sbt test` and `sbt scalafmtCheckAll` from the repo root
    silently skipped it. CI still ran `PyBuilder/jacoco` explicitly, so
    coverage reporting was unaffected, but the local/CI matrix mismatch was
    confusing.
    
    2. **Apply scalafmt to four pre-existing PyBuilder files**
    (`BoundaryValidator.scala`, `EncodableInspector.scala`,
    `PythonTemplateBuilder.scala`, `PythonTemplateBuilderSpec.scala`).
    `scalafmtCheckAll` only iterates over aggregated sub-projects, so change
    1 brought PyBuilder under the format gate for the first time and
    surfaced debt that had been accumulating invisibly since the project was
    introduced. Reformatting these files is the necessary follow-on for
    change 1 to leave CI green; the diffs are purely scaladoc-star
    indentation and multi-line case-class parameter alignment, no semantic
    changes.
    
    3. **Add `PythonTemplateBuilderApiSpec`** covering non-macro pieces of
    `PythonTemplateBuilder` that `PythonTemplateBuilderSpec` only touches
    incidentally — factory constructors, render-mode singletons, renderer
    behavior in both modes, `fromInterpolated` precondition, `+(String)`
    `UnsupportedOperationException`, and `render()` CR/CRLF normalization.
    Also pins current `PythonLexerUtils` behavior on Python triple-quoted
    strings (the lexer is intentionally conservative and not
    triple-quote-aware; pinned so a future triple-quote-aware change trips
    the spec deliberately).
    
    ### Any related issues, documentation, discussions?
    
    Resolves #5023
    
    ### How was this PR tested?
    
    `sbt PyBuilder/test` — 125 tests pass (32 new). `sbt PyBuilder/jacoco`
    line coverage went from 30.12% to 31.73% and method coverage from 13.17%
    to 15.30% (6 newly covered methods); the rest of
    `PythonTemplateBuilder.scala` is the `pybImpl` macro body which jacoco
    cannot instrument at runtime. `sbt scalafmtCheckAll` now passes cleanly
    with PyBuilder included.
    
    ### Was this PR authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 4.7 (1M context)
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
 build.sbt                                          |  13 +-
 .../texera/amber/pybuilder/BoundaryValidator.scala |  52 ++--
 .../amber/pybuilder/EncodableInspector.scala       |  56 ++--
 .../amber/pybuilder/PythonTemplateBuilder.scala    | 336 +++++++++++----------
 .../pybuilder/PythonTemplateBuilderApiSpec.scala   | 247 +++++++++++++++
 .../pybuilder/PythonTemplateBuilderSpec.scala      |  50 +--
 6 files changed, 512 insertions(+), 242 deletions(-)

diff --git a/build.sbt b/build.sbt
index d80a858c93..b7b6b3cfb2 100644
--- a/build.sbt
+++ b/build.sbt
@@ -159,15 +159,18 @@ lazy val WorkflowExecutionService = (project in 
file("amber"))
 // root project definition
 lazy val TexeraProject = (project in file("."))
   .aggregate(
-    DAO,
-    Config,
-    ConfigService,
-    AccessControlService,
+    // common libraries
     Auth,
+    Config,
+    DAO,
+    PyBuilder,
     WorkflowCore,
+    WorkflowOperator,
+    // services
+    AccessControlService,
     ComputingUnitManagingService,
+    ConfigService,
     FileService,
-    WorkflowOperator,
     WorkflowCompilingService,
     WorkflowExecutionService
   )
diff --git 
a/common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/BoundaryValidator.scala
 
b/common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/BoundaryValidator.scala
index 8475661d73..59713f0d6d 100644
--- 
a/common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/BoundaryValidator.scala
+++ 
b/common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/BoundaryValidator.scala
@@ -22,21 +22,21 @@ package org.apache.texera.amber.pybuilder
 import scala.reflect.macros.blackbox
 
 /**
- * Macro-only helper: validates boundaries for Encodable insertions.
- *
- * Compile-time: abort with good messages for direct Encodable args.
- * Runtime: for nested builders (unknown content at compile time), generate a 
check that throws if the builder contains Encodable chunks.
- */
+  * Macro-only helper: validates boundaries for Encodable insertions.
+  *
+  * Compile-time: abort with good messages for direct Encodable args.
+  * Runtime: for nested builders (unknown content at compile time), generate a 
check that throws if the builder contains Encodable chunks.
+  */
 final class BoundaryValidator[C <: blackbox.Context](val c: C) {
   import PythonLexerUtils._
   import c.universe._
 
   /**
-   * Centralized, templatized error messages (Option A).
-   *
-   * NOTE: This object lives inside the class so it can freely use string 
templates
-   * without any macro-context type gymnastics.
-   */
+    * Centralized, templatized error messages (Option A).
+    *
+    * NOTE: This object lives inside the class so it can freely use string 
templates
+    * without any macro-context type gymnastics.
+    */
   private object BoundaryErrors {
 
     // Provide a hint that can differ between compile-time and runtime wording.
@@ -76,19 +76,19 @@ final class BoundaryValidator[C <: blackbox.Context](val c: 
C) {
   }
 
   final case class CompileTimeContext(
-                                       leftPart: String,
-                                       rightPart: String,
-                                       prefixSource: String,
-                                       argIndex: Int,
-                                       errorPos: Position
-                                     )
+      leftPart: String,
+      rightPart: String,
+      prefixSource: String,
+      argIndex: Int,
+      errorPos: Position
+  )
 
   final case class RuntimeContext(
-                                   leftPart: String,
-                                   rightPart: String,
-                                   prefixSource: String,
-                                   argIndex: Int
-                                 )
+      leftPart: String,
+      rightPart: String,
+      prefixSource: String,
+      argIndex: Int
+  )
 
   def validateCompileTime(ctx: CompileTimeContext): Unit = {
     val prefixLine = lineTail(ctx.prefixSource)
@@ -130,11 +130,11 @@ final class BoundaryValidator[C <: blackbox.Context](val 
c: C) {
   }
 
   /**
-   * Generate runtime checks for nested PythonTemplateBuilder args.
-   *
-   * This is only emitted when the boundary context is unsafe. The runtime 
guard is:
-   *   if (arg.containsEncodableString) throw ...
-   */
+    * Generate runtime checks for nested PythonTemplateBuilder args.
+    *
+    * This is only emitted when the boundary context is unsafe. The runtime 
guard is:
+    *   if (arg.containsEncodableString) throw ...
+    */
   def runtimeChecksForNestedBuilder(ctx: RuntimeContext, argIdent: Tree): 
List[Tree] = {
     val prefixLine = lineTail(ctx.prefixSource)
     val argNum = ctx.argIndex + 1
diff --git 
a/common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/EncodableInspector.scala
 
b/common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/EncodableInspector.scala
index 781c1c9a0a..f8622f81fd 100644
--- 
a/common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/EncodableInspector.scala
+++ 
b/common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/EncodableInspector.scala
@@ -22,10 +22,10 @@ package org.apache.texera.amber.pybuilder
 import scala.reflect.macros.blackbox
 
 /**
- * Macro-only helper: inspects argument trees / types / symbols to decide if a 
value is Encodable-marked.
- *
- * NOTE: This must be context-bound because Tree/Type/Annotation are from 
`c.universe`.
- */
+  * Macro-only helper: inspects argument trees / types / symbols to decide if 
a value is Encodable-marked.
+  *
+  * NOTE: This must be context-bound because Tree/Type/Annotation are from 
`c.universe`.
+  */
 final class EncodableInspector[C <: blackbox.Context](val c: C) {
 
   import c.universe._
@@ -49,10 +49,10 @@ final class EncodableInspector[C <: blackbox.Context](val 
c: C) {
     "org.apache.texera.amber.pybuilder.EncodableStringAnnotation"
 
   /**
-   * If we are pointing at a getter/accessor, hop to its accessed field symbol 
when possible.
-   *
-   * Why: Many annotations are placed on constructor params/fields, but call 
sites see the accessor.
-   */
+    * If we are pointing at a getter/accessor, hop to its accessed field 
symbol when possible.
+    *
+    * Why: Many annotations are placed on constructor params/fields, but call 
sites see the accessor.
+    */
   private def safeAccessed(sym: Symbol): Symbol =
     sym match {
       case termAccessor: TermSymbol if termAccessor.isAccessor       => 
termAccessor.accessed
@@ -65,15 +65,15 @@ final class EncodableInspector[C <: blackbox.Context](val 
c: C) {
     val annotationType = annotation.tree.tpe
     annotationType != null && (
       annotationType.typeSymbol.fullName == encodableStringAnnotationFqn ||
-        (annotationType <:< typeOf[EncodableStringAnnotation])
-      )
+      (annotationType <:< typeOf[EncodableStringAnnotation])
+    )
   }
 
   /**
-   * True if a [[Type]] carries @EncodableStringAnnotation as a TYPE_USE 
annotation (via [[java.lang.reflect.AnnotatedType]]).
-   *
-   * Walks common wrappers (existentials, refinements, type refs) to find 
nested annotations.
-   */
+    * True if a [[Type]] carries @EncodableStringAnnotation as a TYPE_USE 
annotation (via [[java.lang.reflect.AnnotatedType]]).
+    *
+    * Walks common wrappers (existentials, refinements, type refs) to find 
nested annotations.
+    */
   private def typeHasEncodableString(typeToCheck: Type): Boolean = {
     def loop(t: Type): Boolean = {
       if (t == null) false
@@ -101,17 +101,17 @@ final class EncodableInspector[C <: blackbox.Context](val 
c: C) {
   }
 
   /**
-   * Checks @EncodableStringAnnotation on either:
-   *   - accessed symbol (field/param), or
-   *   - type (TYPE_USE), via [[java.lang.reflect.AnnotatedType]].
-   */
+    * Checks @EncodableStringAnnotation on either:
+    *   - accessed symbol (field/param), or
+    *   - type (TYPE_USE), via [[java.lang.reflect.AnnotatedType]].
+    */
   def treeHasEncodableString(tree: Tree): Boolean = {
     val rawSym = tree.symbol
     val symHasAnn =
       rawSym != null && rawSym != NoSymbol && {
         val accessed = safeAccessed(rawSym)
         accessed != null && accessed != NoSymbol &&
-          accessed.annotations.exists(annIsEncodableString)
+        accessed.annotations.exists(annIsEncodableString)
       }
 
     val methodReturnHasAnn =
@@ -123,7 +123,7 @@ final class EncodableInspector[C <: blackbox.Context](val 
c: C) {
       })
 
     symHasAnn || methodReturnHasAnn ||
-      (tree.tpe != null && typeHasEncodableString(tree.tpe))
+    (tree.tpe != null && typeHasEncodableString(tree.tpe))
   }
 
   def isPythonTemplateBuilderArg(argExpr: c.Expr[Any]): Boolean = {
@@ -145,18 +145,18 @@ final class EncodableInspector[C <: blackbox.Context](val 
c: C) {
       //  - treat already-wrapped EncodableStringRenderer as encodable
       //  - OR detect @EncodableStringAnnotation on symbol/type
       (tpe != null && (tpe.dealias.widen <:< encodableStringRendererTpe)) ||
-        treeHasEncodableString(argExpr.tree)
+      treeHasEncodableString(argExpr.tree)
     }
   }
 
   /**
-   * Wrap an argument expression as a [[PythonTemplateBuilder.StringRenderer]] 
AST node.
-   *
-   * Priority:
-   * 1) If it's already a StringRenderer, keep it (cast).
-   * 2) Else if Encodable-marked, wrap as EncodableStringRenderer.
-   * 3) Else wrap as PyLiteralStringRenderer.
-   */
+    * Wrap an argument expression as a 
[[PythonTemplateBuilder.StringRenderer]] AST node.
+    *
+    * Priority:
+    * 1) If it's already a StringRenderer, keep it (cast).
+    * 2) Else if Encodable-marked, wrap as EncodableStringRenderer.
+    * 3) Else wrap as PyLiteralStringRenderer.
+    */
   def wrapArg(argExpr: c.Expr[Any]): Tree = {
     val argTree = argExpr.tree
     val argType = argTree.tpe
diff --git 
a/common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilder.scala
 
b/common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilder.scala
index dc9e977d32..a79f162de1 100644
--- 
a/common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilder.scala
+++ 
b/common/pybuilder/src/main/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilder.scala
@@ -27,43 +27,43 @@ import scala.language.experimental.macros
 import scala.reflect.macros.blackbox
 
 /**
- * Convenience type aliases for strings passed into the 
[[PythonTemplateBuilder]] interpolator.
- *
- * Design intent:
- *   - Some strings are “UI-provided” and must be rendered as a Python 
expression that decodes base64 at runtime.
- *   - Other strings are regular Python source fragments and should be spliced 
in as-is.
- *
- * The macro distinguishes Encodable strings via a TYPE_USE annotation 
(`String @EncodableStringAnnotation`).
- */
+  * Convenience type aliases for strings passed into the 
[[PythonTemplateBuilder]] interpolator.
+  *
+  * Design intent:
+  *   - Some strings are “UI-provided” and must be rendered as a Python 
expression that decodes base64 at runtime.
+  *   - Other strings are regular Python source fragments and should be 
spliced in as-is.
+  *
+  * The macro distinguishes Encodable strings via a TYPE_USE annotation 
(`String @EncodableStringAnnotation`).
+  */
 object PyStringTypes {
 
   /**
-   * Treated as an Encodable string by the macro via a TYPE_USE annotation.
-   *
-   * Example:
-   * {{{
-   * import org.apache.texera.amber.pybuilder.PyStringTypes.EncodableStringType
-   * import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._
-   *
-   * val label: EncodableStringType = "Hello"
-   * val code = pyb"print($label)"
-   * }}}
-   */
+    * Treated as an Encodable string by the macro via a TYPE_USE annotation.
+    *
+    * Example:
+    * {{{
+    * import 
org.apache.texera.amber.pybuilder.PyStringTypes.EncodableStringType
+    * import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._
+    *
+    * val label: EncodableStringType = "Hello"
+    * val code = pyb"print($label)"
+    * }}}
+    */
   type EncodableString = String @EncodableStringAnnotation
 
   /**
-   * Normal python string (macro defaults to [[PythonLiteral]] when no 
[[EncodableStringAnnotation]] is present).
-   *
-   * This alias exists mostly for readability and symmetry with 
[[EncodableStringFactory]].
-   */
+    * Normal python string (macro defaults to [[PythonLiteral]] when no 
[[EncodableStringAnnotation]] is present).
+    *
+    * This alias exists mostly for readability and symmetry with 
[[EncodableStringFactory]].
+    */
   type PythonLiteral = String
 
   /**
-   * Helper “constructor” and constants for [[EncodableString]].
-   *
-   * Note: the object and members are annotated so downstream type inference 
tends
-   * to keep the TYPE_USE annotation attached in common scenarios.
-   */
+    * Helper “constructor” and constants for [[EncodableString]].
+    *
+    * Note: the object and members are annotated so downstream type inference 
tends
+    * to keep the TYPE_USE annotation attached in common scenarios.
+    */
   @EncodableStringAnnotation
   object EncodableStringFactory {
 
@@ -77,10 +77,10 @@ object PyStringTypes {
   }
 
   /**
-   * Helper “constructor” and constants for [[PythonLiteral]].
-   *
-   * This does not apply any Encodable semantics. It is regular Scala `String` 
usage.
-   */
+    * Helper “constructor” and constants for [[PythonLiteral]].
+    *
+    * This does not apply any Encodable semantics. It is regular Scala 
`String` usage.
+    */
   object PyLiteralFactory {
 
     /** Identity wrapper, used as a readability hint at call sites. */
@@ -92,117 +92,117 @@ object PyStringTypes {
 }
 
 /**
- * =PythonTemplateBuilder: ergonomic Python codegen via `pyb"..."`=
- *
- * This module provides a tiny DSL for assembling Python source code from 
Scala while preserving two competing goals:
- * (1) developers want to write templates that look like normal Python, and 
(2) user-provided text must not be injected
- * into the emitted Python as raw literals that can break syntax or create 
ambiguous token boundaries.
- *
- * The core idea is that every value spliced into a `pyb"..."` template is 
first classified into one of two buckets:
- *
- *  - '''Python literals''' (ordinary Scala strings or already-safe fragments) 
are inserted as-is.
- *  - '''Encodable strings''' (typically UI-provided text) are base64-encoded 
at build time and rendered as a *Python
- *    expression* that decodes at runtime, rather than being embedded as a 
Python string literal.
- *
- * This classification is driven by a TYPE_USE annotation: `String 
@EncodableStringAnnotation`. The annotation is defined
- * with a runtime retention and is allowed on fields, parameters, local 
variables, and type uses, so it survives many
- * common Scala typing patterns (e.g., inferred vals, constructor params, or 
aliases). Users normally do not construct the
- * annotation directly; instead, they use helper type aliases/factories in 
`PyStringTypes` for readability.
- *
- * ==Render modes==
- *
- * A `PythonTemplateBuilder` can be rendered in two modes:
- *
- *  - `plain`: emit everything as raw text (useful for debugging or when you 
know all content is safe).
- *  - `encode`: emit encodable chunks as Python decode expressions (the 
default `toString` behavior).
- *
- * Internally this is represented as a small sealed trait enum 
(`RenderMode.Plain` / `RenderMode.Encode`) rather than an
- * integer flag, to keep call sites self-documenting and avoid “magic numbers”.
- *
- * ==Chunk model (immutable, composable)==
- *
- * A builder is an immutable list of chunks:
- *
- *  - `Text(value)` for literal template parts
- *  - `Value(renderer)` for interpolated arguments that know how to render in 
each mode
- *
- * Two concrete renderers are provided:
- *
- *  - `EncodableStringRenderer`: pre-encodes `stringValue` as base64 (UTF-8) 
once, and in `Encode` mode produces a Python
- *    expression like `self.decode_python_template('<b64>')` given by 
[[wrapWithPythonDecoderExpr]].
- *  - `PyLiteralStringRenderer`: always emits the raw string value unchanged.
- *
- * Builders can be concatenated with `+` (builder + builder), which merges 
adjacent `Text` chunks for compactness.
- * Direct concatenation with a plain `String` is intentionally unsupported to 
prevent bypassing the macro’s safety checks.
- *
- * ==How the `pyb"..."` macro works==
- *
- * The `pyb` interpolator is implemented as a Scala macro. At compile time it 
receives:
- *
- *  - the literal parts from the `StringContext` (the “gaps” around `$args`)
- *  - the argument trees corresponding to each `$arg`
- *
- * The macro’s pipeline is:
- *
- *  1. '''Extract literal parts''' from the `StringContext` AST and ensure 
they are *string literals*. If any part is not
- *     a literal, compilation aborts. This prevents “template text” from being 
computed dynamically where correctness and
- *     boundary analysis would become unreliable.
- *
- * 2. '''Classify direct encodable arguments''' using `EncodableInspector`:
- * it inspects both the argument symbol and the argument type to determine 
whether the encodable annotation is present.
- * This includes a small “accessor hop” so that annotations placed on 
fields/constructor params are still visible when
- * call sites reference getters.
- *
- * 3. '''Compile-time boundary validation for direct encodables''':
- * if an argument is directly encodable (and not a nested builder), 
`BoundaryValidator.validateCompileTime` is run on
- * its surrounding literal context. The validator performs quick lexical 
checks on the current line:
- *
- *       - the splice must not occur inside an unclosed single/double-quoted 
string
- *       - the splice must not occur after a `#` comment marker
- *       - the splice must not be immediately adjacent to identifier 
characters or quote characters on either side
- *
- * These restrictions exist because an Encodable string renders as a Python 
*expression*, not a Python string literal.
- * Putting an expression inside quotes, inside a comment, or glued to an 
identifier would either be invalid Python or
- * silently change tokenization in surprising ways.
- *
- * 4. '''Lower each argument into a builder''':
- * every `$arg` becomes a `PythonTemplateBuilder`.
- *
- *       - If the argument is already a `PythonTemplateBuilder`, it is used 
directly.
- *       - Otherwise, it is wrapped into a `StringRenderer` 
(`EncodableStringRenderer` or `PyLiteralStringRenderer`) and
- *         turned into a minimal builder containing a single `Value(...)` 
chunk.
- *
- * Each argument is evaluated once and stored in a fresh local `val 
__pyb_argN` so that expensive expressions or
- * side-effects are not duplicated by expansion.
- *
- * 5. '''Runtime safety for nested builders''':
- * for arguments that are themselves `PythonTemplateBuilder`s, the macro 
cannot always know at compile time whether they
- * contain Encodable chunks (they may be computed, returned, or composed 
elsewhere). For these nested builders, the macro
- * conditionally emits runtime guards *only when the surrounding context is 
unsafe* (inside quotes, after comments, or
- * adjacent to “bad neighbor” characters). The guard pattern is:
- *
- * {{{
- *     if (__pyb_argN.containsEncodableString) throw new 
IllegalArgumentException("...")
- * }}}
- *
- * This preserves the ergonomics of composing builders while keeping the same 
safety contract as direct splices.
- *
- * 6. '''Assemble the final builder''':
- * the macro concatenates `text0 + arg0 + text1 + arg1 + ... + textN` into one 
`PythonTemplateBuilder`.
- *
- * ==Lexical checks (best-effort, intentionally small)==
- *
- * The boundary rules rely on `PythonLexerUtils`, a tiny state machine that 
scans only the “current line tail” to decide
- * whether quotes are unbalanced and whether a `#` begins a comment outside 
quotes. This is not a full Python parser.
- * It is deliberately lightweight so the macro stays fast and so the helpers 
can be unit-tested independently.
- *
- * ==Extensibility notes==
- *
- * The design keeps all rendering behavior behind `StringRenderer`, and keeps 
boundary policy in `BoundaryValidator`.
- * If new encoding schemes, alternate runtime decode helpers, or additional 
safety rules are needed, they can be introduced
- * without rewriting the template-building API. In particular, swapping 
`wrapWithPythonDecoderExpr` or adding new renderers
- * is a contained change: the macro only needs to decide *which renderer* to 
use, not *how it renders*.
- */
+  * =PythonTemplateBuilder: ergonomic Python codegen via `pyb"..."`=
+  *
+  * This module provides a tiny DSL for assembling Python source code from 
Scala while preserving two competing goals:
+  * (1) developers want to write templates that look like normal Python, and 
(2) user-provided text must not be injected
+  * into the emitted Python as raw literals that can break syntax or create 
ambiguous token boundaries.
+  *
+  * The core idea is that every value spliced into a `pyb"..."` template is 
first classified into one of two buckets:
+  *
+  *  - '''Python literals''' (ordinary Scala strings or already-safe 
fragments) are inserted as-is.
+  *  - '''Encodable strings''' (typically UI-provided text) are base64-encoded 
at build time and rendered as a *Python
+  *    expression* that decodes at runtime, rather than being embedded as a 
Python string literal.
+  *
+  * This classification is driven by a TYPE_USE annotation: `String 
@EncodableStringAnnotation`. The annotation is defined
+  * with a runtime retention and is allowed on fields, parameters, local 
variables, and type uses, so it survives many
+  * common Scala typing patterns (e.g., inferred vals, constructor params, or 
aliases). Users normally do not construct the
+  * annotation directly; instead, they use helper type aliases/factories in 
`PyStringTypes` for readability.
+  *
+  * ==Render modes==
+  *
+  * A `PythonTemplateBuilder` can be rendered in two modes:
+  *
+  *  - `plain`: emit everything as raw text (useful for debugging or when you 
know all content is safe).
+  *  - `encode`: emit encodable chunks as Python decode expressions (the 
default `toString` behavior).
+  *
+  * Internally this is represented as a small sealed trait enum 
(`RenderMode.Plain` / `RenderMode.Encode`) rather than an
+  * integer flag, to keep call sites self-documenting and avoid “magic 
numbers”.
+  *
+  * ==Chunk model (immutable, composable)==
+  *
+  * A builder is an immutable list of chunks:
+  *
+  *  - `Text(value)` for literal template parts
+  *  - `Value(renderer)` for interpolated arguments that know how to render in 
each mode
+  *
+  * Two concrete renderers are provided:
+  *
+  *  - `EncodableStringRenderer`: pre-encodes `stringValue` as base64 (UTF-8) 
once, and in `Encode` mode produces a Python
+  *    expression like `self.decode_python_template('<b64>')` given by 
[[wrapWithPythonDecoderExpr]].
+  *  - `PyLiteralStringRenderer`: always emits the raw string value unchanged.
+  *
+  * Builders can be concatenated with `+` (builder + builder), which merges 
adjacent `Text` chunks for compactness.
+  * Direct concatenation with a plain `String` is intentionally unsupported to 
prevent bypassing the macro’s safety checks.
+  *
+  * ==How the `pyb"..."` macro works==
+  *
+  * The `pyb` interpolator is implemented as a Scala macro. At compile time it 
receives:
+  *
+  *  - the literal parts from the `StringContext` (the “gaps” around `$args`)
+  *  - the argument trees corresponding to each `$arg`
+  *
+  * The macro’s pipeline is:
+  *
+  *  1. '''Extract literal parts''' from the `StringContext` AST and ensure 
they are *string literals*. If any part is not
+  *     a literal, compilation aborts. This prevents “template text” from 
being computed dynamically where correctness and
+  *     boundary analysis would become unreliable.
+  *
+  * 2. '''Classify direct encodable arguments''' using `EncodableInspector`:
+  * it inspects both the argument symbol and the argument type to determine 
whether the encodable annotation is present.
+  * This includes a small “accessor hop” so that annotations placed on 
fields/constructor params are still visible when
+  * call sites reference getters.
+  *
+  * 3. '''Compile-time boundary validation for direct encodables''':
+  * if an argument is directly encodable (and not a nested builder), 
`BoundaryValidator.validateCompileTime` is run on
+  * its surrounding literal context. The validator performs quick lexical 
checks on the current line:
+  *
+  *       - the splice must not occur inside an unclosed single/double-quoted 
string
+  *       - the splice must not occur after a `#` comment marker
+  *       - the splice must not be immediately adjacent to identifier 
characters or quote characters on either side
+  *
+  * These restrictions exist because an Encodable string renders as a Python 
*expression*, not a Python string literal.
+  * Putting an expression inside quotes, inside a comment, or glued to an 
identifier would either be invalid Python or
+  * silently change tokenization in surprising ways.
+  *
+  * 4. '''Lower each argument into a builder''':
+  * every `$arg` becomes a `PythonTemplateBuilder`.
+  *
+  *       - If the argument is already a `PythonTemplateBuilder`, it is used 
directly.
+  *       - Otherwise, it is wrapped into a `StringRenderer` 
(`EncodableStringRenderer` or `PyLiteralStringRenderer`) and
+  *         turned into a minimal builder containing a single `Value(...)` 
chunk.
+  *
+  * Each argument is evaluated once and stored in a fresh local `val 
__pyb_argN` so that expensive expressions or
+  * side-effects are not duplicated by expansion.
+  *
+  * 5. '''Runtime safety for nested builders''':
+  * for arguments that are themselves `PythonTemplateBuilder`s, the macro 
cannot always know at compile time whether they
+  * contain Encodable chunks (they may be computed, returned, or composed 
elsewhere). For these nested builders, the macro
+  * conditionally emits runtime guards *only when the surrounding context is 
unsafe* (inside quotes, after comments, or
+  * adjacent to “bad neighbor” characters). The guard pattern is:
+  *
+  * {{{
+  *     if (__pyb_argN.containsEncodableString) throw new 
IllegalArgumentException("...")
+  * }}}
+  *
+  * This preserves the ergonomics of composing builders while keeping the same 
safety contract as direct splices.
+  *
+  * 6. '''Assemble the final builder''':
+  * the macro concatenates `text0 + arg0 + text1 + arg1 + ... + textN` into 
one `PythonTemplateBuilder`.
+  *
+  * ==Lexical checks (best-effort, intentionally small)==
+  *
+  * The boundary rules rely on `PythonLexerUtils`, a tiny state machine that 
scans only the “current line tail” to decide
+  * whether quotes are unbalanced and whether a `#` begins a comment outside 
quotes. This is not a full Python parser.
+  * It is deliberately lightweight so the macro stays fast and so the helpers 
can be unit-tested independently.
+  *
+  * ==Extensibility notes==
+  *
+  * The design keeps all rendering behavior behind `StringRenderer`, and keeps 
boundary policy in `BoundaryValidator`.
+  * If new encoding schemes, alternate runtime decode helpers, or additional 
safety rules are needed, they can be introduced
+  * without rewriting the template-building API. In particular, swapping 
`wrapWithPythonDecoderExpr` or adding new renderers
+  * is a contained change: the macro only needs to decide *which renderer* to 
use, not *how it renders*.
+  */
 object PythonTemplateBuilder {
 
   // ===== render mode enum (no Ints) =====
@@ -218,19 +218,19 @@ object PythonTemplateBuilder {
   // ===== wrappers =====
 
   /**
-   * Base abstraction for values that can be spliced into a 
[[PythonTemplateBuilder]].
-   *
-   * A [[StringRenderer]] knows how to render itself depending on `mode`.
-   */
+    * Base abstraction for values that can be spliced into a 
[[PythonTemplateBuilder]].
+    *
+    * A [[StringRenderer]] knows how to render itself depending on `mode`.
+    */
   sealed trait StringRenderer extends Product with Serializable {
     def stringValue: String
     def render(mode: RenderMode): String
   }
 
   /**
-   * Encodable string: encoded-mode wraps with [[wrapWithPythonDecoderExpr]],
-   * plain-mode is raw `stringValue`.
-   */
+    * Encodable string: encoded-mode wraps with [[wrapWithPythonDecoderExpr]],
+    * plain-mode is raw `stringValue`.
+    */
   final case class EncodableStringRenderer(stringValue: String) extends 
StringRenderer {
     private val encodedB64: String =
       
Base64.getEncoder.encodeToString(stringValue.getBytes(StandardCharsets.UTF_8))
@@ -240,8 +240,8 @@ object PythonTemplateBuilder {
   }
 
   /**
-   * Python literal string: always raw `stringValue` regardless of mode.
-   */
+    * Python literal string: always raw `stringValue` regardless of mode.
+    */
   final case class PyLiteralStringRenderer(stringValue: String) extends 
StringRenderer {
     override def render(mode: RenderMode): String = stringValue
   }
@@ -253,12 +253,15 @@ object PythonTemplateBuilder {
   private[pybuilder] final case class Value(value: StringRenderer) extends 
Chunk
 
   /**
-   * Build a [[PythonTemplateBuilder]] from literal parts and already-wrapped 
args.
-   *
-   * @param literalParts raw StringContext parts (length = args + 1)
-   * @param pyArgs       args wrapped as [[StringRenderer]]
-   */
-  private[amber] def fromInterpolated(literalParts: List[String], pyArgs: 
List[StringRenderer]): PythonTemplateBuilder = {
+    * Build a [[PythonTemplateBuilder]] from literal parts and already-wrapped 
args.
+    *
+    * @param literalParts raw StringContext parts (length = args + 1)
+    * @param pyArgs       args wrapped as [[StringRenderer]]
+    */
+  private[amber] def fromInterpolated(
+      literalParts: List[String],
+      pyArgs: List[StringRenderer]
+  ): PythonTemplateBuilder = {
     require(
       literalParts.length == pyArgs.length + 1,
       s"pyb interpolator mismatch: parts=${literalParts.length}, 
args=${pyArgs.length}"
@@ -303,7 +306,8 @@ object PythonTemplateBuilder {
   // ===== custom interpolator =====
 
   /** Adds the `pyb"..."` string interpolator. */
-  implicit final class PythonTemplateBuilderStringContext(private val 
stringContext: StringContext) extends AnyVal {
+  implicit final class PythonTemplateBuilderStringContext(private val 
stringContext: StringContext)
+      extends AnyVal {
     def pyb(argValues: Any*): PythonTemplateBuilder = macro Macros.pybImpl
   }
 
@@ -311,7 +315,7 @@ object PythonTemplateBuilder {
 
     /** Macro entry point for `pyb"..."`. */
     def pybImpl(macroCtx: blackbox.Context)(
-      argValues: macroCtx.Expr[Any]*
+        argValues: macroCtx.Expr[Any]*
     ): macroCtx.Expr[PythonTemplateBuilder] = {
       import macroCtx.universe._
 
@@ -443,10 +447,11 @@ object PythonTemplateBuilder {
 }
 
 /**
- * An immutable builder for Python source produced via `pyb"..."` 
interpolation.
- */
-final class PythonTemplateBuilder private[pybuilder] (private val chunks: 
List[PythonTemplateBuilder.Chunk])
-  extends Serializable {
+  * An immutable builder for Python source produced via `pyb"..."` 
interpolation.
+  */
+final class PythonTemplateBuilder private[pybuilder] (
+    private val chunks: List[PythonTemplateBuilder.Chunk]
+) extends Serializable {
   import PythonTemplateBuilder._
 
   def +(that: PythonTemplateBuilder): PythonTemplateBuilder =
@@ -473,8 +478,7 @@ final class PythonTemplateBuilder private[pybuilder] 
(private val chunks: List[P
       case Text(text)      => out.append(text)
       case Value(renderer) => out.append(renderer.render(renderMode))
     }
-    out.toString
-      .stripMargin
+    out.toString.stripMargin
       .replace("\r\n", "\n")
       .replace("\r", "\n")
   }
diff --git 
a/common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderApiSpec.scala
 
b/common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderApiSpec.scala
new file mode 100644
index 0000000000..acbed3031f
--- /dev/null
+++ 
b/common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderApiSpec.scala
@@ -0,0 +1,247 @@
+/*
+ * 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.pybuilder
+
+import 
org.apache.texera.amber.pybuilder.PythonTemplateBuilder.RenderMode.{Encode, 
Plain}
+import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.{
+  EncodableStringRenderer,
+  PyLiteralStringRenderer,
+  fromInterpolated,
+  wrapWithPythonDecoderExpr
+}
+import org.scalatest.funsuite.AnyFunSuite
+
+import java.nio.charset.StandardCharsets
+import java.util.Base64
+
+/**
+  * Covers the non-macro public surface of PythonTemplateBuilder that 
PythonTemplateBuilderSpec
+  * exercises only incidentally: factories, renderer mode constants, render 
normalization,
+  * concatenation operators, and require/throw preconditions.
+  */
+class PythonTemplateBuilderApiSpec extends AnyFunSuite {
+
+  private def b64(s: String): String =
+    Base64.getEncoder.encodeToString(s.getBytes(StandardCharsets.UTF_8))
+
+  // -------- wrapWithPythonDecoderExpr --------
+
+  test("wrapWithPythonDecoderExpr wraps text into a decode_python_template 
call") {
+    assert(wrapWithPythonDecoderExpr("abc") == 
"self.decode_python_template('abc')")
+  }
+
+  test("wrapWithPythonDecoderExpr does not escape inner content (caller's 
responsibility)") {
+    // The current contract simply interpolates the raw text. Pinning this so 
a future
+    // escape-aware version trips this spec deliberately.
+    assert(wrapWithPythonDecoderExpr("a'b") == 
"self.decode_python_template('a'b')")
+  }
+
+  // -------- RenderMode --------
+
+  test("RenderMode.Plain and RenderMode.Encode are distinct singletons") {
+    assert(Plain != Encode)
+    assert(Plain eq PythonTemplateBuilder.RenderMode.Plain)
+    assert(Encode eq PythonTemplateBuilder.RenderMode.Encode)
+  }
+
+  // -------- EncodableStringRenderer --------
+
+  test("EncodableStringRenderer.render(Plain) returns the raw stringValue") {
+    val r = EncodableStringRenderer("abc")
+    assert(r.render(Plain) == "abc")
+    assert(r.stringValue == "abc")
+  }
+
+  test("EncodableStringRenderer.render(Encode) wraps base64 with the python 
decoder expr") {
+    val r = EncodableStringRenderer("abc")
+    assert(r.render(Encode) == s"self.decode_python_template('${b64("abc")}')")
+  }
+
+  test("EncodableStringRenderer handles empty string in both modes") {
+    val r = EncodableStringRenderer("")
+    assert(r.render(Plain) == "")
+    assert(r.render(Encode) == "self.decode_python_template('')")
+  }
+
+  test("EncodableStringRenderer uses UTF-8 base64 for non-ASCII content") {
+    val raw = "你好"
+    val r = EncodableStringRenderer(raw)
+    assert(r.render(Encode) == s"self.decode_python_template('${b64(raw)}')")
+  }
+
+  // -------- PyLiteralStringRenderer --------
+
+  test("PyLiteralStringRenderer.render ignores mode and returns the raw 
stringValue") {
+    val r = PyLiteralStringRenderer("print('x')")
+    assert(r.render(Plain) == "print('x')")
+    assert(r.render(Encode) == "print('x')")
+  }
+
+  // -------- PyStringTypes factories --------
+
+  test("PyStringTypes.EncodableStringFactory.apply returns the input string 
unchanged") {
+    val out: String = PyStringTypes.EncodableStringFactory("hi")
+    assert(out == "hi")
+  }
+
+  test("PyStringTypes.EncodableStringFactory.empty is the empty string") {
+    val out: String = PyStringTypes.EncodableStringFactory.empty
+    assert(out.isEmpty)
+  }
+
+  test("PyStringTypes.PyLiteralFactory.apply returns the input string 
unchanged") {
+    assert(PyStringTypes.PyLiteralFactory("hi") == "hi")
+  }
+
+  test("PyStringTypes.PyLiteralFactory.empty is the empty string") {
+    assert(PyStringTypes.PyLiteralFactory.empty.isEmpty)
+  }
+
+  // -------- fromInterpolated precondition --------
+
+  test("fromInterpolated requires parts.length == args.length + 1") {
+    val thrown = intercept[IllegalArgumentException] {
+      fromInterpolated(List("only-one-part"), 
List(EncodableStringRenderer("x")))
+    }
+    assert(thrown.getMessage.contains("pyb interpolator mismatch"))
+    assert(thrown.getMessage.contains("parts=1"))
+    assert(thrown.getMessage.contains("args=1"))
+  }
+
+  test("fromInterpolated with zero args and one literal part renders that 
part") {
+    val b = fromInterpolated(List("only"), Nil)
+    assert(b.plain == "only")
+  }
+
+  test("fromInterpolated alternates text/value chunks in order") {
+    val b = fromInterpolated(
+      List("a-", "-b-", "-c"),
+      List(PyLiteralStringRenderer("X"), PyLiteralStringRenderer("Y"))
+    )
+    assert(b.plain == "a-X-b-Y-c")
+  }
+
+  // -------- PythonTemplateBuilder.+ and concatChunks --------
+
+  test("operator + merges adjacent literal-only builders into a single text 
chunk") {
+    val left = fromInterpolated(List("hello "), Nil)
+    val right = fromInterpolated(List("world"), Nil)
+    val merged = left + right
+    assert(merged.plain == "hello world")
+    // Round-trip through encode mode to ensure no chunk fan-out side effects.
+    assert(merged.encode == "hello world")
+  }
+
+  test("operator + preserves value chunks across the join boundary") {
+    val left = fromInterpolated(List("pre-", "-mid"), 
List(EncodableStringRenderer("L")))
+    val right = fromInterpolated(List("-end"), Nil)
+    val merged = left + right
+    assert(merged.plain == "pre-L-mid-end")
+    assert(merged.encode == s"pre-${"self.decode_python_template('" + b64("L") 
+ "')"}-mid-end")
+  }
+
+  test("operator + with empty left builder returns content equivalent to 
right") {
+    val left = fromInterpolated(List(""), Nil)
+    val right = fromInterpolated(List("hi"), Nil)
+    assert((left + right).plain == "hi")
+  }
+
+  test("operator + with empty right builder returns content equivalent to 
left") {
+    val left = fromInterpolated(List("hi"), Nil)
+    val right = fromInterpolated(List(""), Nil)
+    assert((left + right).plain == "hi")
+  }
+
+  test("operator +(String) is unsupported and includes the offending string in 
the message") {
+    val b = fromInterpolated(List("x"), Nil)
+    val thrown = intercept[UnsupportedOperationException] {
+      b + "oops"
+    }
+    assert(thrown.getMessage.contains("oops"))
+  }
+
+  // -------- render() line-ending normalization --------
+
+  test("render normalizes CRLF to LF") {
+    val b = fromInterpolated(List("a\r\nb"), Nil)
+    assert(b.plain == "a\nb")
+  }
+
+  test("render normalizes lone CR to LF") {
+    val b = fromInterpolated(List("a\rb"), Nil)
+    assert(b.plain == "a\nb")
+  }
+
+  test("render preserves existing LF unchanged") {
+    val b = fromInterpolated(List("a\nb"), Nil)
+    assert(b.plain == "a\nb")
+  }
+
+  test("render applies stripMargin (margin char '|' strips preceding 
whitespace per line)") {
+    val b = fromInterpolated(List("first\n  |second"), Nil)
+    assert(b.plain == "first\nsecond")
+  }
+
+  // -------- containsEncodableString on edge inputs --------
+
+  test("containsEncodableString is false for a pure-text builder") {
+    val b = fromInterpolated(List("just text"), Nil)
+    assert(!b.containsEncodableString)
+  }
+
+  test("containsEncodableString is false for a builder holding only 
PyLiteralStringRenderer") {
+    val b = fromInterpolated(
+      List("", ""),
+      List(PyLiteralStringRenderer("raw"))
+    )
+    assert(!b.containsEncodableString)
+  }
+
+  test("containsEncodableString is true if any chunk is an 
EncodableStringRenderer") {
+    val b = fromInterpolated(
+      List("", "", ""),
+      List(PyLiteralStringRenderer("a"), EncodableStringRenderer("b"))
+    )
+    assert(b.containsEncodableString)
+  }
+
+  // -------- triple-quoted Python: pinning current (not-triple-quote-aware) 
behavior --------
+  //
+  // PythonLexerUtils tracks single/double quote state one character at a time 
and does not
+  // recognize Python triple-quoted strings as a single token. With six 
balanced quotes the
+  // lexer happens to also report balanced, but the *intermediate* states 
matter: any time the
+  // line tail ends with an odd count of `"`/`'`, hasUnclosedQuote returns 
true.
+  //
+  // These pin the current conservative behavior. If a future change makes the 
lexer aware of
+  // triple-quoted strings, these specs should be revisited intentionally.
+
+  test("hasUnclosedQuote: six matched double quotes are seen as balanced") {
+    assert(!PythonLexerUtils.hasUnclosedQuote("\"\"\"abc\"\"\""))
+  }
+
+  test("hasUnclosedQuote: three opening double quotes count as unclosed") {
+    // A Python triple-quoted string opener `\"\"\"` is currently reported as 
'inside string'.
+    assert(PythonLexerUtils.hasUnclosedQuote("\"\"\"abc"))
+  }
+
+  test("hasUnclosedQuote: three opening single quotes count as unclosed") {
+    assert(PythonLexerUtils.hasUnclosedQuote("'''abc"))
+  }
+}
diff --git 
a/common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderSpec.scala
 
b/common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderSpec.scala
index 7347ba8c2f..727f5cf14c 100644
--- 
a/common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderSpec.scala
+++ 
b/common/pybuilder/src/test/scala/org/apache/texera/amber/pybuilder/PythonTemplateBuilderSpec.scala
@@ -20,7 +20,11 @@
 package org.apache.texera.amber.pybuilder
 
 import org.apache.texera.amber.pybuilder.PyStringTypes.{EncodableString, 
PythonLiteral}
-import 
org.apache.texera.amber.pybuilder.PythonTemplateBuilder.{EncodableStringRenderer,
 PyLiteralStringRenderer, PythonTemplateBuilderStringContext}
+import org.apache.texera.amber.pybuilder.PythonTemplateBuilder.{
+  EncodableStringRenderer,
+  PyLiteralStringRenderer,
+  PythonTemplateBuilderStringContext
+}
 import org.scalatest.funsuite.AnyFunSuite
 
 import java.nio.charset.StandardCharsets
@@ -235,7 +239,8 @@ class PythonTemplateBuilderSpec extends AnyFunSuite {
   }
 
   test("@StringUI lambda parameter triggers UI encoding") {
-    val uiToBuilder: (String @EncodableStringAnnotation) => 
PythonTemplateBuilder = uiText => pyb"$uiText"
+    val uiToBuilder: (String @EncodableStringAnnotation) => 
PythonTemplateBuilder =
+      uiText => pyb"$uiText"
     val builder = uiToBuilder("lambda")
     assert(builder.encode == decodeExpr("lambda"))
   }
@@ -243,7 +248,9 @@ class PythonTemplateBuilderSpec extends AnyFunSuite {
   test("@StringUI lambda param + map + mkString triggers UI encoding per 
element") {
     val rawItems = List("a", "b", "c")
     val joinedEncoded =
-      rawItems.map((uiItem: String @EncodableStringAnnotation) => 
pyb"$uiItem").mkString("[", ", ", "]")
+      rawItems
+        .map((uiItem: String @EncodableStringAnnotation) => pyb"$uiItem")
+        .mkString("[", ", ", "]")
     assert(joinedEncoded == s"[${rawItems.map(decodeExpr).mkString(", ")}]")
   }
 
@@ -256,7 +263,8 @@ class PythonTemplateBuilderSpec extends AnyFunSuite {
 
   test("Erasing List[String @StringUI] to List[String] drops UI encoding") {
     val uiItems: List[String @EncodableStringAnnotation] = List("erased")
-    val erased: List[String] = uiItems.map((uiItem: String 
@EncodableStringAnnotation) => (uiItem: String))
+    val erased: List[String] =
+      uiItems.map((uiItem: String @EncodableStringAnnotation) => (uiItem: 
String))
     val builder = pyb"${erased.head}"
     assert(builder.encode == "erased")
   }
@@ -410,7 +418,6 @@ class PythonTemplateBuilderSpec extends AnyFunSuite {
     )
   }
 
-
   test("PyString (EncodableString) glued to identifier on the left does not 
compile") {
     assertDoesNotCompile("""
       import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._
@@ -433,11 +440,12 @@ class PythonTemplateBuilderSpec extends AnyFunSuite {
 
     // This is intentionally exhaustive over the implementation-defined "bad 
neighbor" set.
     // We assert only compile success/failure, not the specific error message.
-    badChars.zipWithIndex.foreach { case (ch, i) =>
-      val esc = scalaUnicodeEscape(ch)
+    badChars.zipWithIndex.foreach {
+      case (ch, i) =>
+        val esc = scalaUnicodeEscape(ch)
 
-      val leftAdj =
-        s"""
+        val leftAdj =
+          s"""
            |import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._
            |import org.apache.texera.amber.pybuilder.PyStringTypes._
            |object UiBadLeft_$i {
@@ -446,8 +454,8 @@ class PythonTemplateBuilderSpec extends AnyFunSuite {
            |}
            |""".stripMargin
 
-      val rightAdj =
-        s"""
+        val rightAdj =
+          s"""
            |import org.apache.texera.amber.pybuilder.PythonTemplateBuilder._
            |import org.apache.texera.amber.pybuilder.PyStringTypes._
            |object UiBadRight_$i {
@@ -456,8 +464,8 @@ class PythonTemplateBuilderSpec extends AnyFunSuite {
            |}
            |""".stripMargin
 
-      assertToolboxDoesNotCompile(leftAdj)
-      assertToolboxDoesNotCompile(rightAdj)
+        assertToolboxDoesNotCompile(leftAdj)
+        assertToolboxDoesNotCompile(rightAdj)
     }
   }
 
@@ -500,7 +508,9 @@ class PythonTemplateBuilderSpec extends AnyFunSuite {
     assert(outer.encode == s"pre X=${decodeExpr("Z")} post")
   }
 
-  test("nested PythonTemplateBuilder without UI can appear inside python 
quotes (no runtime checks)") {
+  test(
+    "nested PythonTemplateBuilder without UI can appear inside python quotes 
(no runtime checks)"
+  ) {
     val inner = pyb"hello"
     val outer = pyb"print('$inner')"
     assert(outer.plain == "print('hello')")
@@ -530,7 +540,9 @@ class PythonTemplateBuilderSpec extends AnyFunSuite {
     }
   }
 
-  test("nested PythonTemplateBuilder containing UI after comment marker throws 
at runtime (with and without whitespace)") {
+  test(
+    "nested PythonTemplateBuilder containing UI after comment marker throws at 
runtime (with and without whitespace)"
+  ) {
     val inner = pyb"${EncodableStringRenderer("x")}"
     intercept[IllegalArgumentException] {
       pyb"foo # $inner"
@@ -548,7 +560,9 @@ class PythonTemplateBuilderSpec extends AnyFunSuite {
     intercept[IllegalArgumentException] { pyb"${inner}2" }
   }
 
-  test("runtime guard does NOT throw when nested builder has no UI, even in 
unsafe boundary contexts") {
+  test(
+    "runtime guard does NOT throw when nested builder has no UI, even in 
unsafe boundary contexts"
+  ) {
     val inner = pyb"hello"
     val outer1 = pyb"foo$inner"
     val outer2 = pyb"${inner}bar"
@@ -595,7 +609,9 @@ class PythonTemplateBuilderSpec extends AnyFunSuite {
     assert(builder.encode.contains("self.decode_python_template("))
   }
 
-  test("format(): nested PythonTemplateBuilder containing UI is allowed (no 
runtime false positive)") {
+  test(
+    "format(): nested PythonTemplateBuilder containing UI is allowed (no 
runtime false positive)"
+  ) {
     val workflowParam = "wf"
     val portParam = pyb"int 
(${PythonTemplateBuilder.EncodableStringRenderer("\\.")}),"
 

Reply via email to