spark-test commented on code in PR #59980:
URL: https://github.com/apache/spark/pull/59980#discussion_r4466146674
##########
python/pyspark/sql/transpile.py:
##########
@@ -928,6 +928,13 @@ def _get_transpilers(session: "SparkSession") ->
List[AbstractTranspiler]:
if not configured_transpilers:
return []
transpiler_names = configured_transpilers.split(",")
+ unknown_names = [name for name in transpiler_names if name not in
AbstractTranspiler.varieties]
Review Comment:
**Non-blocking (P2):** `unknown_names` is built from the raw `split(",")`
entries, so an empty or whitespace-only entry (from a trailing or doubled
comma, or `"catalyst, "`) is reported as an unknown transpiler, although it
names nothing and the returned list is the same as for `"catalyst"`. With
`pyTranspilers="catalyst,"`, every UDF definition that reaches transpilation
(`transpilePyUDFs=true`, ANSI on, classic session) now warns `Ignoring unknown
transpilers [''] ...`. Under warnings-as-errors (`-W error`, or pytest's
`filterwarnings = error`), defining a UDF that lowered on the base branch now
fails with `Exception transpiling UDF ...` instead. Spark's other
comma-separated confs treat such entries as formatting
(`SparkStringUtils.stringToSeq` drops them).
Skipping blank entries in this check, e.g. `if name.strip() and name not in
AbstractTranspiler.varieties`, avoids this. A padded name such as `" catalyst"`
should still be reported, because selection does not match it either.
**Verification:**
- **Behavior:** For transpiler-name values that differ only by trailing,
doubled, or whitespace-only entries, the selected transpilers are identical and
no unknown-transpiler warning is recorded.
- **Regression:** With transpilation and ANSI enabled and warnings escalated
to errors, defining a lowerable UDF succeeds and produces lowered options for
each transpiler-name value whose only unregistered entries are empty or
whitespace-only, covering trailing ('catalyst,'), doubled
('catalyst,,catalyst') and whitespace-only ('catalyst, ') entries.
- **Compatibility:** Values with a non-blank unregistered entry keep their
current transpiler selection and still produce exactly one warning naming
exactly those entries.
##########
python/pyspark/sql/transpile.py:
##########
@@ -928,6 +928,13 @@ def _get_transpilers(session: "SparkSession") ->
List[AbstractTranspiler]:
if not configured_transpilers:
return []
transpiler_names = configured_transpilers.split(",")
+ unknown_names = [name for name in transpiler_names if name not in
AbstractTranspiler.varieties]
+ if unknown_names:
+ warnings.warn(
Review Comment:
**Non-blocking (P2):** This warning is raised inside `_transpile_func`'s
catch-all: `_get_transpilers` is called at line 1385 within the `try` whose
`except Exception` (line 1406) returns no options. Once a warnings filter turns
the UserWarning into an exception, the warn raises before the registered
transpilers are returned, so none of them is tried, although the message says
only the unknown entries are ignored.
For example, take `pyTranspilers="catalyst,catalist"` with
`simplefilter("error")` plus an ignore for `Unable to transpile UDF`, which
warnings-as-errors users already need in order to define UDFs that cannot be
lowered. A UDF that the base branch lowered with `catalyst` now gets no lowered
options and no visible warning. An error filter aimed only at this message does
the same, and the requested error never surfaces: the user sees only a
non-fatal `Unable to transpile UDF ...` warning. The UDF then runs as a plain
Python UDF (BatchEvalPythonExec, no codegen or filter pushdown), and a
non-inner join condition in which it references both sides fails to plan.
Results stay correct, and only UDF definitions that reach transpilation
(`transpilePyUDFs=true`, ANSI on, classic session) are affected. The module
already avoids this hazard for the source parse: `_syntax_warnings_suppressed`
exists because otherwise "under warnings-as-errors the parse raises and
lowering silently turns off".
**Recommended change:** Stop raising the unknown-transpiler warning inside
transpiler selection. Have selection return the unregistered entries alongside
the transpilers it keeps, and pass them out of the transpilation attempt with
its existing results. The UDF constructor then issues the single UserWarning
after the attempt and outside its fallback handling. Update the PySpark UDF
transpile unit tests to check the warning at UDF definition, and the outcome
under an escalating warnings filter.
**Why this works:** Selection of the registered transpilers, and lowering
with them, now completes before any warning is raised, and neither fallback
handler can catch the warning. Without escalation, the UDF keeps its lowered
options and the user sees the warning. When a filter escalates the warning, the
exception carrying the unknown-name message propagates from UDF definition, so
the diagnostic can no longer silently switch lowering off.
**Scope:** Move the reporting of unregistered transpiler names from inside
the transpiler's catch-all to the UDF constructor, after the transpilation
attempt, and test it at UDF definition.
**Compatibility:** Without escalation, the transpilers selected for every
pyTranspilers value, and the resulting lowering, stay as on the base branch.
The transpilation attempt keeps its rule that an inability to transpile never
breaks a working UDF.
**Risks:** Under a filter that escalates this warning, defining a UDF while
pyTranspilers contains an unregistered entry raises the unknown-name
UserWarning directly. On the base branch such a value produced no warning at
all, and the current patch instead raises a nested fallback warning or runs the
UDF without lowering. That is the explicit outcome the filter asks for, and
ignoring the message restores silent operation.
**Constraints:** The transpilation attempt must still never raise because a
function cannot be transpiled, as its existing fallback contract requires. Only
the constructor-level warning may propagate, and only when a filter escalates
it. Do not add a conf read to UDF definition; carry the unregistered entries
out of the existing transpilation call.
**Exclusions:**
- Which configured entries count as unregistered, for example empty entries
left by a stray comma, is handled by the separate fix for blank entries.
**Success:** With transpilation enabled, ANSI on, and pyTranspilers set to a
registered plus an unregistered name such as 'catalyst,catalist', defining a
lowerable UDF under default filters keeps its lowered options and emits exactly
one UserWarning naming the unregistered entry. Under a warnings filter that
escalates that UserWarning, including one that ignores the 'Unable to transpile
UDF' fallback warning, defining the UDF raises the unknown-name warning and
never yields a silently unlowered UDF. A value whose entries are all
registered, such as the default 'catalyst', emits no warning and lowers exactly
as on the base branch under every warnings filter.
**Verification:**
- **Regression:** With a registered plus an unregistered name configured,
defining a lowerable UDF raises the unknown-name UserWarning, instead of
producing a UDF with no lowered options, under each warnings filter that
escalates that warning while leaving the 'Unable to transpile UDF' fallback
warning non-fatal or ignored, covering at least all warnings escalated with
that fallback message ignored, and an error filter for the unknown-name message
alone.
- **Behavior:** Under default warnings filters, defining a lowerable UDF
with a mixed transpiler-name value keeps non-empty lowered options and records
exactly one warning naming only the unregistered entries.
- **Compatibility:** With the default transpiler-name value, defining a UDF
emits no new warning and produces the same lowered options under default and
error warnings filters.
##########
python/pyspark/sql/tests/test_udf_transpile_unit.py:
##########
@@ -179,6 +180,27 @@ def plus_four(x):
"when both transpilePyUDFs and ANSI mode are enabled",
)
+ def test_get_transpilers_warns_on_unknown_names(self):
+ import warnings
+
+ for names, expected, unknown in [
+ ("catalyst", [CatalystTranspiler], None),
+ ("catalyst,catalist", [CatalystTranspiler], "catalist"),
+ ("catalist", [], "catalist"),
+ ]:
+ with self.subTest(names=names):
+ with
self.sql_conf({"spark.sql.experimental.optimizer.pyTranspilers": names}):
+ with warnings.catch_warnings(record=True) as caught:
+ warnings.simplefilter("always")
+ transpilers = _get_transpilers(self.spark)
+ self.assertEqual([type(t) for t in transpilers], expected)
+ messages = [str(w.message) for w in caught if "pyTranspilers"
in str(w.message)]
+ if unknown is None:
+ self.assertEqual(messages, [])
+ else:
+ self.assertEqual(len(messages), 1)
+ self.assertIn(repr(unknown), messages[0])
Review Comment:
**Nit (P3):** These assertions only check that one message mentions
`pyTranspilers` and contains `repr(unknown)`. A regression that also reports
`catalyst` as ignored, such as `Ignoring unknown transpilers ['catalyst',
'catalist'] ...; registered transpilers are ['catalist']`, passes every
subtest. Asserting the full expected message, or the exact ignored and
registered lists, for each case would pin the content this PR adds.
**Verification:**
- **Regression:** A variant of the warning that keeps the same transpiler
selection but lists 'catalyst' among the ignored entries, or misstates the
registered transpilers, makes the unregistered-name test fail.
--
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]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]