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

zhengruifeng 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 b34b6cdef0e2 [SPARK-58657][PYTHON][TEST] Add tests for 
pa.Array.from_pandas with the mask argument
b34b6cdef0e2 is described below

commit b34b6cdef0e20ee2772f5280270209959c5046bc
Author: Spenser Sun <[email protected]>
AuthorDate: Tue Aug 11 11:43:20 2026 +0800

    [SPARK-58657][PYTHON][TEST] Add tests for pa.Array.from_pandas with the 
mask argument
    
    ### What changes were proposed in this pull request?
    
    This PR adds a golden-file test pinning the behavior of 
`pa.Array.from_pandas()` under the `mask` argument, part of the umbrella effort 
([SPARK-54936](https://issues.apache.org/jira/browse/SPARK-54936)) to monitor 
upstream PyArrow/pandas behavior that PySpark's pandas <-> Arrow conversion 
layer depends on. These tests run without a Spark session and act as a drift 
canary: if a library upgrade changes the primitive's behavior, the golden 
comparison fails loudly at the exact cell.
    
    The changes:
    
    - New `test_pyarrow_array_from_pandas_non_default.py` with 
`PyArrowArrayFromPandasMaskTests`, which subclasses 
`_PyArrowFromPandasTestBase` (from the default test) to reuse its source Series 
inventory, and records `golden_pyarrow_array_from_pandas_mask.{csv,md}` (98 
rows x 3 columns: `pandas series | mask=None | mask=isnull()`).
    - A small behavior-preserving refactor of the merged default test, 
`test_pyarrow_array_from_pandas_default.py`:
      - the source-Series inventory and `repr_from_pandas_result` were already 
lifted into a test-free base `_PyArrowFromPandasTestBase`, with 
`PyArrowArrayFromPandasDefaultTests` as a thin subclass, so the non-default 
tests can subclass the base directly (no unbound-call trick, and the base 
carries no `test_*` to be re-collected);
      - the shared `_from_pandas_cell(series, **kwargs)` cell formatter is now 
a method on that base, used by both the default and the new mask test.
    - Registered the new module in `dev/sparktestsupport/modules.py`.
    
    PySpark does not pass `mask` freely; it derives it from how the Series is 
stored (`python/pyspark/sql/conversion.py:435`, 
`python/pyspark/sql/pandas/conversion.py:113`):
    
    ```python
    mask = None if hasattr(series.array, "__arrow_array__") else series.isnull()
    ```
    
    The golden records both `mask=None` and `mask=isnull()` so the two regimes 
stay observable:
    
    - **numpy-backed** dtypes do not implement `__arrow_array__`, so PySpark 
passes `mask=series.isnull()`. `from_pandas` also infers nulls from the Series 
at `mask=None`, so the two columns agree; the test pins that agreement.
    - **protocol** dtypes (implementing `__arrow_array__` -- the nullable 
extension, `string[python]`, and `[pyarrow]` dtypes) return a finished Arrow 
array with their own validity bitmap, so PyArrow rejects a caller-supplied mask 
with `ValueError` on the argument's mere presence (even an all-False no-op mask 
raises). PySpark passes `mask=None` for them, and `mask=isnull()` records 
`ERRValueError`.
    
    Because `mask` is fixed by the input row's dtype, it is a column pair 
rather than an independent matrix dimension.
    
    ### Why are the changes needed?
    
    PySpark calls `pa.Array.from_pandas(series, mask=mask, ...)` on the pandas 
-> Arrow path (`createDataFrame(pandas_df)` and every pandas UDF's return 
value). The `mask` argument's two-regime behavior -- silently accepted on 
numpy-backed input, rejected on protocol input -- is upstream behavior PySpark 
relies on but does not itself test. A golden test makes any future drift (for 
example, protocol dtypes beginning to accept a mask, or `mask=None` no longer 
inferring nulls) fail visibly i [...]
    
    ### Does this PR introduce _any_ user-facing change?
    
    No. This adds tests only.
    
    ### How was this patch tested?
    
    New golden-file test, run with and without `SPARK_GENERATE_GOLDEN_FILES=1` 
(regeneration is byte-identical). Validated across the full support matrix -- 
PyArrow 18, 19, 20, 21, 22, 23, 24, 25 x pandas 2 and 3 (16/16 combinations 
pass), each in a fresh virtualenv running the committed test against the 
committed golden. `ruff check` and `ruff format --check` are clean. The 
pandas-3 differences are recorded as version-guarded `overrides` (notably 
`string:inferred`, whose dtype becomes th [...]
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Code (Anthropic), model Claude Opus 4.8
    
    Closes #57862 from Spenserrrr/from-pandas-mask-tests.
    
    Authored-by: Spenser Sun <[email protected]>
    Signed-off-by: Ruifeng Zheng <[email protected]>
    (cherry picked from commit 4c86e4c3f1adea1e78c9776c3372f144051081c9)
    Signed-off-by: Ruifeng Zheng <[email protected]>
---
 dev/sparktestsupport/modules.py                    |   1 +
 .../golden_pyarrow_array_from_pandas_mask.csv      |  99 ++++++++++++
 .../golden_pyarrow_array_from_pandas_mask.md       | 100 ++++++++++++
 .../test_pyarrow_array_from_pandas_default.py      |  38 +++--
 .../test_pyarrow_array_from_pandas_non_default.py  | 167 +++++++++++++++++++++
 5 files changed, 393 insertions(+), 12 deletions(-)

diff --git a/dev/sparktestsupport/modules.py b/dev/sparktestsupport/modules.py
index 2ac49a4019eb..f4e3e5877054 100644
--- a/dev/sparktestsupport/modules.py
+++ b/dev/sparktestsupport/modules.py
@@ -538,6 +538,7 @@ pyspark_core = Module(
         # unittests for upstream projects
         "pyspark.tests.upstream.pyarrow.test_pyarrow_array_cast",
         
"pyspark.tests.upstream.pyarrow.test_pyarrow_array_from_pandas_default",
+        
"pyspark.tests.upstream.pyarrow.test_pyarrow_array_from_pandas_non_default",
         "pyspark.tests.upstream.pyarrow.test_pyarrow_array_type_inference",
         "pyspark.tests.upstream.pyarrow.test_pyarrow_arrow_to_pandas_default",
         
"pyspark.tests.upstream.pyarrow.test_pyarrow_arrow_to_pandas_non_default",
diff --git 
a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_array_from_pandas_mask.csv
 
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_array_from_pandas_mask.csv
new file mode 100644
index 000000000000..7af02dca017f
--- /dev/null
+++ 
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_array_from_pandas_mask.csv
@@ -0,0 +1,99 @@
+test case      pandas series   mask=None       mask=isnull()
+int8:standard  [0, 1, -1, 127, -128]@Series[int8]      [0, 1, -1, 127, 
-128]@int8      [0, 1, -1, 127, -128]@int8
+int8:empty     []@Series[int8] []@int8 []@int8
+int16:standard [0, 1, -1, 32767, -32768]@Series[int16] [0, 1, -1, 32767, 
-32768]@int16 [0, 1, -1, 32767, -32768]@int16
+int16:empty    []@Series[int16]        []@int16        []@int16
+int32:standard [0, 1, -1, 2147483647, -2147483648]@Series[int32]       [0, 1, 
-1, 2147483647, -2147483648]@int32       [0, 1, -1, 2147483647, 
-2147483648]@int32
+int32:empty    []@Series[int32]        []@int32        []@int32
+int64:standard [0, 1, -1, 9223372036854775807, 
-9223372036854775808]@Series[int64]     [0, 1, -1, 9223372036854775807, 
-9223372036854775808]@int64     [0, 1, -1, 9223372036854775807, 
-9223372036854775808]@int64
+int64:empty    []@Series[int64]        []@int64        []@int64
+int64:nullable [0.0, 1.0, nan]@Series[float64] [0.0, 1.0, None]@float64        
[0.0, 1.0, None]@float64
+uint8:standard [0, 1, 255]@Series[uint8]       [0, 1, 255]@uint8       [0, 1, 
255]@uint8
+uint16:standard        [0, 1, 65535]@Series[uint16]    [0, 1, 65535]@uint16    
[0, 1, 65535]@uint16
+uint32:standard        [0, 1, 4294967295]@Series[uint32]       [0, 1, 
4294967295]@uint32       [0, 1, 4294967295]@uint32
+uint64:standard        [0, 1, 18446744073709551615]@Series[uint64]     [0, 1, 
18446744073709551615]@uint64     [0, 1, 18446744073709551615]@uint64
+float32:standard       [0.0, 1.5, -1.5]@Series[float32]        [0.0, 1.5, 
-1.5]@float32        [0.0, 1.5, -1.5]@float32
+float32:nullable       [0.0, nan, 1.5]@Series[float32] [0.0, None, 
1.5]@float32        [0.0, None, 1.5]@float32
+float32:empty  []@Series[float32]      []@float32      []@float32
+float64:standard       [0.0, 1.5, -1.5]@Series[float64]        [0.0, 1.5, 
-1.5]@float64        [0.0, 1.5, -1.5]@float64
+float64:nullable       [0.0, nan, 1.5]@Series[float64] [0.0, None, 
1.5]@float64        [0.0, None, 1.5]@float64
+float64:empty  []@Series[float64]      []@float64      []@float64
+bool:standard  [True, False, True]@Series[bool]        [True, False, 
True]@bool        [True, False, True]@bool
+bool:empty     []@Series[bool] []@bool []@bool
+object:string  ['hello', 'world', '']@Series[object]   [hello, world, ]@string 
[hello, world, ]@string
+object:string-nullable ['hello', None, 'world']@Series[object] [hello, None, 
world]@string     [hello, None, world]@string
+string:inferred        ['hello', 'world']@Series[object]       [hello, 
world]@string   [hello, world]@string
+object:bytes   [b'hello', b'world']@Series[object]     [b'hello', 
b'world']@binary     [b'hello', b'world']@binary
+object:empty   []@Series[object]       []@null []@null
+object:all-null        [None, None]@Series[object]     [None, None]@null       
[None, None]@null
+object:decimal [Decimal('1.50'), Decimal('-2.25')]@Series[object]      [1.50, 
-2.25]@decimal128(3, 2)  [1.50, -2.25]@decimal128(3, 2)
+list<int64>:standard   [[1, 2], [3]]@Series[object]    [[1, 2], 
[3]]@list<item: int64> [[1, 2], [3]]@list<item: int64>
+list<int64>:nullable   [[1, 2], None]@Series[object]   [[1, 2], 
None]@list<item: int64>        [[1, 2], None]@list<item: int64>
+list<int64>:null-element       [[1, None], [3]]@Series[object] [[1, None], 
[3]]@list<item: int64>      [[1, None], [3]]@list<item: int64>
+list<string>:standard  [['a', 'b'], ['c']]@Series[object]      [['a', 'b'], 
['c']]@list<item: string>  [['a', 'b'], ['c']]@list<item: string>
+list<list<int64>>:standard     [[[1, 2], [3]], [[4]]]@Series[object]   [[[1, 
2], [3]], [[4]]]@list<item: list<item: int64>>    [[[1, 2], [3]], 
[[4]]]@list<item: list<item: int64>>
+list<struct>:standard  [[{'a': 1}], [{'a': 2}]]@Series[object] [[{'a': 1}], 
[{'a': 2}]]@list<item: struct<a: int64>>   [[{'a': 1}], [{'a': 2}]]@list<item: 
struct<a: int64>>
+struct:standard        [{'a': 1, 'b': 'x'}]@Series[object]     [[('a', 1), 
('b', 'x')]]@struct<a: int64, b: string>    [[('a', 1), ('b', 'x')]]@struct<a: 
int64, b: string>
+struct:nullable        [{'a': 1, 'b': 'x'}, None]@Series[object]       [[('a', 
1), ('b', 'x')], None]@struct<a: int64, b: string>      [[('a', 1), ('b', 
'x')], None]@struct<a: int64, b: string>
+struct<struct>:standard        [{'a': {'b': 1}}]@Series[object]        [[('a', 
{'b': 1})]]@struct<a: struct<b: int64>> [[('a', {'b': 1})]]@struct<a: struct<b: 
int64>>
+struct<list<int64>>:standard   [{'a': [1, 2]}]@Series[object]  [[('a', [1, 
2])]]@struct<a: list<item: int64>>  [[('a', [1, 2])]]@struct<a: list<item: 
int64>>
+datetime64[ns]:standard        [Timestamp('2024-06-15 
18:30:00')]@Series[datetime64[ns]]       [2024-06-15 18:30:00]@timestamp[ns]    
 [2024-06-15 18:30:00]@timestamp[ns]
+datetime64[ns]:nullable        [Timestamp('2024-06-15 18:30:00'), 
NaT]@Series[datetime64[ns]]  [2024-06-15 18:30:00, None]@timestamp[ns]       
[2024-06-15 18:30:00, None]@timestamp[ns]
+datetime64[ns]:empty   []@Series[datetime64[ns]]       []@timestamp[ns]        
[]@timestamp[ns]
+datetime64[ns,tz]:standard     [Timestamp('2024-06-15 18:30:00+0000', 
tz='UTC')]@Series[datetime64[ns, UTC]]   [2024-06-15 
18:30:00+00:00]@timestamp[ns, tz=UTC]       [2024-06-15 
18:30:00+00:00]@timestamp[ns, tz=UTC]
+datetime64[ns,tz]:nullable     [Timestamp('2024-06-15 18:30:00+0000', 
tz='UTC'), NaT]@Series[datetime64[ns, UTC]]      [2024-06-15 18:30:00+00:00, 
None]@timestamp[ns, tz=UTC] [2024-06-15 18:30:00+00:00, None]@timestamp[ns, 
tz=UTC]
+timedelta64[ns]:standard       [Timedelta('1 days 00:00:00'), Timedelta('0 
days 02:00:00')]@Series[timedelta64[ns]]    [1 days 00:00:00, 0 days 
02:00:00]@duration[ns] [1 days 00:00:00, 0 days 02:00:00]@duration[ns]
+timedelta64[ns]:nullable       [Timedelta('1 days 00:00:00'), 
NaT]@Series[timedelta64[ns]]     [1 days 00:00:00, None]@duration[ns]    [1 
days 00:00:00, None]@duration[ns]
+datetime64[us]:standard        [Timestamp('2024-06-15 
18:30:00')]@Series[datetime64[us]]       [2024-06-15 18:30:00]@timestamp[us]    
 [2024-06-15 18:30:00]@timestamp[us]
+datetime64[us]:nullable        [Timestamp('2024-06-15 18:30:00'), 
NaT]@Series[datetime64[us]]  [2024-06-15 18:30:00, None]@timestamp[us]       
[2024-06-15 18:30:00, None]@timestamp[us]
+datetime64[us]:empty   []@Series[datetime64[us]]       []@timestamp[us]        
[]@timestamp[us]
+datetime64[us,tz]:standard     [Timestamp('2024-06-15 18:30:00+0000', 
tz='UTC')]@Series[datetime64[us, UTC]]   [2024-06-15 
18:30:00+00:00]@timestamp[us, tz=UTC]       [2024-06-15 
18:30:00+00:00]@timestamp[us, tz=UTC]
+datetime64[us,tz]:nullable     [Timestamp('2024-06-15 18:30:00+0000', 
tz='UTC'), NaT]@Series[datetime64[us, UTC]]      [2024-06-15 18:30:00+00:00, 
None]@timestamp[us, tz=UTC] [2024-06-15 18:30:00+00:00, None]@timestamp[us, 
tz=UTC]
+timedelta64[us]:standard       [Timedelta('1 days 00:00:00'), Timedelta('0 
days 02:00:00')]@Series[timedelta64[us]]    [1 day, 0:00:00, 
2:00:00]@duration[us]  [1 day, 0:00:00, 2:00:00]@duration[us]
+timedelta64[us]:nullable       [Timedelta('1 days 00:00:00'), 
NaT]@Series[timedelta64[us]]     [1 day, 0:00:00, None]@duration[us]     [1 
day, 0:00:00, None]@duration[us]
+datetime64:inferred    [Timestamp('2024-06-15 
18:30:00')]@Series[datetime64[ns]]       [2024-06-15 18:30:00]@timestamp[ns]    
 [2024-06-15 18:30:00]@timestamp[ns]
+timedelta64:inferred   [Timedelta('1 days 00:00:00'), Timedelta('0 days 
02:00:00')]@Series[timedelta64[ns]]    [1 days 00:00:00, 0 days 
02:00:00]@duration[ns] [1 days 00:00:00, 0 days 02:00:00]@duration[ns]
+datetime64[us]:out-of-ns-range [Timestamp('1500-01-01 
00:00:00')]@Series[datetime64[us]]       [1500-01-01 00:00:00]@timestamp[us]    
 [1500-01-01 00:00:00]@timestamp[us]
+date:standard  [datetime.date(2024, 6, 15)]@Series[object]     
[2024-06-15]@date32[day]        [2024-06-15]@date32[day]
+time:standard  [datetime.time(18, 30, 45)]@Series[object]      
[18:30:45]@time64[us]   [18:30:45]@time64[us]
+object:datetime        [datetime.datetime(2024, 6, 15, 18, 30)]@Series[object] 
[2024-06-15 18:30:00]@timestamp[us]     [2024-06-15 18:30:00]@timestamp[us]
+object:datetime-sub-us [Timestamp('2024-01-01 
00:00:00.000000123')]@Series[object]     [2024-01-01 00:00:00]@timestamp[us]    
 [2024-01-01 00:00:00]@timestamp[us]
+object:timedelta       [datetime.timedelta(days=1, 
seconds=7200)]@Series[object]       [1 day, 2:00:00]@duration[us]   [1 day, 
2:00:00]@duration[us]
+category:standard      ['a', 'b', 'a']@Series[category]        [a, b, 
a]@dictionary<values=string, indices=int8, ordered=0>    [a, b, 
a]@dictionary<values=string, indices=int8, ordered=0>
+category:nullable      ['a', nan, 'b']@Series[category]        [a, None, 
b]@dictionary<values=string, indices=int8, ordered=0> [a, None, 
b]@dictionary<values=string, indices=int8, ordered=0>
+Int8:standard  [0, 1, 127, -128]@Series[Int8]  [0, 1, 127, -128]@int8  
ERR@ValueError
+Int8:nullable  [0, 1, <NA>]@Series[Int8]       [0, 1, None]@int8       
ERR@ValueError
+Int16:standard [0, 1, 32767, -32768]@Series[Int16]     [0, 1, 32767, 
-32768]@int16     ERR@ValueError
+Int16:nullable [0, 1, <NA>]@Series[Int16]      [0, 1, None]@int16      
ERR@ValueError
+Int32:standard [0, 1, 2147483647, -2147483648]@Series[Int32]   [0, 1, 
2147483647, -2147483648]@int32   ERR@ValueError
+Int32:nullable [0, 1, <NA>]@Series[Int32]      [0, 1, None]@int32      
ERR@ValueError
+Int64:standard [0, 1, 9223372036854775807, -9223372036854775808]@Series[Int64] 
[0, 1, 9223372036854775807, -9223372036854775808]@int64 ERR@ValueError
+Int64:nullable [0, 1, <NA>]@Series[Int64]      [0, 1, None]@int64      
ERR@ValueError
+UInt64:standard        [0, 1, 18446744073709551615]@Series[UInt64]     [0, 1, 
18446744073709551615]@uint64     ERR@ValueError
+Int64:empty    []@Series[Int64]        []@int64        ERR@ValueError
+Int64:all-null [<NA>, <NA>]@Series[Int64]      [None, None]@int64      
ERR@ValueError
+Float64:standard       [0.0, 1.5]@Series[Float64]      [0.0, 1.5]@float64      
ERR@ValueError
+Float64:nullable       [0.0, <NA>]@Series[Float64]     [0.0, None]@float64     
ERR@ValueError
+boolean:standard       [True, False]@Series[boolean]   [True, False]@bool      
ERR@ValueError
+boolean:nullable       [True, <NA>]@Series[boolean]    [True, None]@bool       
ERR@ValueError
+string[python]:standard        ['hello', 'world']@Series[string]       [hello, 
world]@string   ERR@ValueError
+string[python]:nullable        ['hello', <NA>]@Series[string]  [hello, 
None]@string    ERR@ValueError
+string[python]:empty   []@Series[string]       []@string       ERR@ValueError
+int64[pyarrow]:standard        [0, 1, -1]@Series[int64[pyarrow]]       [0, 1, 
-1]@int64        ERR@ValueError
+int64[pyarrow]:nullable        [0, 1, <NA>]@Series[int64[pyarrow]]     [0, 1, 
None]@int64      ERR@ValueError
+int64[pyarrow]:empty   []@Series[int64[pyarrow]]       []@int64        
ERR@ValueError
+double[pyarrow]:nullable       [0.0, <NA>]@Series[double[pyarrow]]     [0.0, 
None]@float64     ERR@ValueError
+bool[pyarrow]:nullable [True, <NA>]@Series[bool[pyarrow]]      [True, 
None]@bool       ERR@ValueError
+string[pyarrow]:standard       ['hello', 'world']@Series[string]       [hello, 
world]@large_string     ERR@ValueError
+string[pyarrow]:nullable       ['hello', <NA>]@Series[string]  [hello, 
None]@large_string      ERR@ValueError
+string[pyarrow]:empty  []@Series[string]       []@large_string ERR@ValueError
+timestamp[us][pyarrow]:standard        [Timestamp('2024-01-01 
12:00:00')]@Series[timestamp[us][pyarrow]]       [2024-01-01 
12:00:00]@timestamp[us]     ERR@ValueError
+int64[pyarrow]:single-chunk    [1, 2]@Series[int64[pyarrow]]   [1, 2]@int64    
ERR@ValueError
+int64[pyarrow]:multi-chunk     [1, 2, 3]@Series[int64[pyarrow]]        [1, 2, 
3]@chunked<int64>        ERR@ValueError
+int64:overflow [300, 1]@Series[int64]  [300, 1]@int64  [300, 1]@int64
+float64:fractional     [1.5, 2.5]@Series[float64]      [1.5, 2.5]@float64      
[1.5, 2.5]@float64
+float64:infinity       [inf, 1.0]@Series[float64]      [inf, 1.0]@float64      
[inf, 1.0]@float64
+float64:precision      [1.1234567890123]@Series[float64]       
[1.1234567890123]@float64       [1.1234567890123]@float64
+datetime64[ns]:sub-us  [Timestamp('2024-01-01 
00:00:00.000000123')]@Series[datetime64[ns]]     [2024-01-01 
00:00:00.000000123]@timestamp[ns]   [2024-01-01 
00:00:00.000000123]@timestamp[ns]
+object:date-then-datetime      [datetime.date(2024, 1, 1), 
datetime.datetime(2024, 1, 1, 5, 30)]@Series[object]        [2024-01-01, 
2024-01-01]@date32[day]    [2024-01-01, 2024-01-01]@date32[day]
+object:datetime-then-date      [datetime.datetime(2024, 1, 1, 5, 30), 
datetime.date(2024, 1, 1)]@Series[object]        ERR@ArrowTypeError      
ERR@ArrowTypeError
diff --git 
a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_array_from_pandas_mask.md
 
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_array_from_pandas_mask.md
new file mode 100644
index 000000000000..8b3c5ff0f85d
--- /dev/null
+++ 
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_array_from_pandas_mask.md
@@ -0,0 +1,100 @@
+| test case                       | pandas series                              
                                          | mask=None                           
                            | mask=isnull()                                     
              |
+|---------------------------------|--------------------------------------------------------------------------------------|-----------------------------------------------------------------|-----------------------------------------------------------------|
+| int8:standard                   | [0, 1, -1, 127, -128]@Series[int8]         
                                          | [0, 1, -1, 127, -128]@int8          
                            | [0, 1, -1, 127, -128]@int8                        
              |
+| int8:empty                      | []@Series[int8]                            
                                          | []@int8                             
                            | []@int8                                           
              |
+| int16:standard                  | [0, 1, -1, 32767, -32768]@Series[int16]    
                                          | [0, 1, -1, 32767, -32768]@int16     
                            | [0, 1, -1, 32767, -32768]@int16                   
              |
+| int16:empty                     | []@Series[int16]                           
                                          | []@int16                            
                            | []@int16                                          
              |
+| int32:standard                  | [0, 1, -1, 2147483647, 
-2147483648]@Series[int32]                                    | [0, 1, -1, 
2147483647, -2147483648]@int32                       | [0, 1, -1, 2147483647, 
-2147483648]@int32                       |
+| int32:empty                     | []@Series[int32]                           
                                          | []@int32                            
                            | []@int32                                          
              |
+| int64:standard                  | [0, 1, -1, 9223372036854775807, 
-9223372036854775808]@Series[int64]                  | [0, 1, -1, 
9223372036854775807, -9223372036854775808]@int64     | [0, 1, -1, 
9223372036854775807, -9223372036854775808]@int64     |
+| int64:empty                     | []@Series[int64]                           
                                          | []@int64                            
                            | []@int64                                          
              |
+| int64:nullable                  | [0.0, 1.0, nan]@Series[float64]            
                                          | [0.0, 1.0, None]@float64            
                            | [0.0, 1.0, None]@float64                          
              |
+| uint8:standard                  | [0, 1, 255]@Series[uint8]                  
                                          | [0, 1, 255]@uint8                   
                            | [0, 1, 255]@uint8                                 
              |
+| uint16:standard                 | [0, 1, 65535]@Series[uint16]               
                                          | [0, 1, 65535]@uint16                
                            | [0, 1, 65535]@uint16                              
              |
+| uint32:standard                 | [0, 1, 4294967295]@Series[uint32]          
                                          | [0, 1, 4294967295]@uint32           
                            | [0, 1, 4294967295]@uint32                         
              |
+| uint64:standard                 | [0, 1, 
18446744073709551615]@Series[uint64]                                          | 
[0, 1, 18446744073709551615]@uint64                             | [0, 1, 
18446744073709551615]@uint64                             |
+| float32:standard                | [0.0, 1.5, -1.5]@Series[float32]           
                                          | [0.0, 1.5, -1.5]@float32            
                            | [0.0, 1.5, -1.5]@float32                          
              |
+| float32:nullable                | [0.0, nan, 1.5]@Series[float32]            
                                          | [0.0, None, 1.5]@float32            
                            | [0.0, None, 1.5]@float32                          
              |
+| float32:empty                   | []@Series[float32]                         
                                          | []@float32                          
                            | []@float32                                        
              |
+| float64:standard                | [0.0, 1.5, -1.5]@Series[float64]           
                                          | [0.0, 1.5, -1.5]@float64            
                            | [0.0, 1.5, -1.5]@float64                          
              |
+| float64:nullable                | [0.0, nan, 1.5]@Series[float64]            
                                          | [0.0, None, 1.5]@float64            
                            | [0.0, None, 1.5]@float64                          
              |
+| float64:empty                   | []@Series[float64]                         
                                          | []@float64                          
                            | []@float64                                        
              |
+| bool:standard                   | [True, False, True]@Series[bool]           
                                          | [True, False, True]@bool            
                            | [True, False, True]@bool                          
              |
+| bool:empty                      | []@Series[bool]                            
                                          | []@bool                             
                            | []@bool                                           
              |
+| object:string                   | ['hello', 'world', '']@Series[object]      
                                          | [hello, world, ]@string             
                            | [hello, world, ]@string                           
              |
+| object:string-nullable          | ['hello', None, 'world']@Series[object]    
                                          | [hello, None, world]@string         
                            | [hello, None, world]@string                       
              |
+| string:inferred                 | ['hello', 'world']@Series[object]          
                                          | [hello, world]@string               
                            | [hello, world]@string                             
              |
+| object:bytes                    | [b'hello', b'world']@Series[object]        
                                          | [b'hello', b'world']@binary         
                            | [b'hello', b'world']@binary                       
              |
+| object:empty                    | []@Series[object]                          
                                          | []@null                             
                            | []@null                                           
              |
+| object:all-null                 | [None, None]@Series[object]                
                                          | [None, None]@null                   
                            | [None, None]@null                                 
              |
+| object:decimal                  | [Decimal('1.50'), 
Decimal('-2.25')]@Series[object]                                   | [1.50, 
-2.25]@decimal128(3, 2)                                  | [1.50, 
-2.25]@decimal128(3, 2)                                  |
+| list<int64>:standard            | [[1, 2], [3]]@Series[object]               
                                          | [[1, 2], [3]]@list<item: int64>     
                            | [[1, 2], [3]]@list<item: int64>                   
              |
+| list<int64>:nullable            | [[1, 2], None]@Series[object]              
                                          | [[1, 2], None]@list<item: int64>    
                            | [[1, 2], None]@list<item: int64>                  
              |
+| list<int64>:null-element        | [[1, None], [3]]@Series[object]            
                                          | [[1, None], [3]]@list<item: int64>  
                            | [[1, None], [3]]@list<item: int64>                
              |
+| list<string>:standard           | [['a', 'b'], ['c']]@Series[object]         
                                          | [['a', 'b'], ['c']]@list<item: 
string>                          | [['a', 'b'], ['c']]@list<item: string>       
                   |
+| list<list<int64>>:standard      | [[[1, 2], [3]], [[4]]]@Series[object]      
                                          | [[[1, 2], [3]], [[4]]]@list<item: 
list<item: int64>>            | [[[1, 2], [3]], [[4]]]@list<item: list<item: 
int64>>            |
+| list<struct>:standard           | [[{'a': 1}], [{'a': 2}]]@Series[object]    
                                          | [[{'a': 1}], [{'a': 2}]]@list<item: 
struct<a: int64>>           | [[{'a': 1}], [{'a': 2}]]@list<item: struct<a: 
int64>>           |
+| struct:standard                 | [{'a': 1, 'b': 'x'}]@Series[object]        
                                          | [[('a', 1), ('b', 'x')]]@struct<a: 
int64, b: string>            | [[('a', 1), ('b', 'x')]]@struct<a: int64, b: 
string>            |
+| struct:nullable                 | [{'a': 1, 'b': 'x'}, None]@Series[object]  
                                          | [[('a', 1), ('b', 'x')], 
None]@struct<a: int64, b: string>      | [[('a', 1), ('b', 'x')], 
None]@struct<a: int64, b: string>      |
+| struct<struct>:standard         | [{'a': {'b': 1}}]@Series[object]           
                                          | [[('a', {'b': 1})]]@struct<a: 
struct<b: int64>>                 | [[('a', {'b': 1})]]@struct<a: struct<b: 
int64>>                 |
+| struct<list<int64>>:standard    | [{'a': [1, 2]}]@Series[object]             
                                          | [[('a', [1, 2])]]@struct<a: 
list<item: int64>>                  | [[('a', [1, 2])]]@struct<a: list<item: 
int64>>                  |
+| datetime64[ns]:standard         | [Timestamp('2024-06-15 
18:30:00')]@Series[datetime64[ns]]                            | [2024-06-15 
18:30:00]@timestamp[ns]                             | [2024-06-15 
18:30:00]@timestamp[ns]                             |
+| datetime64[ns]:nullable         | [Timestamp('2024-06-15 18:30:00'), 
NaT]@Series[datetime64[ns]]                       | [2024-06-15 18:30:00, 
None]@timestamp[ns]                       | [2024-06-15 18:30:00, 
None]@timestamp[ns]                       |
+| datetime64[ns]:empty            | []@Series[datetime64[ns]]                  
                                          | []@timestamp[ns]                    
                            | []@timestamp[ns]                                  
              |
+| datetime64[ns,tz]:standard      | [Timestamp('2024-06-15 18:30:00+0000', 
tz='UTC')]@Series[datetime64[ns, UTC]]        | [2024-06-15 
18:30:00+00:00]@timestamp[ns, tz=UTC]               | [2024-06-15 
18:30:00+00:00]@timestamp[ns, tz=UTC]               |
+| datetime64[ns,tz]:nullable      | [Timestamp('2024-06-15 18:30:00+0000', 
tz='UTC'), NaT]@Series[datetime64[ns, UTC]]   | [2024-06-15 18:30:00+00:00, 
None]@timestamp[ns, tz=UTC]         | [2024-06-15 18:30:00+00:00, 
None]@timestamp[ns, tz=UTC]         |
+| timedelta64[ns]:standard        | [Timedelta('1 days 00:00:00'), 
Timedelta('0 days 02:00:00')]@Series[timedelta64[ns]] | [1 days 00:00:00, 0 
days 02:00:00]@duration[ns]                 | [1 days 00:00:00, 0 days 
02:00:00]@duration[ns]                 |
+| timedelta64[ns]:nullable        | [Timedelta('1 days 00:00:00'), 
NaT]@Series[timedelta64[ns]]                          | [1 days 00:00:00, 
None]@duration[ns]                            | [1 days 00:00:00, 
None]@duration[ns]                            |
+| datetime64[us]:standard         | [Timestamp('2024-06-15 
18:30:00')]@Series[datetime64[us]]                            | [2024-06-15 
18:30:00]@timestamp[us]                             | [2024-06-15 
18:30:00]@timestamp[us]                             |
+| datetime64[us]:nullable         | [Timestamp('2024-06-15 18:30:00'), 
NaT]@Series[datetime64[us]]                       | [2024-06-15 18:30:00, 
None]@timestamp[us]                       | [2024-06-15 18:30:00, 
None]@timestamp[us]                       |
+| datetime64[us]:empty            | []@Series[datetime64[us]]                  
                                          | []@timestamp[us]                    
                            | []@timestamp[us]                                  
              |
+| datetime64[us,tz]:standard      | [Timestamp('2024-06-15 18:30:00+0000', 
tz='UTC')]@Series[datetime64[us, UTC]]        | [2024-06-15 
18:30:00+00:00]@timestamp[us, tz=UTC]               | [2024-06-15 
18:30:00+00:00]@timestamp[us, tz=UTC]               |
+| datetime64[us,tz]:nullable      | [Timestamp('2024-06-15 18:30:00+0000', 
tz='UTC'), NaT]@Series[datetime64[us, UTC]]   | [2024-06-15 18:30:00+00:00, 
None]@timestamp[us, tz=UTC]         | [2024-06-15 18:30:00+00:00, 
None]@timestamp[us, tz=UTC]         |
+| timedelta64[us]:standard        | [Timedelta('1 days 00:00:00'), 
Timedelta('0 days 02:00:00')]@Series[timedelta64[us]] | [1 day, 0:00:00, 
2:00:00]@duration[us]                          | [1 day, 0:00:00, 
2:00:00]@duration[us]                          |
+| timedelta64[us]:nullable        | [Timedelta('1 days 00:00:00'), 
NaT]@Series[timedelta64[us]]                          | [1 day, 0:00:00, 
None]@duration[us]                             | [1 day, 0:00:00, 
None]@duration[us]                             |
+| datetime64:inferred             | [Timestamp('2024-06-15 
18:30:00')]@Series[datetime64[ns]]                            | [2024-06-15 
18:30:00]@timestamp[ns]                             | [2024-06-15 
18:30:00]@timestamp[ns]                             |
+| timedelta64:inferred            | [Timedelta('1 days 00:00:00'), 
Timedelta('0 days 02:00:00')]@Series[timedelta64[ns]] | [1 days 00:00:00, 0 
days 02:00:00]@duration[ns]                 | [1 days 00:00:00, 0 days 
02:00:00]@duration[ns]                 |
+| datetime64[us]:out-of-ns-range  | [Timestamp('1500-01-01 
00:00:00')]@Series[datetime64[us]]                            | [1500-01-01 
00:00:00]@timestamp[us]                             | [1500-01-01 
00:00:00]@timestamp[us]                             |
+| date:standard                   | [datetime.date(2024, 6, 
15)]@Series[object]                                          | 
[2024-06-15]@date32[day]                                        | 
[2024-06-15]@date32[day]                                        |
+| time:standard                   | [datetime.time(18, 30, 45)]@Series[object] 
                                          | [18:30:45]@time64[us]               
                            | [18:30:45]@time64[us]                             
              |
+| object:datetime                 | [datetime.datetime(2024, 6, 15, 18, 
30)]@Series[object]                              | [2024-06-15 
18:30:00]@timestamp[us]                             | [2024-06-15 
18:30:00]@timestamp[us]                             |
+| object:datetime-sub-us          | [Timestamp('2024-01-01 
00:00:00.000000123')]@Series[object]                          | [2024-01-01 
00:00:00]@timestamp[us]                             | [2024-01-01 
00:00:00]@timestamp[us]                             |
+| object:timedelta                | [datetime.timedelta(days=1, 
seconds=7200)]@Series[object]                            | [1 day, 
2:00:00]@duration[us]                                   | [1 day, 
2:00:00]@duration[us]                                   |
+| category:standard               | ['a', 'b', 'a']@Series[category]           
                                          | [a, b, a]@dictionary<values=string, 
indices=int8, ordered=0>    | [a, b, a]@dictionary<values=string, indices=int8, 
ordered=0>    |
+| category:nullable               | ['a', nan, 'b']@Series[category]           
                                          | [a, None, 
b]@dictionary<values=string, indices=int8, ordered=0> | [a, None, 
b]@dictionary<values=string, indices=int8, ordered=0> |
+| Int8:standard                   | [0, 1, 127, -128]@Series[Int8]             
                                          | [0, 1, 127, -128]@int8              
                            | ERR@ValueError                                    
              |
+| Int8:nullable                   | [0, 1, <NA>]@Series[Int8]                  
                                          | [0, 1, None]@int8                   
                            | ERR@ValueError                                    
              |
+| Int16:standard                  | [0, 1, 32767, -32768]@Series[Int16]        
                                          | [0, 1, 32767, -32768]@int16         
                            | ERR@ValueError                                    
              |
+| Int16:nullable                  | [0, 1, <NA>]@Series[Int16]                 
                                          | [0, 1, None]@int16                  
                            | ERR@ValueError                                    
              |
+| Int32:standard                  | [0, 1, 2147483647, 
-2147483648]@Series[Int32]                                        | [0, 1, 
2147483647, -2147483648]@int32                           | ERR@ValueError       
                                           |
+| Int32:nullable                  | [0, 1, <NA>]@Series[Int32]                 
                                          | [0, 1, None]@int32                  
                            | ERR@ValueError                                    
              |
+| Int64:standard                  | [0, 1, 9223372036854775807, 
-9223372036854775808]@Series[Int64]                      | [0, 1, 
9223372036854775807, -9223372036854775808]@int64         | ERR@ValueError       
                                           |
+| Int64:nullable                  | [0, 1, <NA>]@Series[Int64]                 
                                          | [0, 1, None]@int64                  
                            | ERR@ValueError                                    
              |
+| UInt64:standard                 | [0, 1, 
18446744073709551615]@Series[UInt64]                                          | 
[0, 1, 18446744073709551615]@uint64                             | 
ERR@ValueError                                                  |
+| Int64:empty                     | []@Series[Int64]                           
                                          | []@int64                            
                            | ERR@ValueError                                    
              |
+| Int64:all-null                  | [<NA>, <NA>]@Series[Int64]                 
                                          | [None, None]@int64                  
                            | ERR@ValueError                                    
              |
+| Float64:standard                | [0.0, 1.5]@Series[Float64]                 
                                          | [0.0, 1.5]@float64                  
                            | ERR@ValueError                                    
              |
+| Float64:nullable                | [0.0, <NA>]@Series[Float64]                
                                          | [0.0, None]@float64                 
                            | ERR@ValueError                                    
              |
+| boolean:standard                | [True, False]@Series[boolean]              
                                          | [True, False]@bool                  
                            | ERR@ValueError                                    
              |
+| boolean:nullable                | [True, <NA>]@Series[boolean]               
                                          | [True, None]@bool                   
                            | ERR@ValueError                                    
              |
+| string[python]:standard         | ['hello', 'world']@Series[string]          
                                          | [hello, world]@string               
                            | ERR@ValueError                                    
              |
+| string[python]:nullable         | ['hello', <NA>]@Series[string]             
                                          | [hello, None]@string                
                            | ERR@ValueError                                    
              |
+| string[python]:empty            | []@Series[string]                          
                                          | []@string                           
                            | ERR@ValueError                                    
              |
+| int64[pyarrow]:standard         | [0, 1, -1]@Series[int64[pyarrow]]          
                                          | [0, 1, -1]@int64                    
                            | ERR@ValueError                                    
              |
+| int64[pyarrow]:nullable         | [0, 1, <NA>]@Series[int64[pyarrow]]        
                                          | [0, 1, None]@int64                  
                            | ERR@ValueError                                    
              |
+| int64[pyarrow]:empty            | []@Series[int64[pyarrow]]                  
                                          | []@int64                            
                            | ERR@ValueError                                    
              |
+| double[pyarrow]:nullable        | [0.0, <NA>]@Series[double[pyarrow]]        
                                          | [0.0, None]@float64                 
                            | ERR@ValueError                                    
              |
+| bool[pyarrow]:nullable          | [True, <NA>]@Series[bool[pyarrow]]         
                                          | [True, None]@bool                   
                            | ERR@ValueError                                    
              |
+| string[pyarrow]:standard        | ['hello', 'world']@Series[string]          
                                          | [hello, world]@large_string         
                            | ERR@ValueError                                    
              |
+| string[pyarrow]:nullable        | ['hello', <NA>]@Series[string]             
                                          | [hello, None]@large_string          
                            | ERR@ValueError                                    
              |
+| string[pyarrow]:empty           | []@Series[string]                          
                                          | []@large_string                     
                            | ERR@ValueError                                    
              |
+| timestamp[us][pyarrow]:standard | [Timestamp('2024-01-01 
12:00:00')]@Series[timestamp[us][pyarrow]]                    | [2024-01-01 
12:00:00]@timestamp[us]                             | ERR@ValueError            
                                      |
+| int64[pyarrow]:single-chunk     | [1, 2]@Series[int64[pyarrow]]              
                                          | [1, 2]@int64                        
                            | ERR@ValueError                                    
              |
+| int64[pyarrow]:multi-chunk      | [1, 2, 3]@Series[int64[pyarrow]]           
                                          | [1, 2, 3]@chunked<int64>            
                            | ERR@ValueError                                    
              |
+| int64:overflow                  | [300, 1]@Series[int64]                     
                                          | [300, 1]@int64                      
                            | [300, 1]@int64                                    
              |
+| float64:fractional              | [1.5, 2.5]@Series[float64]                 
                                          | [1.5, 2.5]@float64                  
                            | [1.5, 2.5]@float64                                
              |
+| float64:infinity                | [inf, 1.0]@Series[float64]                 
                                          | [inf, 1.0]@float64                  
                            | [inf, 1.0]@float64                                
              |
+| float64:precision               | [1.1234567890123]@Series[float64]          
                                          | [1.1234567890123]@float64           
                            | [1.1234567890123]@float64                         
              |
+| datetime64[ns]:sub-us           | [Timestamp('2024-01-01 
00:00:00.000000123')]@Series[datetime64[ns]]                  | [2024-01-01 
00:00:00.000000123]@timestamp[ns]                   | [2024-01-01 
00:00:00.000000123]@timestamp[ns]                   |
+| object:date-then-datetime       | [datetime.date(2024, 1, 1), 
datetime.datetime(2024, 1, 1, 5, 30)]@Series[object]     | [2024-01-01, 
2024-01-01]@date32[day]                            | [2024-01-01, 
2024-01-01]@date32[day]                            |
+| object:datetime-then-date       | [datetime.datetime(2024, 1, 1, 5, 30), 
datetime.date(2024, 1, 1)]@Series[object]     | ERR@ArrowTypeError              
                                | ERR@ArrowTypeError                            
                  |
\ No newline at end of file
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 56122eabf513..125ab62789e4 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
@@ -89,16 +89,15 @@ if have_numpy:
     import numpy as np
 
 
[email protected](
-    not have_pyarrow or not have_pandas or not have_numpy,
-    pyarrow_requirement_message or pandas_requirement_message or 
numpy_requirement_message,
-)
-class PyArrowArrayFromPandasDefaultTests(GoldenFileTestMixin, 
unittest.TestCase):
+class _PyArrowFromPandasTestBase(GoldenFileTestMixin, unittest.TestCase):
     """
-    Tests pa.Array.from_pandas() with default arguments via golden file 
comparison.
+    Shared machinery for pa.Array.from_pandas() golden file tests.
 
-    Three group methods build the source Series so the non-default tests can 
reuse the whole
-    inventory or one group.  Groups are disjoint and their order fixes the row 
order.
+    Owns the source Series inventory: three disjoint group methods unioned by
+    ``_build_source_arrays``, whose order fixes the row order.  The default 
test below and
+    the non-default tests in ``test_pyarrow_array_from_pandas_non_default.py`` 
subclass this
+    to reuse the whole inventory or one group, along with 
``repr_from_pandas_result``.  This
+    base defines no ``test_*`` methods, so it contributes no tests itself.
     """
 
     @staticmethod
@@ -115,6 +114,16 @@ class 
PyArrowArrayFromPandasDefaultTests(GoldenFileTestMixin, unittest.TestCase)
             return f"{values}@chunked<{arrow_type}>"
         return rendered
 
+    def _from_pandas_cell(self, series, **from_pandas_kwargs) -> str:
+        """
+        Convert ``series`` via ``from_pandas(**from_pandas_kwargs)`` and 
format the result
+        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))
+        except Exception as e:
+            return f"ERR@{type(e).__name__}"
+
     def _numpy_backed_sources(self):
         """
         Series whose storage is numpy, so ``hasattr(series.array, 
"__arrow_array__")`` is
@@ -352,6 +361,14 @@ class 
PyArrowArrayFromPandasDefaultTests(GoldenFileTestMixin, unittest.TestCase)
             sources.update(group)
         return sources
 
+
[email protected](
+    not have_pyarrow or not have_pandas or not have_numpy,
+    pyarrow_requirement_message or pandas_requirement_message or 
numpy_requirement_message,
+)
+class PyArrowArrayFromPandasDefaultTests(_PyArrowFromPandasTestBase):
+    """Tests pa.Array.from_pandas() with default arguments via golden file 
comparison."""
+
     def test_from_pandas_default(self):
         """Test pa.Array.from_pandas() with default arguments against golden 
file."""
         sources = self._build_source_arrays()
@@ -392,10 +409,7 @@ class 
PyArrowArrayFromPandasDefaultTests(GoldenFileTestMixin, unittest.TestCase)
             if col_name == "pandas series":
                 return self.repr_value(series, max_len=0)
             else:
-                try:
-                    return 
self.repr_from_pandas_result(pa.Array.from_pandas(series))
-                except Exception as e:
-                    return f"ERR@{type(e).__name__}"
+                return self._from_pandas_cell(series)
 
         self.compare_or_generate_golden_matrix(
             row_names=row_names,
diff --git 
a/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_non_default.py
 
b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_non_default.py
new file mode 100644
index 000000000000..6439e189ff43
--- /dev/null
+++ 
b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_non_default.py
@@ -0,0 +1,167 @@
+#
+# 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.
+#
+
+"""
+Tests for PyArrow Array.from_pandas() with non-default arguments, using golden 
file
+comparison.
+
+PySpark passes ``pa.Array.from_pandas(series, mask=mask, type=arrow_type, 
safe=safecheck)``
+to convert a pandas Series into an Arrow array.  The bare-argument behavior is 
covered by
+``test_pyarrow_array_from_pandas_default.py``, whose source Series inventory
+(``_PyArrowFromPandasTestBase``) is reused here; this file records the 
non-default
+arguments so CI fails loudly if they drift across pandas/PyArrow/NumPy 
upgrades.
+
+## The ``mask`` argument
+
+PySpark derives ``mask`` from how the Series is stored
+(``conversion.py:435``, ``pandas/conversion.py:113``)::
+
+    mask = None if hasattr(series.array, "__arrow_array__") else 
series.isnull()
+
+- **numpy-backed** dtypes take ``mask=series.isnull()``, which agrees with the 
nulls
+  ``from_pandas`` infers at ``mask=None``; this test pins that agreement.
+- **protocol** dtypes (implementing ``__arrow_array__``) return a finished 
Arrow array with
+  its own validity bitmap, so PyArrow rejects any mask -- even an all-False 
no-op -- with
+  ``ValueError``; PySpark passes ``mask=None``.
+
+So ``mask`` is fixed by the row's dtype, hence a column pair (``mask=None`` vs
+``mask=isnull()``) rather than a matrix dimension.
+
+## Golden File Cell Format
+
+Each cell uses the value@type format:
+- pandas Series: "python_list_repr@Series[dtype]"
+- PyArrow Array: "python_list_repr@arrow_type"
+- PyArrow ChunkedArray: "python_list_repr@chunked<arrow_type>"
+- Error: "ERR@ExceptionClassName"
+
+## Regenerating Golden Files
+
+Set SPARK_GENERATE_GOLDEN_FILES=1 before running:
+
+    SPARK_GENERATE_GOLDEN_FILES=1 python -m pytest \\
+        
python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_non_default.py
+
+## PyArrow and pandas Version Compatibility
+
+The golden files capture behavior for specific PyArrow and pandas versions.
+Regenerate when upgrading either dependency, as from_pandas() behavior may 
change.
+The committed golden files were generated with pandas 2.3.3, pyarrow 24.0.0, 
and
+numpy 2.4.1.
+"""
+
+import unittest
+
+from pyspark.loose_version import LooseVersion
+from pyspark.testing.utils import (
+    have_pyarrow,
+    have_pandas,
+    have_numpy,
+    pyarrow_requirement_message,
+    pandas_requirement_message,
+    numpy_requirement_message,
+)
+
+# Imported as a module, not `from ... import _PyArrowFromPandasTestBase`, so 
that the
+# non-default classes pick up only the shared base -- the default test class 
is not
+# re-collected here.
+from pyspark.tests.upstream.pyarrow import 
test_pyarrow_array_from_pandas_default
+
+if have_pandas:
+    import pandas as pd
+
+
[email protected](
+    not have_pyarrow or not have_pandas or not have_numpy,
+    pyarrow_requirement_message or pandas_requirement_message or 
numpy_requirement_message,
+)
+class PyArrowArrayFromPandasMaskTests(
+    test_pyarrow_array_from_pandas_default._PyArrowFromPandasTestBase
+):
+    """
+    Tests pa.Array.from_pandas(series, mask=...) via golden file comparison.
+
+    Reuses the full source inventory.  numpy-backed rows accept a mask and 
record equal
+    results for mask=None and mask=isnull(); protocol rows reject a mask and 
record
+    ERR@ValueError for mask=isnull().
+    """
+
+    COL_MASK_NONE = "mask=None"
+    COL_MASK_ISNULL = "mask=isnull()"
+
+    def test_from_pandas_mask(self):
+        """Test pa.Array.from_pandas() with the mask argument against golden 
file."""
+        sources = self._build_source_arrays()
+        row_names = list(sources.keys())
+        col_names = ["pandas series", self.COL_MASK_NONE, self.COL_MASK_ISNULL]
+
+        # Version-specific expected values go here, keyed by (row, col), when 
a newer
+        # pandas/PyArrow/NumPy legitimately changes a cell's output.
+        overrides: dict[tuple[str, str], str] = {}
+        if LooseVersion(pd.__version__) >= LooseVersion("3.0.0"):
+            # The deliberately unpinned rows infer microseconds on pandas 3 
where pandas 2
+            # infers nanoseconds, and a categorical's values inherit the new 
str dtype's
+            # large_string backing.  These stay numpy-backed, so both mask 
columns move
+            # together and accept the mask.
+            overrides[("datetime64:inferred", "pandas series")] = (
+                "[Timestamp('2024-06-15 18:30:00')]@Series[datetime64[us]]"
+            )
+            overrides[("timedelta64:inferred", "pandas series")] = (
+                "[Timedelta('1 days 00:00:00'), "
+                "Timedelta('0 days 02:00:00')]@Series[timedelta64[us]]"
+            )
+            for col in (self.COL_MASK_NONE, self.COL_MASK_ISNULL):
+                overrides[("datetime64:inferred", col)] = "[2024-06-15 
18:30:00]@timestamp[us]"
+                overrides[("timedelta64:inferred", col)] = "[1 day, 0:00:00, 
2:00:00]@duration[us]"
+                overrides[("category:standard", col)] = (
+                    "[a, b, a]@dictionary<values=large_string, indices=int8, 
ordered=0>"
+                )
+                overrides[("category:nullable", col)] = (
+                    "[a, None, b]@dictionary<values=large_string, 
indices=int8, ordered=0>"
+                )
+            # string:inferred is object (numpy-backed) on pandas 2 but the 
dedicated str
+            # dtype (large_string, __arrow_array__) on pandas 3, so it moves 
from a
+            # mask-accepting row to a protocol row: mask=isnull() flips to 
ERR@ValueError.
+            overrides[("string:inferred", "pandas series")] = "['hello', 
'world']@Series[str]"
+            overrides[("string:inferred", self.COL_MASK_NONE)] = "[hello, 
world]@large_string"
+            overrides[("string:inferred", self.COL_MASK_ISNULL)] = 
"ERR@ValueError"
+
+        def compute_cell(row_name, col_name):
+            series = sources[row_name]
+            if col_name == "pandas series":
+                return self.repr_value(series, max_len=0)
+            elif col_name == self.COL_MASK_NONE:
+                return self._from_pandas_cell(series, mask=None)
+            elif col_name == self.COL_MASK_ISNULL:
+                return self._from_pandas_cell(series, mask=series.isnull())
+            else:
+                raise ValueError(f"unknown column: {col_name}")
+
+        self.compare_or_generate_golden_matrix(
+            row_names=row_names,
+            col_names=col_names,
+            compute_cell=compute_cell,
+            golden_file_prefix="golden_pyarrow_array_from_pandas_mask",
+            index_name="test case",
+            overrides=overrides,
+        )
+
+
+if __name__ == "__main__":
+    from pyspark.testing import main
+
+    main()


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

Reply via email to