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

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


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new c0e0237653ab [SPARK-58766][PYTHON][TEST][FOLLOWUP] Render non-UTF-8 
binary->string casts in golden files and narrow the cell try/except
c0e0237653ab is described below

commit c0e0237653ab09f1f440228887336378f7f82a83
Author: Spenser Sun <[email protected]>
AuthorDate: Mon Aug 24 18:15:44 2026 +0000

    [SPARK-58766][PYTHON][TEST][FOLLOWUP] Render non-UTF-8 binary->string casts 
in golden files and narrow the cell try/except
    
    ### What changes were proposed in this pull request?
    
    Follow-up to #58000 (SPARK-58766), which made `_scalar_str` 
(`goldenutils.py`) render out-of-range temporal scalars instead of crashing. 
This does the same for the binary case and narrows the cell try/except:
    
    - **Binary repr:** an unsafe `binary`/`large_binary` -> 
`string`/`large_string` cast relabels the buffer without validating UTF-8, so 
`str(scalar)` raises `UnicodeDecodeError`. `_scalar_str` now renders the raw 
bytes for such scalars (`scalar.as_buffer().to_pybytes()`, e.g. `b'\xff'`), so 
the 4 affected `golden_pyarrow_scalar_cast_unsafe` cells render instead of 
`ERRUnicodeDecodeError`. Raw bytes keep it a drift canary that tracks what 
Arrow actually stores.
    - **Narrowing:** move `repr_value` outside the `try` in the 3 cell helpers 
(`_try_cast`, `_to_pandas_cell`, `_from_pandas_cell`), so only the conversion 
is guarded and a formatter bug fails loudly.
    
    ### Why are the changes needed?
    
    These goldens are drift canaries for the PyArrow behavior PySpark relies on 
(SPARK-54936). `ERRUnicodeDecodeError` hid a real upstream behavior behind a 
formatting failure.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. Test-only.
    
    ### How was this patch tested?
    
    Regenerated the golden (4 cells change) and swept PyArrow 18-25 x pandas 
2/3; the full `upstream/pyarrow/` and `sql/tests/coercion/` golden tests pass.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Opus 4.8)
    
    Closes #58091 from Spenserrrr/binary-repr-narrowing.
    
    Authored-by: Spenser Sun <[email protected]>
    Signed-off-by: Yicong-Huang <[email protected]>
    (cherry picked from commit f95006c1a2b47b7432b71d29677a876da761ccb4)
    Signed-off-by: Yicong-Huang <[email protected]>
---
 python/pyspark/testing/goldenutils.py               | 21 ++++++++++++++++-----
 .../pyarrow/golden_pyarrow_scalar_cast_unsafe.csv   |  4 ++--
 .../pyarrow/golden_pyarrow_scalar_cast_unsafe.md    |  4 ++--
 .../upstream/pyarrow/test_pyarrow_array_cast.py     |  2 +-
 .../test_pyarrow_array_from_pandas_default.py       |  3 ++-
 .../pyarrow/test_pyarrow_arrow_to_pandas_default.py |  3 ++-
 6 files changed, 25 insertions(+), 12 deletions(-)

diff --git a/python/pyspark/testing/goldenutils.py 
b/python/pyspark/testing/goldenutils.py
index df699a81b065..f9ba1f66fd98 100644
--- a/python/pyspark/testing/goldenutils.py
+++ b/python/pyspark/testing/goldenutils.py
@@ -316,10 +316,13 @@ class GoldenFileTestMixin:
         """
         Render one PyArrow scalar for a golden cell via PyArrow's own 
``str(scalar)``.
 
-        A temporal value can be valid in Arrow yet outside Python's 
``datetime`` range
-        (e.g. a date32 past year 9999), where ``str`` builds a Python datetime 
and
-        raises ``OverflowError``.  Record ``temporal overflow`` for those 
instead of
-        failing.  A non-temporal ``OverflowError`` is unexpected, so it 
propagates.
+        Some values are valid in Arrow yet unrenderable via ``str``:
+        - a temporal value past Python's ``datetime`` range (e.g. date32 past 
year 9999)
+          raises ``OverflowError`` -> the marker ``temporal overflow``;
+        - an unsafe ``binary``->``string`` cast relabels bytes without UTF-8 
validation,
+          so non-UTF-8 bytes raise ``UnicodeDecodeError`` -> the raw bytes, so 
the golden
+          still tracks what Arrow stored for them.
+        An unexpected error (non-temporal overflow, non-string decode error) 
propagates.
         """
         try:
             return str(scalar).replace("\x00", "\\0")
@@ -330,6 +333,14 @@ class GoldenFileTestMixin:
             if pa.types.is_temporal(scalar.type):
                 return "temporal overflow"
             raise
+        except UnicodeDecodeError:
+            # An unsafe binary->string cast relabels bytes as a string without 
UTF-8
+            # validation, so str() cannot decode non-UTF-8 bytes; render the 
raw bytes
+            # so the golden still tracks what Arrow stored for them.  A 
UnicodeDecodeError
+            # on a non-string type is unexpected, so re-raise.
+            if pa.types.is_string(scalar.type) or 
pa.types.is_large_string(scalar.type):
+                return repr(scalar.as_buffer().to_pybytes())
+            raise
 
     @classmethod
     def repr_arrow_value(
@@ -339,7 +350,7 @@ class GoldenFileTestMixin:
         Format a PyArrow Array/ChunkedArray for golden file.
 
         Each element is rendered by ``_scalar_str`` (PyArrow's scalar 
formatting, with
-        an out-of-range temporal fallback).
+        fallbacks for values that are valid in Arrow but unrenderable via 
``str``).
 
         Parameters
         ----------
diff --git 
a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_scalar_cast_unsafe.csv 
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_scalar_cast_unsafe.csv
index eb413767be56..6e8bf7bf685c 100644
--- 
a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_scalar_cast_unsafe.csv
+++ 
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_scalar_cast_unsafe.csv
@@ -35,8 +35,8 @@ string:unicode        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInval
 large_string:numeric   [0, 1, -1, None]@int8   [0, 1, -1, None]@int16  [0, 1, 
-1, None]@int32  [0, 1, -1, None]@int64  ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        [0.0, 
1.0, -1.0, None]@float16  [0.0, 1.0, -1.0, None]@float32  [0.0, 1.0, -1.0, 
None]@float64  ERR@ArrowInvalid        [0, 1, -1, None]@string [0, 1, -1, 
None]@large_string   [b'0', b'1', b'-1', None]@binary        [b'0', b'1', 
b'-1', None]@large_binary  ERR@ArrowInvalid        [0E-10, 1.0000000000, 
-1.0000000000, None]@decimal128(38, 1 [...]
 large_string:alpha     ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        [abc, , None]@string    [abc, , None]@large_string      
[b'abc', b'', None]@binary      [b'abc', b'', None]@large_binary        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@Arro [...]
 large_string:unicode   ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        [你好, مرحبا, 🎉, None]@string     [你好, مرحبا, 🎉, 
None]@large_string       [b'\xe4\xbd\xa0\xe5\xa5\xbd', 
b'\xd9\x85\xd8\xb1\xd8\xad\xd8\xa8\xd8\xa7', b'\xf0\x9f\x8e\x89', None]@binary  
  [b'\xe4\xbd\xa0\xe5\xa5\xbd', b'\xd9\x85\xd8\xb1\xd8\xad\xd8\xa8\xd8\xa7', 
b'\xf0\x9f\x8e\x89',  [...]
-binary:standard        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@UnicodeDecodeError  ERR@UnicodeDecodeError  
[b'\x00', b'\xff', b'hello', b'', None]@binary  [b'\x00', b'\xff', b'hello', 
b'', None]@large_binary    ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowNotImplementedError    
ERR@ArrowNotImplementedError    ERR@ArrowNotImple [...]
-large_binary:standard  ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@UnicodeDecodeError  ERR@UnicodeDecodeError  
[b'\x00', b'\xff', b'hello', b'', None]@binary  [b'\x00', b'\xff', b'hello', 
b'', None]@large_binary    ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowNotImplementedError    
ERR@ArrowNotImplementedError    ERR@ArrowNo [...]
+binary:standard        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        [\0, b'\xff', hello, , None]@string     [\0, b'\xff', 
hello, , None]@large_string       [b'\x00', b'\xff', b'hello', b'', 
None]@binary  [b'\x00', b'\xff', b'hello', b'', None]@large_binary    
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowNotImplementedError    ERR@ArrowNotIm [...]
+large_binary:standard  ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowInvalid        [\0, b'\xff', hello, , None]@string     [\0, b'\xff', 
hello, , None]@large_string       [b'\x00', b'\xff', b'hello', b'', 
None]@binary  [b'\x00', b'\xff', b'hello', b'', None]@large_binary    
ERR@ArrowInvalid        ERR@ArrowInvalid        ERR@ArrowInvalid        
ERR@ArrowNotImplementedError    ERR@Arro [...]
 fixed_size_binary[16]:standard ERR@ArrowNotImplementedError    
ERR@ArrowNotImplementedError    ERR@ArrowNotImplementedError    
ERR@ArrowNotImplementedError    ERR@ArrowNotImplementedError    
ERR@ArrowNotImplementedError    ERR@ArrowNotImplementedError    
ERR@ArrowNotImplementedError    ERR@ArrowNotImplementedError    
ERR@ArrowNotImplementedError    ERR@ArrowNotImplementedError    
ERR@ArrowNotImplementedError    [0123456789abcdef, 
\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0, None]@string       [0123456789abcdef, 
\0\0\0\0\0\0\0\0\0\0\0\0\0\0 [...]
 decimal128(38, 10):standard    [0, 1, -1, None]@int8   [0, 1, -1, None]@int16  
[0, 1, -1, None]@int32  [0, 1, -1, None]@int64  [0, 1, 255, None]@uint8 [0, 1, 
65535, None]@uint16      [0, 1, 4294967295, None]@uint32 [0, 1, 
18446744073709551615, None]@uint64       ERR@ArrowNotImplementedError    [0.0, 
1.5, -1.5, None]@float32  [0.0, 1.5, -1.5, None]@float64  
ERR@ArrowNotImplementedError    [0E-10, 1.5000000000, -1.5000000000, 
None]@string       [0E-10, 1.5000000000, -1.5000000000, None]@large_string 
ERR@ArrowNotImplemente [...]
 decimal128(38, 10):large       [-1, None]@int8 [-7169, None]@int16     
[1410065407, None]@int32        [9999999999, None]@int64        [255, 
None]@uint8       [58367, None]@uint16    [1410065407, None]@uint32       
[9999999999, None]@uint64       ERR@ArrowNotImplementedError    [10000000000.0, 
None]@float32   [9999999999.0, None]@float64    ERR@ArrowNotImplementedError    
[9999999999.0000000000, None]@string    [9999999999.0000000000, 
None]@large_string      ERR@ArrowNotImplementedError    
ERR@ArrowNotImplementedError    ERR@ArrowNotImplementedError    [99999 [...]
diff --git 
a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_scalar_cast_unsafe.md 
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_scalar_cast_unsafe.md
index e35d32fa14f8..dac777b15df4 100644
--- a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_scalar_cast_unsafe.md
+++ b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_scalar_cast_unsafe.md
@@ -36,8 +36,8 @@
 | large_string:numeric                       | [0, 1, -1, None]@int8        | 
[0, 1, -1, None]@int16       | [0, 1, -1, None]@int32                           
   | [0, 1, -1, None]@int64                                                     
    | ERR@ArrowInvalid             | ERR@ArrowInvalid             | 
ERR@ArrowInvalid                         | ERR@ArrowInvalid                     
                      | [0.0, 1.0, -1.0, None]@float16                 | [0.0, 
1.0, -1.0, None]@float32    [...]
 | large_string:alpha                         | ERR@ArrowInvalid             | 
ERR@ArrowInvalid             | ERR@ArrowInvalid                                 
   | ERR@ArrowInvalid                                                           
    | ERR@ArrowInvalid             | ERR@ArrowInvalid             | 
ERR@ArrowInvalid                         | ERR@ArrowInvalid                     
                      | ERR@ArrowInvalid                               | 
ERR@ArrowInvalid                  [...]
 | large_string:unicode                       | ERR@ArrowInvalid             | 
ERR@ArrowInvalid             | ERR@ArrowInvalid                                 
   | ERR@ArrowInvalid                                                           
    | ERR@ArrowInvalid             | ERR@ArrowInvalid             | 
ERR@ArrowInvalid                         | ERR@ArrowInvalid                     
                      | ERR@ArrowInvalid                               | 
ERR@ArrowInvalid                  [...]
-| binary:standard                            | ERR@ArrowInvalid             | 
ERR@ArrowInvalid             | ERR@ArrowInvalid                                 
   | ERR@ArrowInvalid                                                           
    | ERR@ArrowInvalid             | ERR@ArrowInvalid             | 
ERR@ArrowInvalid                         | ERR@ArrowInvalid                     
                      | ERR@ArrowInvalid                               | 
ERR@ArrowInvalid                  [...]
-| large_binary:standard                      | ERR@ArrowInvalid             | 
ERR@ArrowInvalid             | ERR@ArrowInvalid                                 
   | ERR@ArrowInvalid                                                           
    | ERR@ArrowInvalid             | ERR@ArrowInvalid             | 
ERR@ArrowInvalid                         | ERR@ArrowInvalid                     
                      | ERR@ArrowInvalid                               | 
ERR@ArrowInvalid                  [...]
+| binary:standard                            | ERR@ArrowInvalid             | 
ERR@ArrowInvalid             | ERR@ArrowInvalid                                 
   | ERR@ArrowInvalid                                                           
    | ERR@ArrowInvalid             | ERR@ArrowInvalid             | 
ERR@ArrowInvalid                         | ERR@ArrowInvalid                     
                      | ERR@ArrowInvalid                               | 
ERR@ArrowInvalid                  [...]
+| large_binary:standard                      | ERR@ArrowInvalid             | 
ERR@ArrowInvalid             | ERR@ArrowInvalid                                 
   | ERR@ArrowInvalid                                                           
    | ERR@ArrowInvalid             | ERR@ArrowInvalid             | 
ERR@ArrowInvalid                         | ERR@ArrowInvalid                     
                      | ERR@ArrowInvalid                               | 
ERR@ArrowInvalid                  [...]
 | fixed_size_binary[16]:standard             | ERR@ArrowNotImplementedError | 
ERR@ArrowNotImplementedError | ERR@ArrowNotImplementedError                     
   | ERR@ArrowNotImplementedError                                               
    | ERR@ArrowNotImplementedError | ERR@ArrowNotImplementedError | 
ERR@ArrowNotImplementedError             | ERR@ArrowNotImplementedError         
                      | ERR@ArrowNotImplementedError                   | 
ERR@ArrowNotImplementedError      [...]
 | decimal128(38, 10):standard                | [0, 1, -1, None]@int8        | 
[0, 1, -1, None]@int16       | [0, 1, -1, None]@int32                           
   | [0, 1, -1, None]@int64                                                     
    | [0, 1, 255, None]@uint8      | [0, 1, 65535, None]@uint16   | [0, 1, 
4294967295, None]@uint32          | [0, 1, 18446744073709551615, None]@uint64   
               | ERR@ArrowNotImplementedError                   | [0.0, 1.5, 
-1.5, None]@float32    [...]
 | decimal128(38, 10):large                   | [-1, None]@int8              | 
[-7169, None]@int16          | [1410065407, None]@int32                         
   | [9999999999, None]@int64                                                   
    | [255, None]@uint8            | [58367, None]@uint16         | 
[1410065407, None]@uint32                | [9999999999, None]@uint64            
                      | ERR@ArrowNotImplementedError                   | 
[10000000000.0, None]@float32     [...]
diff --git a/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_cast.py 
b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_cast.py
index 128557fa6548..7b22bbd0b44f 100644
--- a/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_cast.py
+++ b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_cast.py
@@ -127,9 +127,9 @@ class _PyArrowCastTestBase(GoldenFileTestMixin, 
unittest.TestCase):
         """
         try:
             result = src_arr.cast(tgt_type, safe=safe)
-            return self.repr_value(result, max_len=0)
         except Exception as e:
             return f"ERR@{type(e).__name__}"
+        return self.repr_value(result, max_len=0)
 
 
 # ============================================================
diff --git 
a/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_default.py
 
b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_default.py
index 6b7db7ee8c18..a2f7b99afb38 100644
--- 
a/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_default.py
+++ 
b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_default.py
@@ -120,9 +120,10 @@ class _PyArrowFromPandasTestBase(GoldenFileTestMixin, 
unittest.TestCase):
         as a golden-file cell, returning ``ERR@<ExceptionClass>`` if the 
conversion raises.
         """
         try:
-            return self.repr_from_pandas_result(pa.Array.from_pandas(series, 
**from_pandas_kwargs))
+            result = pa.Array.from_pandas(series, **from_pandas_kwargs)
         except Exception as e:
             return f"ERR@{type(e).__name__}"
+        return self.repr_from_pandas_result(result)
 
     def _numpy_backed_sources(self):
         """
diff --git 
a/python/pyspark/tests/upstream/pyarrow/test_pyarrow_arrow_to_pandas_default.py 
b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_arrow_to_pandas_default.py
index 68c9c4e5de51..fc154d70988d 100644
--- 
a/python/pyspark/tests/upstream/pyarrow/test_pyarrow_arrow_to_pandas_default.py
+++ 
b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_arrow_to_pandas_default.py
@@ -92,9 +92,10 @@ class _PyArrowToPandasTestBase(GoldenFileTestMixin, 
unittest.TestCase):
         conversion raises.
         """
         try:
-            return self.repr_value(arr.to_pandas(**to_pandas_kwargs), 
max_len=0)
+            result = arr.to_pandas(**to_pandas_kwargs)
         except Exception as e:
             return f"ERR@{type(e).__name__}"
+        return self.repr_value(result, max_len=0)
 
     def _numeric_sources(self):
         """Integer, float, and boolean arrays."""


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to