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

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


The following commit(s) were added to refs/heads/master by this push:
     new 5a9792961eba [SPARK-54946][PYTHON][TEST] Add tests for 
pa.Array.to_pandas with coerce_temporal_nanoseconds
5a9792961eba is described below

commit 5a9792961eba9da641dd5a2aa18a09aa1f1fd1d8
Author: Spenser Sun <[email protected]>
AuthorDate: Fri Jul 24 18:13:55 2026 +0000

    [SPARK-54946][PYTHON][TEST] Add tests for pa.Array.to_pandas with 
coerce_temporal_nanoseconds
    
    ### What changes were proposed in this pull request?
    This PR adds golden-file tests that pin the behavior of PyArrow's 
`Array.to_pandas()` with non-default arguments, starting with 
`coerce_temporal_nanoseconds=True`.
    
    - New test: 
`python/pyspark/tests/upstream/pyarrow/test_pyarrow_arrow_to_pandas_non_default.py`
    - New golden files: 
`golden_pyarrow_arrow_to_pandas_coerce_temporal.{csv,md}`
    - Registered the module in `dev/sparktestsupport/modules.py`.
    
    **Structure.** The file is organized as a shared `_PyArrowToPandasTestBase` 
and one subclass per flag, where each subclass brings its own source arrays and 
test method(s) and produces its own golden file. `coerce_temporal_nanoseconds` 
is the first such subclass; a future flag like `zero_copy` (SPARK-54945) which 
needs a different row set can be added as another subclass in this same file 
rather than a new file. `test_pyarrow_arrow_to_pandas_default.py` stays 
separate.
    
    **Rows.** Limited to the temporal types the argument affects (`timestamp` 
and `duration` in units s/ms/us/ns, tz-aware `timestamp`, `date`, `time`), plus 
overflow cases and a few non-temporal control rows (`int64`/`float64`/`string`) 
to show the argument leaves non-temporal types unaffected.
    
    **Columns.** The golden file records, per source array, the resulting 
pandas Series dtype and values under two output columns: 
`coerce_temporal_nanoseconds=True` (default `date_as_object=True`) and 
`coerce_temporal_nanoseconds=True, date_as_object=False`. The second column is 
added as a column rather than as extra rows because the matrix applies one 
conversion uniformly across all rows.
    
    **Recorded behavior** (generated with pandas 2.3.3, pyarrow 24.0.0, numpy 
2.4.1):
    - `timestamp[s|ms|us|ns]` all coerce to `datetime64[ns]`.
    - tz-aware `timestamp[us, tz=UTC]` coerces to `datetime64[ns, UTC]`.
    - `duration[s|ms|us|ns]` all coerce to `timedelta64[ns]`.
    - `date32`/`date64` stay `object` (`datetime.date`) by default, but become 
`datetime64[ns]` with `date_as_object=False`; `time32`/`time64` stay `object`.
    - A far-future second-resolution timestamp (year 2500) exceeds the 
nanosecond range and raises `ArrowInvalid`, whereas an out-of-range 
`duration[s]` silently wraps to a bogus value — both are pinned.
    
    The recorded cells were verified to be identical across pyarrow 18-25 (each 
paired with a pandas/numpy that version supports), so no version-specific 
overrides are needed for the temporal cells.
    
    ### Why are the changes needed?
    This is part of SPARK-54936 ("Monitor behaviour changes from upstream").
    
    ### Does this PR introduce _any_ user-facing change?
    No.
    
    ### How was this patch tested?
    New golden-file test. Generated the golden files with
    `SPARK_GENERATE_GOLDEN_FILES=1` (pandas 2.3.3, pyarrow 24.0.0, numpy 2.4.1),
    reviewed every cell, then confirmed the test passes in comparison mode:
    
    python -m pytest
      
python/pyspark/tests/upstream/pyarrow/test_pyarrow_arrow_to_pandas_coerce_temporal.py
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Generated-by: Claude Code (Opus 4.8)
    
    Closes #57435 from Spenserrrr/coerce-temporal-nanoseconds-tests.
    
    Authored-by: Spenser Sun <[email protected]>
    Signed-off-by: Yicong-Huang <[email protected]>
---
 dev/sparktestsupport/modules.py                    |   1 +
 ...den_pyarrow_arrow_to_pandas_coerce_temporal.csv |  52 ++++
 ...lden_pyarrow_arrow_to_pandas_coerce_temporal.md |  53 ++++
 .../test_pyarrow_arrow_to_pandas_non_default.py    | 327 +++++++++++++++++++++
 4 files changed, 433 insertions(+)

diff --git a/dev/sparktestsupport/modules.py b/dev/sparktestsupport/modules.py
index 3edebcee6537..77aa43eb8d94 100644
--- a/dev/sparktestsupport/modules.py
+++ b/dev/sparktestsupport/modules.py
@@ -524,6 +524,7 @@ pyspark_core = Module(
         "pyspark.tests.upstream.pyarrow.test_pyarrow_array_cast",
         "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",
         "pyspark.tests.upstream.pyarrow.test_pyarrow_ignore_timezone",
         "pyspark.tests.upstream.pyarrow.test_pyarrow_scalar_type_coercion",
         "pyspark.tests.upstream.pyarrow.test_pyarrow_scalar_type_inference",
diff --git 
a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_arrow_to_pandas_coerce_temporal.csv
 
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_arrow_to_pandas_coerce_temporal.csv
new file mode 100644
index 000000000000..d4a8df1a2a62
--- /dev/null
+++ 
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_arrow_to_pandas_coerce_temporal.csv
@@ -0,0 +1,52 @@
+test case      pyarrow array   pandas series   pandas series 
(date_as_object=False)
+timestamp[s]:standard  [2024-01-01 12:00:00, 2024-06-15 18:30:00]@timestamp[s] 
[Timestamp('2024-01-01 12:00:00'), Timestamp('2024-06-15 
18:30:00')]@Series[datetime64[ns]]     [Timestamp('2024-01-01 12:00:00'), 
Timestamp('2024-06-15 18:30:00')]@Series[datetime64[ns]]
+timestamp[s]:nullable  [2024-01-01 12:00:00, None]@timestamp[s]        
[Timestamp('2024-01-01 12:00:00'), NaT]@Series[datetime64[ns]]  
[Timestamp('2024-01-01 12:00:00'), NaT]@Series[datetime64[ns]]
+timestamp[s]:empty     []@timestamp[s] []@Series[datetime64[ns]]       
[]@Series[datetime64[ns]]
+timestamp[ms]:standard [2024-01-01 12:00:00, 2024-06-15 
18:30:00]@timestamp[ms]        [Timestamp('2024-01-01 12:00:00'), 
Timestamp('2024-06-15 18:30:00')]@Series[datetime64[ns]]     
[Timestamp('2024-01-01 12:00:00'), Timestamp('2024-06-15 
18:30:00')]@Series[datetime64[ns]]
+timestamp[ms]:nullable [2024-01-01 12:00:00, None]@timestamp[ms]       
[Timestamp('2024-01-01 12:00:00'), NaT]@Series[datetime64[ns]]  
[Timestamp('2024-01-01 12:00:00'), NaT]@Series[datetime64[ns]]
+timestamp[ms]:empty    []@timestamp[ms]        []@Series[datetime64[ns]]       
[]@Series[datetime64[ns]]
+timestamp[us]:standard [2024-01-01 12:00:00, 2024-06-15 
18:30:00]@timestamp[us]        [Timestamp('2024-01-01 12:00:00'), 
Timestamp('2024-06-15 18:30:00')]@Series[datetime64[ns]]     
[Timestamp('2024-01-01 12:00:00'), Timestamp('2024-06-15 
18:30:00')]@Series[datetime64[ns]]
+timestamp[us]:nullable [2024-01-01 12:00:00, None]@timestamp[us]       
[Timestamp('2024-01-01 12:00:00'), NaT]@Series[datetime64[ns]]  
[Timestamp('2024-01-01 12:00:00'), NaT]@Series[datetime64[ns]]
+timestamp[us]:empty    []@timestamp[us]        []@Series[datetime64[ns]]       
[]@Series[datetime64[ns]]
+timestamp[ns]:standard [2024-01-01 12:00:00, 2024-06-15 
18:30:00]@timestamp[ns]        [Timestamp('2024-01-01 12:00:00'), 
Timestamp('2024-06-15 18:30:00')]@Series[datetime64[ns]]     
[Timestamp('2024-01-01 12:00:00'), Timestamp('2024-06-15 
18:30:00')]@Series[datetime64[ns]]
+timestamp[ns]:nullable [2024-01-01 12:00:00, None]@timestamp[ns]       
[Timestamp('2024-01-01 12:00:00'), NaT]@Series[datetime64[ns]]  
[Timestamp('2024-01-01 12:00:00'), NaT]@Series[datetime64[ns]]
+timestamp[ns]:empty    []@timestamp[ns]        []@Series[datetime64[ns]]       
[]@Series[datetime64[ns]]
+timestamp[us,tz=UTC]:standard  [2024-01-01 12:00:00+00:00, 2024-06-15 
18:30:00+00:00]@timestamp[us, tz=UTC]    [Timestamp('2024-01-01 12:00:00+0000', 
tz='UTC'), Timestamp('2024-06-15 18:30:00+0000', 
tz='UTC')]@Series[datetime64[ns, UTC]]  [Timestamp('2024-01-01 12:00:00+0000', 
tz='UTC'), Timestamp('2024-06-15 18:30:00+0000', 
tz='UTC')]@Series[datetime64[ns, UTC]]
+timestamp[us,tz=UTC]:nullable  [2024-01-01 12:00:00+00:00, None]@timestamp[us, 
tz=UTC] [Timestamp('2024-01-01 12:00:00+0000', tz='UTC'), 
NaT]@Series[datetime64[ns, UTC]]      [Timestamp('2024-01-01 12:00:00+0000', 
tz='UTC'), NaT]@Series[datetime64[ns, UTC]]
+timestamp[us,tz=UTC]:empty     []@timestamp[us, tz=UTC]        
[]@Series[datetime64[ns, UTC]]  []@Series[datetime64[ns, UTC]]
+timestamp[s]:overflow  [2500-01-01 00:00:00]@timestamp[s]      
ERR@ArrowInvalid        ERR@ArrowInvalid
+duration[s]:standard   [1 day, 0:00:00, 2:30:00]@duration[s]   [Timedelta('1 
days 00:00:00'), Timedelta('0 days 02:30:00')]@Series[timedelta64[ns]]    
[Timedelta('1 days 00:00:00'), Timedelta('0 days 
02:30:00')]@Series[timedelta64[ns]]
+duration[s]:nullable   [1 day, 0:00:00, None]@duration[s]      [Timedelta('1 
days 00:00:00'), NaT]@Series[timedelta64[ns]]     [Timedelta('1 days 
00:00:00'), NaT]@Series[timedelta64[ns]]
+duration[s]:empty      []@duration[s]  []@Series[timedelta64[ns]]      
[]@Series[timedelta64[ns]]
+duration[ms]:standard  [1 day, 0:00:00, 2:30:00]@duration[ms]  [Timedelta('1 
days 00:00:00'), Timedelta('0 days 02:30:00')]@Series[timedelta64[ns]]    
[Timedelta('1 days 00:00:00'), Timedelta('0 days 
02:30:00')]@Series[timedelta64[ns]]
+duration[ms]:nullable  [1 day, 0:00:00, None]@duration[ms]     [Timedelta('1 
days 00:00:00'), NaT]@Series[timedelta64[ns]]     [Timedelta('1 days 
00:00:00'), NaT]@Series[timedelta64[ns]]
+duration[ms]:empty     []@duration[ms] []@Series[timedelta64[ns]]      
[]@Series[timedelta64[ns]]
+duration[us]:standard  [1 day, 0:00:00, 2:30:00]@duration[us]  [Timedelta('1 
days 00:00:00'), Timedelta('0 days 02:30:00')]@Series[timedelta64[ns]]    
[Timedelta('1 days 00:00:00'), Timedelta('0 days 
02:30:00')]@Series[timedelta64[ns]]
+duration[us]:nullable  [1 day, 0:00:00, None]@duration[us]     [Timedelta('1 
days 00:00:00'), NaT]@Series[timedelta64[ns]]     [Timedelta('1 days 
00:00:00'), NaT]@Series[timedelta64[ns]]
+duration[us]:empty     []@duration[us] []@Series[timedelta64[ns]]      
[]@Series[timedelta64[ns]]
+duration[ns]:standard  [1 days 00:00:00, 0 days 02:30:00]@duration[ns] 
[Timedelta('1 days 00:00:00'), Timedelta('0 days 
02:30:00')]@Series[timedelta64[ns]]    [Timedelta('1 days 00:00:00'), 
Timedelta('0 days 02:30:00')]@Series[timedelta64[ns]]
+duration[ns]:nullable  [1 days 00:00:00, None]@duration[ns]    [Timedelta('1 
days 00:00:00'), NaT]@Series[timedelta64[ns]]     [Timedelta('1 days 
00:00:00'), NaT]@Series[timedelta64[ns]]
+duration[ns]:empty     []@duration[ns] []@Series[timedelta64[ns]]      
[]@Series[timedelta64[ns]]
+duration[s]:overflow   [109500 days, 0:00:00]@duration[s]      
[Timedelta('-104004 days +00:25:26.290448384')]@Series[timedelta64[ns]] 
[Timedelta('-104004 days +00:25:26.290448384')]@Series[timedelta64[ns]]
+date32:standard        [2024-01-01, 2024-06-15]@date32[day]    
[datetime.date(2024, 1, 1), datetime.date(2024, 6, 15)]@Series[object]  
[Timestamp('2024-01-01 00:00:00'), Timestamp('2024-06-15 
00:00:00')]@Series[datetime64[ns]]
+date32:nullable        [2024-01-01, None]@date32[day]  [datetime.date(2024, 1, 
1), None]@Series[object]        [Timestamp('2024-01-01 00:00:00'), 
NaT]@Series[datetime64[ns]]
+date32:empty   []@date32[day]  []@Series[object]       
[]@Series[datetime64[ns]]
+date64:standard        [2024-01-01, 2024-06-15]@date64[ms]     
[datetime.date(2024, 1, 1), datetime.date(2024, 6, 15)]@Series[object]  
[Timestamp('2024-01-01 00:00:00'), Timestamp('2024-06-15 
00:00:00')]@Series[datetime64[ns]]
+date64:nullable        [2024-01-01, None]@date64[ms]   [datetime.date(2024, 1, 
1), None]@Series[object]        [Timestamp('2024-01-01 00:00:00'), 
NaT]@Series[datetime64[ns]]
+date64:empty   []@date64[ms]   []@Series[object]       
[]@Series[datetime64[ns]]
+time32[s]:standard     [12:30:00, 18:45:30]@time32[s]  [datetime.time(12, 30), 
datetime.time(18, 45, 30)]@Series[object]       [datetime.time(12, 30), 
datetime.time(18, 45, 30)]@Series[object]
+time32[s]:nullable     [12:30:00, None]@time32[s]      [datetime.time(12, 30), 
None]@Series[object]    [datetime.time(12, 30), None]@Series[object]
+time32[s]:empty        []@time32[s]    []@Series[object]       
[]@Series[object]
+time32[ms]:standard    [12:30:00, 18:45:30]@time32[ms] [datetime.time(12, 30), 
datetime.time(18, 45, 30)]@Series[object]       [datetime.time(12, 30), 
datetime.time(18, 45, 30)]@Series[object]
+time32[ms]:nullable    [12:30:00, None]@time32[ms]     [datetime.time(12, 30), 
None]@Series[object]    [datetime.time(12, 30), None]@Series[object]
+time32[ms]:empty       []@time32[ms]   []@Series[object]       
[]@Series[object]
+time64[us]:standard    [12:30:00, 18:45:30]@time64[us] [datetime.time(12, 30), 
datetime.time(18, 45, 30)]@Series[object]       [datetime.time(12, 30), 
datetime.time(18, 45, 30)]@Series[object]
+time64[us]:nullable    [12:30:00, None]@time64[us]     [datetime.time(12, 30), 
None]@Series[object]    [datetime.time(12, 30), None]@Series[object]
+time64[us]:empty       []@time64[us]   []@Series[object]       
[]@Series[object]
+time64[ns]:standard    [12:30:00, 18:45:30]@time64[ns] [datetime.time(12, 30), 
datetime.time(18, 45, 30)]@Series[object]       [datetime.time(12, 30), 
datetime.time(18, 45, 30)]@Series[object]
+time64[ns]:nullable    [12:30:00, None]@time64[ns]     [datetime.time(12, 30), 
None]@Series[object]    [datetime.time(12, 30), None]@Series[object]
+time64[ns]:empty       []@time64[ns]   []@Series[object]       
[]@Series[object]
+int64:standard [0, 1, -1]@int64        [0, 1, -1]@Series[int64]        [0, 1, 
-1]@Series[int64]
+int64:nullable [0, 1, None]@int64      [0.0, 1.0, nan]@Series[float64] [0.0, 
1.0, nan]@Series[float64]
+float64:standard       [0.0, 1.5, -1.5]@float64        [0.0, 1.5, 
-1.5]@Series[float64]        [0.0, 1.5, -1.5]@Series[float64]
+string:standard        [hello, world, ]@string ['hello', 'world', 
'']@Series[object]   ['hello', 'world', '']@Series[object]
diff --git 
a/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_arrow_to_pandas_coerce_temporal.md
 
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_arrow_to_pandas_coerce_temporal.md
new file mode 100644
index 000000000000..45eec337f059
--- /dev/null
+++ 
b/python/pyspark/tests/upstream/pyarrow/golden_pyarrow_arrow_to_pandas_coerce_temporal.md
@@ -0,0 +1,53 @@
+| test case                     | pyarrow array                                
                                | pandas series                                 
                                                                                
 | pandas series (date_as_object=False)                                         
                                                  |
+|-------------------------------|------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
+| timestamp[s]:standard         | [2024-01-01 12:00:00, 2024-06-15 
18:30:00]@timestamp[s]                      | [Timestamp('2024-01-01 
12:00:00'), Timestamp('2024-06-15 18:30:00')]@Series[datetime64[ns]]            
                        | [Timestamp('2024-01-01 12:00:00'), 
Timestamp('2024-06-15 18:30:00')]@Series[datetime64[ns]]                        
            |
+| timestamp[s]:nullable         | [2024-01-01 12:00:00, None]@timestamp[s]     
                                | [Timestamp('2024-01-01 12:00:00'), 
NaT]@Series[datetime64[ns]]                                                     
            | [Timestamp('2024-01-01 12:00:00'), NaT]@Series[datetime64[ns]]    
                                                             |
+| timestamp[s]:empty            | []@timestamp[s]                              
                                | []@Series[datetime64[ns]]                     
                                                                                
 | []@Series[datetime64[ns]]                                                    
                                                  |
+| timestamp[ms]:standard        | [2024-01-01 12:00:00, 2024-06-15 
18:30:00]@timestamp[ms]                     | [Timestamp('2024-01-01 
12:00:00'), Timestamp('2024-06-15 18:30:00')]@Series[datetime64[ns]]            
                        | [Timestamp('2024-01-01 12:00:00'), 
Timestamp('2024-06-15 18:30:00')]@Series[datetime64[ns]]                        
            |
+| timestamp[ms]:nullable        | [2024-01-01 12:00:00, None]@timestamp[ms]    
                                | [Timestamp('2024-01-01 12:00:00'), 
NaT]@Series[datetime64[ns]]                                                     
            | [Timestamp('2024-01-01 12:00:00'), NaT]@Series[datetime64[ns]]    
                                                             |
+| timestamp[ms]:empty           | []@timestamp[ms]                             
                                | []@Series[datetime64[ns]]                     
                                                                                
 | []@Series[datetime64[ns]]                                                    
                                                  |
+| timestamp[us]:standard        | [2024-01-01 12:00:00, 2024-06-15 
18:30:00]@timestamp[us]                     | [Timestamp('2024-01-01 
12:00:00'), Timestamp('2024-06-15 18:30:00')]@Series[datetime64[ns]]            
                        | [Timestamp('2024-01-01 12:00:00'), 
Timestamp('2024-06-15 18:30:00')]@Series[datetime64[ns]]                        
            |
+| timestamp[us]:nullable        | [2024-01-01 12:00:00, None]@timestamp[us]    
                                | [Timestamp('2024-01-01 12:00:00'), 
NaT]@Series[datetime64[ns]]                                                     
            | [Timestamp('2024-01-01 12:00:00'), NaT]@Series[datetime64[ns]]    
                                                             |
+| timestamp[us]:empty           | []@timestamp[us]                             
                                | []@Series[datetime64[ns]]                     
                                                                                
 | []@Series[datetime64[ns]]                                                    
                                                  |
+| timestamp[ns]:standard        | [2024-01-01 12:00:00, 2024-06-15 
18:30:00]@timestamp[ns]                     | [Timestamp('2024-01-01 
12:00:00'), Timestamp('2024-06-15 18:30:00')]@Series[datetime64[ns]]            
                        | [Timestamp('2024-01-01 12:00:00'), 
Timestamp('2024-06-15 18:30:00')]@Series[datetime64[ns]]                        
            |
+| timestamp[ns]:nullable        | [2024-01-01 12:00:00, None]@timestamp[ns]    
                                | [Timestamp('2024-01-01 12:00:00'), 
NaT]@Series[datetime64[ns]]                                                     
            | [Timestamp('2024-01-01 12:00:00'), NaT]@Series[datetime64[ns]]    
                                                             |
+| timestamp[ns]:empty           | []@timestamp[ns]                             
                                | []@Series[datetime64[ns]]                     
                                                                                
 | []@Series[datetime64[ns]]                                                    
                                                  |
+| timestamp[us,tz=UTC]:standard | [2024-01-01 12:00:00+00:00, 2024-06-15 
18:30:00+00:00]@timestamp[us, tz=UTC] | [Timestamp('2024-01-01 12:00:00+0000', 
tz='UTC'), Timestamp('2024-06-15 18:30:00+0000', 
tz='UTC')]@Series[datetime64[ns, UTC]] | [Timestamp('2024-01-01 12:00:00+0000', 
tz='UTC'), Timestamp('2024-06-15 18:30:00+0000', 
tz='UTC')]@Series[datetime64[ns, UTC]] |
+| timestamp[us,tz=UTC]:nullable | [2024-01-01 12:00:00+00:00, 
None]@timestamp[us, tz=UTC]                      | [Timestamp('2024-01-01 
12:00:00+0000', tz='UTC'), NaT]@Series[datetime64[ns, UTC]]                     
                        | [Timestamp('2024-01-01 12:00:00+0000', tz='UTC'), 
NaT]@Series[datetime64[ns, UTC]]                                             |
+| timestamp[us,tz=UTC]:empty    | []@timestamp[us, tz=UTC]                     
                                | []@Series[datetime64[ns, UTC]]                
                                                                                
 | []@Series[datetime64[ns, UTC]]                                               
                                                  |
+| timestamp[s]:overflow         | [2500-01-01 00:00:00]@timestamp[s]           
                                | ERR@ArrowInvalid                              
                                                                                
 | ERR@ArrowInvalid                                                             
                                                  |
+| duration[s]:standard          | [1 day, 0:00:00, 2:30:00]@duration[s]        
                                | [Timedelta('1 days 00:00:00'), Timedelta('0 
days 02:30:00')]@Series[timedelta64[ns]]                                        
   | [Timedelta('1 days 00:00:00'), Timedelta('0 days 
02:30:00')]@Series[timedelta64[ns]]                                           |
+| duration[s]:nullable          | [1 day, 0:00:00, None]@duration[s]           
                                | [Timedelta('1 days 00:00:00'), 
NaT]@Series[timedelta64[ns]]                                                    
                | [Timedelta('1 days 00:00:00'), NaT]@Series[timedelta64[ns]]   
                                                                 |
+| duration[s]:empty             | []@duration[s]                               
                                | []@Series[timedelta64[ns]]                    
                                                                                
 | []@Series[timedelta64[ns]]                                                   
                                                  |
+| duration[ms]:standard         | [1 day, 0:00:00, 2:30:00]@duration[ms]       
                                | [Timedelta('1 days 00:00:00'), Timedelta('0 
days 02:30:00')]@Series[timedelta64[ns]]                                        
   | [Timedelta('1 days 00:00:00'), Timedelta('0 days 
02:30:00')]@Series[timedelta64[ns]]                                           |
+| duration[ms]:nullable         | [1 day, 0:00:00, None]@duration[ms]          
                                | [Timedelta('1 days 00:00:00'), 
NaT]@Series[timedelta64[ns]]                                                    
                | [Timedelta('1 days 00:00:00'), NaT]@Series[timedelta64[ns]]   
                                                                 |
+| duration[ms]:empty            | []@duration[ms]                              
                                | []@Series[timedelta64[ns]]                    
                                                                                
 | []@Series[timedelta64[ns]]                                                   
                                                  |
+| duration[us]:standard         | [1 day, 0:00:00, 2:30:00]@duration[us]       
                                | [Timedelta('1 days 00:00:00'), Timedelta('0 
days 02:30:00')]@Series[timedelta64[ns]]                                        
   | [Timedelta('1 days 00:00:00'), Timedelta('0 days 
02:30:00')]@Series[timedelta64[ns]]                                           |
+| duration[us]:nullable         | [1 day, 0:00:00, None]@duration[us]          
                                | [Timedelta('1 days 00:00:00'), 
NaT]@Series[timedelta64[ns]]                                                    
                | [Timedelta('1 days 00:00:00'), NaT]@Series[timedelta64[ns]]   
                                                                 |
+| duration[us]:empty            | []@duration[us]                              
                                | []@Series[timedelta64[ns]]                    
                                                                                
 | []@Series[timedelta64[ns]]                                                   
                                                  |
+| duration[ns]:standard         | [1 days 00:00:00, 0 days 
02:30:00]@duration[ns]                              | [Timedelta('1 days 
00:00:00'), Timedelta('0 days 02:30:00')]@Series[timedelta64[ns]]               
                            | [Timedelta('1 days 00:00:00'), Timedelta('0 days 
02:30:00')]@Series[timedelta64[ns]]                                           |
+| duration[ns]:nullable         | [1 days 00:00:00, None]@duration[ns]         
                                | [Timedelta('1 days 00:00:00'), 
NaT]@Series[timedelta64[ns]]                                                    
                | [Timedelta('1 days 00:00:00'), NaT]@Series[timedelta64[ns]]   
                                                                 |
+| duration[ns]:empty            | []@duration[ns]                              
                                | []@Series[timedelta64[ns]]                    
                                                                                
 | []@Series[timedelta64[ns]]                                                   
                                                  |
+| duration[s]:overflow          | [109500 days, 0:00:00]@duration[s]           
                                | [Timedelta('-104004 days 
+00:25:26.290448384')]@Series[timedelta64[ns]]                                  
                      | [Timedelta('-104004 days 
+00:25:26.290448384')]@Series[timedelta64[ns]]                                  
                      |
+| date32:standard               | [2024-01-01, 2024-06-15]@date32[day]         
                                | [datetime.date(2024, 1, 1), 
datetime.date(2024, 6, 15)]@Series[object]                                      
                   | [Timestamp('2024-01-01 00:00:00'), Timestamp('2024-06-15 
00:00:00')]@Series[datetime64[ns]]                                    |
+| date32:nullable               | [2024-01-01, None]@date32[day]               
                                | [datetime.date(2024, 1, 1), 
None]@Series[object]                                                            
                   | [Timestamp('2024-01-01 00:00:00'), 
NaT]@Series[datetime64[ns]]                                                     
            |
+| date32:empty                  | []@date32[day]                               
                                | []@Series[object]                             
                                                                                
 | []@Series[datetime64[ns]]                                                    
                                                  |
+| date64:standard               | [2024-01-01, 2024-06-15]@date64[ms]          
                                | [datetime.date(2024, 1, 1), 
datetime.date(2024, 6, 15)]@Series[object]                                      
                   | [Timestamp('2024-01-01 00:00:00'), Timestamp('2024-06-15 
00:00:00')]@Series[datetime64[ns]]                                    |
+| date64:nullable               | [2024-01-01, None]@date64[ms]                
                                | [datetime.date(2024, 1, 1), 
None]@Series[object]                                                            
                   | [Timestamp('2024-01-01 00:00:00'), 
NaT]@Series[datetime64[ns]]                                                     
            |
+| date64:empty                  | []@date64[ms]                                
                                | []@Series[object]                             
                                                                                
 | []@Series[datetime64[ns]]                                                    
                                                  |
+| time32[s]:standard            | [12:30:00, 18:45:30]@time32[s]               
                                | [datetime.time(12, 30), datetime.time(18, 45, 
30)]@Series[object]                                                             
 | [datetime.time(12, 30), datetime.time(18, 45, 30)]@Series[object]            
                                                  |
+| time32[s]:nullable            | [12:30:00, None]@time32[s]                   
                                | [datetime.time(12, 30), None]@Series[object]  
                                                                                
 | [datetime.time(12, 30), None]@Series[object]                                 
                                                  |
+| time32[s]:empty               | []@time32[s]                                 
                                | []@Series[object]                             
                                                                                
 | []@Series[object]                                                            
                                                  |
+| time32[ms]:standard           | [12:30:00, 18:45:30]@time32[ms]              
                                | [datetime.time(12, 30), datetime.time(18, 45, 
30)]@Series[object]                                                             
 | [datetime.time(12, 30), datetime.time(18, 45, 30)]@Series[object]            
                                                  |
+| time32[ms]:nullable           | [12:30:00, None]@time32[ms]                  
                                | [datetime.time(12, 30), None]@Series[object]  
                                                                                
 | [datetime.time(12, 30), None]@Series[object]                                 
                                                  |
+| time32[ms]:empty              | []@time32[ms]                                
                                | []@Series[object]                             
                                                                                
 | []@Series[object]                                                            
                                                  |
+| time64[us]:standard           | [12:30:00, 18:45:30]@time64[us]              
                                | [datetime.time(12, 30), datetime.time(18, 45, 
30)]@Series[object]                                                             
 | [datetime.time(12, 30), datetime.time(18, 45, 30)]@Series[object]            
                                                  |
+| time64[us]:nullable           | [12:30:00, None]@time64[us]                  
                                | [datetime.time(12, 30), None]@Series[object]  
                                                                                
 | [datetime.time(12, 30), None]@Series[object]                                 
                                                  |
+| time64[us]:empty              | []@time64[us]                                
                                | []@Series[object]                             
                                                                                
 | []@Series[object]                                                            
                                                  |
+| time64[ns]:standard           | [12:30:00, 18:45:30]@time64[ns]              
                                | [datetime.time(12, 30), datetime.time(18, 45, 
30)]@Series[object]                                                             
 | [datetime.time(12, 30), datetime.time(18, 45, 30)]@Series[object]            
                                                  |
+| time64[ns]:nullable           | [12:30:00, None]@time64[ns]                  
                                | [datetime.time(12, 30), None]@Series[object]  
                                                                                
 | [datetime.time(12, 30), None]@Series[object]                                 
                                                  |
+| time64[ns]:empty              | []@time64[ns]                                
                                | []@Series[object]                             
                                                                                
 | []@Series[object]                                                            
                                                  |
+| int64:standard                | [0, 1, -1]@int64                             
                                | [0, 1, -1]@Series[int64]                      
                                                                                
 | [0, 1, -1]@Series[int64]                                                     
                                                  |
+| int64:nullable                | [0, 1, None]@int64                           
                                | [0.0, 1.0, nan]@Series[float64]               
                                                                                
 | [0.0, 1.0, nan]@Series[float64]                                              
                                                  |
+| float64:standard              | [0.0, 1.5, -1.5]@float64                     
                                | [0.0, 1.5, -1.5]@Series[float64]              
                                                                                
 | [0.0, 1.5, -1.5]@Series[float64]                                             
                                                  |
+| string:standard               | [hello, world, ]@string                      
                                | ['hello', 'world', '']@Series[object]         
                                                                                
 | ['hello', 'world', '']@Series[object]                                        
                                                  |
\ No newline at end of file
diff --git 
a/python/pyspark/tests/upstream/pyarrow/test_pyarrow_arrow_to_pandas_non_default.py
 
b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_arrow_to_pandas_non_default.py
new file mode 100644
index 000000000000..6587f5deb4da
--- /dev/null
+++ 
b/python/pyspark/tests/upstream/pyarrow/test_pyarrow_arrow_to_pandas_non_default.py
@@ -0,0 +1,327 @@
+#
+# 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.to_pandas() with non-default arguments, using golden 
file
+comparison.
+
+PySpark relies on these arguments in production: when building pandas objects 
from
+Arrow data (see ``python/pyspark/sql/pandas/conversion.py``), it calls
+``to_pandas(coerce_temporal_nanoseconds=True)`` (and, when applicable,
+``date_as_object=True``) so that the Arrow path produces the same
+nanosecond-resolution ``datetime64[ns]`` / ``timedelta64[ns]`` values as the
+non-Arrow path.  These tests record how each Arrow type behaves under those
+arguments so CI fails loudly if the behavior drifts across pandas/PyArrow/NumPy
+upgrades.
+
+The shared ``_PyArrowToPandasTestBase`` holds the golden-file matrix driver and
+the per-cell conversion helper.  Each concrete test class supplies its own 
source
+arrays (the rows relevant to the argument under test) and one test method per
+argument combination (each producing its own golden file).  New ``to_pandas``
+arguments (e.g. ``zero_copy_only``) can be added as additional classes without
+touching the existing ones.
+
+## Golden File Cell Format
+
+Each cell uses the value@type format:
+- numpy ndarray: "python_list_repr@ndarray[dtype]"
+- pandas Series: "python_list_repr@Series[dtype]"
+- Error: "ERR@ExceptionClassName"
+
+Values are formatted via tolist() for stable, Python-native representation.
+
+## 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_arrow_to_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 to_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 datetime
+import inspect
+import os
+import unittest
+from typing import Callable, List, Optional
+
+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,
+)
+from pyspark.testing.goldenutils import GoldenFileTestMixin
+
+if have_pandas:
+    import pandas as pd
+if have_pyarrow:
+    import pyarrow as pa
+
+
+class _PyArrowToPandasTestBase(GoldenFileTestMixin, unittest.TestCase):
+    """
+    Shared machinery for pa.Array.to_pandas() golden file tests.
+
+    Concrete subclasses provide their own ``_build_source_arrays`` (the rows) 
and
+    one or more ``test_*`` methods that call 
``compare_or_generate_golden_matrix``.
+    This base defines no ``test_*`` methods, so it contributes no tests itself.
+    """
+
+    def compare_or_generate_golden_matrix(
+        self,
+        row_names: List[str],
+        col_names: List[str],
+        compute_cell: Callable[[str, str], str],
+        golden_file_prefix: str,
+        index_name: str = "source \\ target",
+        overrides: Optional[dict[tuple[str, str], str]] = None,
+    ) -> None:
+        """
+        Run a matrix of computations and compare against (or generate) a 
golden file.
+
+        1. If SPARK_GENERATE_GOLDEN_FILES=1, compute every cell, build a
+           DataFrame, and save it as the new golden CSV / Markdown file.
+        2. Otherwise, load the existing golden file and assert that every cell
+           matches the freshly computed value.
+        """
+        generating = self.is_generating_golden()
+
+        test_dir = os.path.dirname(inspect.getfile(type(self)))
+        golden_csv = os.path.join(test_dir, f"{golden_file_prefix}.csv")
+        golden_md = os.path.join(test_dir, f"{golden_file_prefix}.md")
+
+        golden = None
+        if not generating:
+            golden = self.load_golden_csv(golden_csv)
+
+        errors = []
+        results = {}
+
+        for row_name in row_names:
+            for col_name in col_names:
+                result = compute_cell(row_name, col_name)
+                results[(row_name, col_name)] = result
+
+                if not generating:
+                    if overrides and (row_name, col_name) in overrides:
+                        expected = overrides[(row_name, col_name)]
+                    else:
+                        expected = golden.loc[row_name, col_name]
+                    if expected != result:
+                        errors.append(
+                            f"{row_name} -> {col_name}: expected '{expected}', 
got '{result}'"
+                        )
+
+        if generating:
+            import pandas as pd
+
+            index = pd.Index(row_names, name=index_name)
+            df = pd.DataFrame(index=index)
+            for col_name in col_names:
+                df[col_name] = [results[(row, col_name)] for row in row_names]
+            self.save_golden(df, golden_csv, golden_md)
+        else:
+            self.assertEqual(
+                len(errors),
+                0,
+                f"\n{len(errors)} golden file mismatches:\n" + 
"\n".join(errors),
+            )
+
+    def _to_pandas_cell(self, arr, **to_pandas_kwargs) -> str:
+        """
+        Convert ``arr`` via ``to_pandas(**to_pandas_kwargs)`` and format the
+        result as a golden-file cell, returning ``ERR@<ExceptionClass>`` if the
+        conversion raises.
+        """
+        try:
+            return self.repr_value(arr.to_pandas(**to_pandas_kwargs), 
max_len=0)
+        except Exception as e:
+            return f"ERR@{type(e).__name__}"
+
+
[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 PyArrowArrayToPandasCoerceTemporalTests(_PyArrowToPandasTestBase):
+    """
+    Tests pa.Array.to_pandas(coerce_temporal_nanoseconds=True) via golden file 
comparison.
+
+    Covers the temporal Arrow types the argument affects (timestamp and 
duration in
+    units s/ms/us/ns, tz-aware timestamp, date, and time), plus overflow cases 
and
+    a few non-temporal control rows.  Each type is tested without nulls, with 
a null,
+    and empty.
+
+    The date rows are recorded under two output columns: the default
+    ``date_as_object=True`` (object-dtype ``datetime.date``, which coercion 
does not
+    affect) and ``date_as_object=False`` (the numeric ``datetime64[ns]`` path 
where
+    coercion takes effect).
+    """
+
+    def _build_source_arrays(self):
+        """Build an ordered dict of named source PyArrow arrays for testing."""
+        sources = {}
+
+        # =====================================================================
+        # Timestamp types (the primary target of coerce_temporal_nanoseconds)
+        # =====================================================================
+        dt1 = datetime.datetime(2024, 1, 1, 12, 0, 0)
+        dt2 = datetime.datetime(2024, 6, 15, 18, 30, 0)
+        for unit in ["s", "ms", "us", "ns"]:
+            sources[f"timestamp[{unit}]:standard"] = pa.array([dt1, dt2], 
pa.timestamp(unit))
+            sources[f"timestamp[{unit}]:nullable"] = pa.array([dt1, None], 
pa.timestamp(unit))
+            sources[f"timestamp[{unit}]:empty"] = pa.array([], 
pa.timestamp(unit))
+        # Timestamp with timezone
+        sources["timestamp[us,tz=UTC]:standard"] = pa.array(
+            [dt1, dt2], pa.timestamp("us", tz="UTC")
+        )
+        sources["timestamp[us,tz=UTC]:nullable"] = pa.array(
+            [dt1, None], pa.timestamp("us", tz="UTC")
+        )
+        sources["timestamp[us,tz=UTC]:empty"] = pa.array([], 
pa.timestamp("us", tz="UTC"))
+        # Overflow: coercion to nanoseconds has a valid range (~1677-2262); a
+        # far-future second-resolution timestamp cannot fit and should error.
+        sources["timestamp[s]:overflow"] = pa.array(
+            [datetime.datetime(2500, 1, 1)], pa.timestamp("s")
+        )
+
+        # =====================================================================
+        # Duration types (also coerced to nanoseconds)
+        # =====================================================================
+        td1 = datetime.timedelta(days=1)
+        td2 = datetime.timedelta(hours=2, minutes=30)
+        for unit in ["s", "ms", "us", "ns"]:
+            sources[f"duration[{unit}]:standard"] = pa.array([td1, td2], 
pa.duration(unit))
+            sources[f"duration[{unit}]:nullable"] = pa.array([td1, None], 
pa.duration(unit))
+            sources[f"duration[{unit}]:empty"] = pa.array([], 
pa.duration(unit))
+        # Overflow: a duration beyond ~292 years exceeds the int64 nanosecond
+        # range. Unlike timestamp overflow (which raises), coercing it silently
+        # wraps around to a bogus value; this row pins that behavior.
+        sources["duration[s]:overflow"] = pa.array(
+            [datetime.timedelta(days=300 * 365)], pa.duration("s")
+        )
+
+        # =====================================================================
+        # Date types. With the default date_as_object=True, pandas yields an
+        # object-dtype Series of datetime.date, so coerce_temporal_nanoseconds
+        # has nothing to coerce; the "date_as_object=False" column forces the
+        # numeric datetime64[ns] path where the argument actually takes effect.
+        # =====================================================================
+        d1 = datetime.date(2024, 1, 1)
+        d2 = datetime.date(2024, 6, 15)
+        sources["date32:standard"] = pa.array([d1, d2], pa.date32())
+        sources["date32:nullable"] = pa.array([d1, None], pa.date32())
+        sources["date32:empty"] = pa.array([], pa.date32())
+        sources["date64:standard"] = pa.array([d1, d2], pa.date64())
+        sources["date64:nullable"] = pa.array([d1, None], pa.date64())
+        sources["date64:empty"] = pa.array([], pa.date64())
+
+        # =====================================================================
+        # Time types (control: pandas yields object-dtype datetime.time, no
+        # native time-of-day dtype, so coerce_temporal_nanoseconds is a no-op)
+        # =====================================================================
+        t1 = datetime.time(12, 30, 0)
+        t2 = datetime.time(18, 45, 30)
+        sources["time32[s]:standard"] = pa.array([t1, t2], pa.time32("s"))
+        sources["time32[s]:nullable"] = pa.array([t1, None], pa.time32("s"))
+        sources["time32[s]:empty"] = pa.array([], pa.time32("s"))
+        sources["time32[ms]:standard"] = pa.array([t1, t2], pa.time32("ms"))
+        sources["time32[ms]:nullable"] = pa.array([t1, None], pa.time32("ms"))
+        sources["time32[ms]:empty"] = pa.array([], pa.time32("ms"))
+        sources["time64[us]:standard"] = pa.array([t1, t2], pa.time64("us"))
+        sources["time64[us]:nullable"] = pa.array([t1, None], pa.time64("us"))
+        sources["time64[us]:empty"] = pa.array([], pa.time64("us"))
+        sources["time64[ns]:standard"] = pa.array([t1, t2], pa.time64("ns"))
+        sources["time64[ns]:nullable"] = pa.array([t1, None], pa.time64("ns"))
+        sources["time64[ns]:empty"] = pa.array([], pa.time64("ns"))
+
+        # =====================================================================
+        # Non-temporal controls (unaffected by coerce_temporal_nanoseconds)
+        # =====================================================================
+        sources["int64:standard"] = pa.array([0, 1, -1], pa.int64())
+        sources["int64:nullable"] = pa.array([0, 1, None], pa.int64())
+        sources["float64:standard"] = pa.array([0.0, 1.5, -1.5], pa.float64())
+        sources["string:standard"] = pa.array(["hello", "world", ""], 
pa.string())
+
+        return sources
+
+    # Output column recording to_pandas(coerce_temporal_nanoseconds=True).
+    # date_as_object stays at its default (True).
+    COL_PANDAS = "pandas series"
+
+    # Output column recording to_pandas(coerce_temporal_nanoseconds=True,
+    # date_as_object=False). date_as_object=False is the only path on which
+    # coercion observably affects date types.
+    COL_PANDAS_DATE_AS_OBJECT_FALSE = "pandas series (date_as_object=False)"
+
+    def test_to_pandas_coerce_temporal_nanoseconds(self):
+        """Test pa.Array.to_pandas(coerce_temporal_nanoseconds=True) against 
golden file."""
+        sources = self._build_source_arrays()
+        row_names = list(sources.keys())
+        col_names = [
+            "pyarrow array",
+            self.COL_PANDAS,
+            self.COL_PANDAS_DATE_AS_OBJECT_FALSE,
+        ]
+
+        # Version-specific expected values go here, keyed by (row, col), when a
+        # newer pandas/PyArrow/NumPy legitimately changes a cell's output.
+        # Add a LooseVersion-guarded block for each known drift.
+        overrides: dict[tuple[str, str], str] = {}
+        # Pandas 3 uses its dedicated string dtype for non-empty Arrow string 
arrays.
+        if LooseVersion(pd.__version__) >= LooseVersion("3.0.0"):
+            str_expected = "['hello', 'world', '']@Series[str]"
+            overrides[("string:standard", self.COL_PANDAS)] = str_expected
+            overrides[("string:standard", 
self.COL_PANDAS_DATE_AS_OBJECT_FALSE)] = str_expected
+
+        def compute_cell(row_name, col_name):
+            arr = sources[row_name]
+            if col_name == "pyarrow array":
+                return self.repr_value(arr, max_len=0)
+            elif col_name == self.COL_PANDAS:
+                return self._to_pandas_cell(arr, 
coerce_temporal_nanoseconds=True)
+            elif col_name == self.COL_PANDAS_DATE_AS_OBJECT_FALSE:
+                return self._to_pandas_cell(
+                    arr, coerce_temporal_nanoseconds=True, date_as_object=False
+                )
+            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_arrow_to_pandas_coerce_temporal",
+            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