Skip to content

[SPARK-58657][PYTHON][TESTS] Add tests for pa.Array.from_pandas with the mask argument - #57862

Open
Spenserrrr wants to merge 3 commits into
apache:masterfrom
Spenserrrr:from-pandas-mask-tests
Open

[SPARK-58657][PYTHON][TESTS] Add tests for pa.Array.from_pandas with the mask argument#57862
Spenserrrr wants to merge 3 commits into
apache:masterfrom
Spenserrrr:from-pandas-mask-tests

Conversation

@Spenserrrr

Copy link
Copy Markdown
Contributor

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) 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):

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 ERR@ValueError.

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 in CI instead of silently changing conversion results.

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 the dedicated str type on pandas 3 and therefore moves from a mask-accepting row to a protocol row).

Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Anthropic), model Claude Opus 4.8

@Spenserrrr
Spenserrrr marked this pull request as ready for review August 7, 2026 21:05
@Spenserrrr

Copy link
Copy Markdown
Contributor Author

Hi @Yicong-Huang @zhengruifeng! This is a PR adding from_pandas monitoring for upstream behavior monitoring. Could you take a look when you have a moment? Thank you!

if __name__ == "__main__":
from pyspark.testing import main

main()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Spenserrrr Please rebase in accordance with #57864.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your reminder! I just rebased it.

Move the source-Series inventory (the three group methods and
_build_source_arrays) and repr_from_pandas_result into a test-free base
class, and make PyArrowArrayFromPandasDefaultTests a thin subclass that
holds only the skip guard and test_from_pandas_default. The upcoming
non-default tests (mask, type, nested) can then subclass the base to
reuse the rows directly instead of borrowing them via an unbound call.

Behavior-preserving: same rows and computation, the golden files
regenerate byte-identically, and only one test is still collected.

Co-authored-by: Isaac
…the mask argument

Add a golden-file test pinning pa.Array.from_pandas() under the mask
argument, in a new test_pyarrow_array_from_pandas_non_default.py that
subclasses _PyArrowFromPandasTestBase to reuse the default test's source
Series inventory.

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()

The golden records both mask=None and mask=isnull() so the two regimes stay
observable: numpy-backed dtypes accept the mask and agree with the nulls
from_pandas infers at mask=None, while protocol dtypes (implementing
__arrow_array__) reject any mask with ValueError, which is why PySpark
passes mask=None for them.

Also move the shared _from_pandas_cell helper onto _PyArrowFromPandasTestBase
so the default and non-default tests share one cell formatter.

Validated across pyarrow 18-25 x pandas 2 and 3 (16/16).

Co-authored-by: Isaac
…st_pyarrow_array_from_pandas_non_default

### What changes were proposed in this pull request?

Change the `__main__` entry point of
`test_pyarrow_array_from_pandas_non_default.py` from
`main(globals()["__file__"])` to a bare `main()`.

### Why are the changes needed?

`pyspark.testing.main(module=None)` forwards its argument to
`unittest.main(module=...)`, which treats it as a module name. Passing
`globals()["__file__"]` hands it a filesystem path, which fails to import:

```
ModuleNotFoundError: No module named '/__w/spark/spark/python/pyspark/tests/upstream/pyarrow/test_pyarrow_array_from_pandas_non_default'
```

This aborts the pyspark-sql test module (exit code 19). A bare `main()`
resolves the real module name via `__spec__`, matching every other pyspark
test file.

### Does this PR introduce _any_ user-facing change?

No. Test-only fix.

### How was this patch tested?

Ran the module locally; before, it raised `ModuleNotFoundError`; after, it runs
to completion (`Ran 1 test ... OK`). `dev/lint-python --ruff` clean.

### Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Opus 4.8)
@Spenserrrr
Spenserrrr force-pushed the from-pandas-mask-tests branch from 60360eb to 243503f Compare August 8, 2026 19:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants