This is an automated email from the ASF dual-hosted git repository.
LuciferYang pushed a commit to branch branch-4.2
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-4.2 by this push:
new fd33cc3c6053 [SPARK-57595][PYTHON] Declarative Pipelines analysis
context should not mask the original error when registration fails
fd33cc3c6053 is described below
commit fd33cc3c605325ebf705ead772567b93380590d0
Author: YangJie <[email protected]>
AuthorDate: Wed Jun 24 13:39:36 2026 +0800
[SPARK-57595][PYTHON] Declarative Pipelines analysis context should not
mask the original error when registration fails
### What changes were proposed in this pull request?
`add_pipeline_analysis_context` sets `extension_id = None` up front and
only assigns it after the extension is registered, but the `finally` block
always calls `client.remove_user_context_extension(extension_id)`. If any step
before that assignment fails, `extension_id` is still `None`, and
`remove_user_context_extension(None)` runs `None.find(...)` and raises
`AttributeError`, which hides the real failure.
This change runs the cleanup only when an extension was actually registered:
```python
finally:
if extension_id is not None:
client.remove_user_context_extension(extension_id)
```
### Why are the changes needed?
A failure while setting up the analysis context (for example the proto
import or `add_threadlocal_user_context_extension`) currently surfaces as
`AttributeError: 'NoneType' object has no attribute 'find'` instead of the
error that actually caused it.
### Does this PR introduce _any_ user-facing change?
No. It only changes which exception is raised when setup fails: the
original one rather than the `AttributeError` that was masking it.
### How was this patch tested?
Added a test that makes extension registration raise and checks that the
original error propagates and that no extension is left registered. It fails
without the fix and passes with it.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Claude Opus 4.8)
Closes #56652 from LuciferYang/sdp-analysis-context-finally-guard.
Authored-by: YangJie <[email protected]>
Signed-off-by: yangjie01 <[email protected]>
(cherry picked from commit 6c2a12678d3ec1c241deff1441b6278465948c94)
Signed-off-by: yangjie01 <[email protected]>
---
.../pipelines/add_pipeline_analysis_context.py | 5 ++-
.../tests/test_add_pipeline_analysis_context.py | 38 ++++++++++++++++++++++
2 files changed, 42 insertions(+), 1 deletion(-)
diff --git a/python/pyspark/pipelines/add_pipeline_analysis_context.py
b/python/pyspark/pipelines/add_pipeline_analysis_context.py
index 6ecabdf43b07..6d0bd4dd7308 100644
--- a/python/pyspark/pipelines/add_pipeline_analysis_context.py
+++ b/python/pyspark/pipelines/add_pipeline_analysis_context.py
@@ -45,4 +45,7 @@ def add_pipeline_analysis_context(
extension_id = client.add_threadlocal_user_context_extension(extension)
yield
finally:
- client.remove_user_context_extension(extension_id)
+ # extension_id stays None if registering the extension above failed;
skip cleanup in that
+ # case so we don't call remove_user_context_extension(None) and mask
the original error.
+ if extension_id is not None:
+ client.remove_user_context_extension(extension_id)
diff --git
a/python/pyspark/pipelines/tests/test_add_pipeline_analysis_context.py
b/python/pyspark/pipelines/tests/test_add_pipeline_analysis_context.py
index 44e60f4597c6..f99e7d56c3d5 100644
--- a/python/pyspark/pipelines/tests/test_add_pipeline_analysis_context.py
+++ b/python/pyspark/pipelines/tests/test_add_pipeline_analysis_context.py
@@ -15,6 +15,7 @@
# limitations under the License.
#
import unittest
+from unittest import mock
from pyspark.testing.connectutils import (
ReusedConnectTestCase,
@@ -89,6 +90,43 @@ class AddPipelineAnalysisContextTests(ReusedConnectTestCase):
thread_local_extensions_after_2 =
self.spark.client.thread_local.user_context_extensions
self.assertEqual(len(thread_local_extensions_after_2), 0)
+ def test_setup_failure_does_not_mask_original_error(self):
+ # If any setup step fails before the extension is registered,
extension_id stays None and
+ # the finally block must skip remove_user_context_extension(None) -
which would raise
+ # AttributeError and mask the original error. Cover each step that can
fail.
+ import pyspark.sql.connect.proto as pb2
+ from google.protobuf import any_pb2
+
+ failing_any = mock.MagicMock()
+ failing_any.Pack.side_effect = ValueError("boom")
+
+ failure_points = {
+ "context construction": mock.patch.object(
+ pb2, "PipelineAnalysisContext", side_effect=ValueError("boom")
+ ),
+ "extension packing": mock.patch.object(any_pb2, "Any",
return_value=failing_any),
+ "extension registration": mock.patch.object(
+ self.spark.client,
+ "add_threadlocal_user_context_extension",
+ side_effect=ValueError("boom"),
+ ),
+ }
+ for step, patcher in failure_points.items():
+ with self.subTest(failing_step=step):
+ with patcher:
+ with self.assertRaises(ValueError):
+ with add_pipeline_analysis_context(
+ self.spark, "test_dataflow_graph_id", None
+ ):
+ pass
+ # A failed setup should not leave an extension registered.
Read lazily, since the
+ # attribute is only created on first successful registration,
which never happens
+ # here.
+ thread_local_extensions = getattr(
+ self.spark.client.thread_local, "user_context_extensions",
[]
+ )
+ self.assertEqual(len(thread_local_extensions), 0)
+
if __name__ == "__main__":
from pyspark.testing import main
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]