Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
5696d6d
Nest a struct column's fields under the column's own name
nelson2005 Sep 11, 2026
784d0f0
Name the type of a unicode or bytes output column
nelson2005 Sep 11, 2026
6ba4c94
Build each output column with its declared type and name it on failure
nelson2005 Sep 11, 2026
7d607f4
Give each viewer its own cache files, and refuse a non-boolean cache …
nelson2005 Sep 11, 2026
9c165be
Make the read-only view impossible to flip back
nelson2005 Sep 11, 2026
f1f5a1b
Say which column, field and element an error is about, and cut wide t…
nelson2005 Sep 11, 2026
1df2bdd
Say what the docs got wrong about binding, bitmaps, zones, widths and…
nelson2005 Sep 11, 2026
5c3dca9
Declare the pyspark and pandas floors that actually run
nelson2005 Sep 11, 2026
5d5ee36
Admit pyarrow 25
nelson2005 Sep 11, 2026
0282074
Make the suite validate the tree under test and fail when its Spark l…
nelson2005 Sep 11, 2026
9cb1ece
Check struct keys at every depth, and refuse a ready-built array whos…
nelson2005 Sep 11, 2026
98bc4b8
Print a KeyError's message as written, and refuse a batch that names …
nelson2005 Sep 11, 2026
403e06f
Say only what holds about flipping a copy, DISABLE_JIT and a negative…
nelson2005 Sep 11, 2026
d8d3821
Check a map's struct keys too, leave a malformed pair to pa.array, an…
nelson2005 Sep 12, 2026
c89b526
Count what the cut-type message leaves out, not the type's full length
nelson2005 Sep 13, 2026
91dba71
Carry nulls out of a UDF through a (data, bitmap) pair
nelson2005 Sep 13, 2026
0dc5190
Demote arrays_viewers to the three viewers the adapters use
nelson2005 Sep 13, 2026
e6b2e75
Name the nullable output column Nullable, and refuse a passed-through…
nelson2005 Sep 14, 2026
d5cd821
Check a handed-out bitmap against the count it covers, not the batch'…
nelson2005 Sep 19, 2026
5d91704
Pass over a row the key check cannot look inside, as pa.array does
nelson2005 Sep 19, 2026
6083df9
Refuse a bitmap that is not an ndarray before reading its dtype
nelson2005 Sep 19, 2026
bfb4c56
Say which output shapes the declared type's refusals hold for
nelson2005 Sep 19, 2026
ef8e332
Say what Spark does with a column of another accessor family
nelson2005 Sep 19, 2026
b90c313
Refuse an output_schema that is not a pyarrow Schema at factory time
nelson2005 Sep 20, 2026
7c3c440
Pin the masking a Nullable dictionary column takes
nelson2005 Sep 20, 2026
0123b7d
Compile is_null_struct with one signature, and one cache entry
nelson2005 Sep 20, 2026
f683c81
Say that a day-unit datetime64 output comes back date32
nelson2005 Sep 20, 2026
713f883
Widen a day-unit datetime64 before building its declared timestamp
nelson2005 Sep 20, 2026
1e8f18f
Pass over str, bytes and numeric ndarray rows in the key check
nelson2005 Sep 21, 2026
32168c0
Say that both extras declare the pandas floor the README table gives
nelson2005 Sep 21, 2026
b2ec9c5
Refuse a string for every JIT option numba would read as true
nelson2005 Sep 21, 2026
7200bb5
Pin that a generator output keeps its rows past the key check
nelson2005 Sep 21, 2026
6d254f8
Keep the rows of a record array with no fields
nelson2005 Sep 21, 2026
26f9a54
Build a viewer the first time its dtype is asked for, and keep it
nelson2005 Sep 21, 2026
4669531
Pin every branch of the ready-built array depth check
nelson2005 Sep 21, 2026
db553d9
Infer a day-unit datetime64 first under any declared type
nelson2005 Sep 21, 2026
2dc62e7
Refuse an output key that is not a str by name
nelson2005 Sep 21, 2026
20f4b63
Look through a dictionary in the ready-built array depth check
nelson2005 Sep 21, 2026
0a5b3f6
Say that a tuple and an object array holding None carry a null out too
nelson2005 Sep 21, 2026
7405d11
Pin five guards the suite passed without
nelson2005 Sep 21, 2026
569e91d
Say what a bare array does to a row that came in null
nelson2005 Sep 24, 2026
2a7d362
Say when the input's bitmap belongs on a result, and how to build one
nelson2005 Sep 24, 2026
09d7449
Reword the zero-copy sentence in _with_validity
nelson2005 Sep 24, 2026
b53a383
Keep a handed-out bitmap alive while the UDF runs
nelson2005 Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
344 changes: 331 additions & 13 deletions .github/scripts/mutation_guard_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,173 @@
(
"output path stops routing unicode via tolist()",
"numbarrow/core/mapinarrow_factory.py",
'if getattr(output, "dtype", None) is not None and output.dtype.kind == "U":',
"if False:",
' if kind == "U":',
" if False:",
),
(
"output path stops routing bytes via tolist()",
"numbarrow/core/mapinarrow_factory.py",
' if kind == "S":',
" if False:",
),
(
"unicode output stops naming its type",
"numbarrow/core/mapinarrow_factory.py",
" return pa.array(value.tolist(), type=arrow_type or pa.string())",
" return pa.array(value.tolist(), type=arrow_type)",
),
(
"a dict under one output key stops being refused",
"numbarrow/core/mapinarrow_factory.py",
" if isinstance(value, Mapping):",
" if False:",
),
(
"a struct dict key no field has stops being refused",
"numbarrow/core/mapinarrow_factory.py",
" if unexpected_keys:\n raise ValueError(\n"
" f\"declared {type_repr(arrow_type)} but the dicts",
" if False:\n raise ValueError(\n"
" f\"declared {type_repr(arrow_type)} but the dicts",
),
(
"a struct key inside a struct field stops being checked",
"numbarrow/core/mapinarrow_factory.py",
" if _carries_keys(child_type):",
" if False:",
),
(
"a struct key inside a list stops being checked",
"numbarrow/core/mapinarrow_factory.py",
" elif _is_list_like(arrow_type):\n _check_keys(",
" elif False:\n _check_keys(",
),
(
"the key check stops passing over a row it cannot look inside",
"numbarrow/core/mapinarrow_factory.py",
' if hasattr(row, "__iter__")\n',
" if True\n",
),
(
"the key check spreads a str or bytes row again",
"numbarrow/core/mapinarrow_factory.py",
" and not isinstance(row, (str, bytes))\n",
"",
),
(
"the key check spreads a numeric ndarray row again",
"numbarrow/core/mapinarrow_factory.py",
' and not (isinstance(row, np.ndarray) and row.dtype.kind != "O")\n',
"",
),
(
"a struct key inside a map's keys stops being checked",
"numbarrow/core/mapinarrow_factory.py",
" if _carries_keys(arrow_type.key_type):",
" if False:",
),
(
"a ready-built array whose fields differ stops being refused",
"numbarrow/core/mapinarrow_factory.py",
" if unexpected:",
" if False:",
),
(
"output columns of different lengths stop being refused",
"numbarrow/core/mapinarrow_factory.py",
" if len(set(lengths.values())) > 1:",
" if False:",
),
(
"a ChunkedArray stops being named as such",
"numbarrow/core/adapters.py",
" if isinstance(pa_array, pa.ChunkedArray):",
" if False:",
),
(
"a struct child's failure stops naming the field",
"numbarrow/utils/arrow_array_utils.py",
' raise renamed(exc, f"struct field {field_name!r}") from exc',
" raise",
),
(
"invalid UTF-8 stops naming the element",
"numbarrow/utils/arrow_array_utils.py",
" except UnicodeDecodeError as exc:",
" except ():",
),
(
"a string input_columns stops being refused",
"numbarrow/core/mapinarrow_factory.py",
" if isinstance(input_columns, str):",
" if False:",
),
(
"an output_schema that is not a pyarrow Schema stops being refused",
"numbarrow/core/mapinarrow_factory.py",
" if output_schema is not None and not isinstance(output_schema, pa.Schema):",
" if False:",
),
(
"a missing input column stops naming the batch's columns",
"numbarrow/core/mapinarrow_factory.py",
" if col not in names:",
" if False:",
),
(
"a column the batch carries twice stops being refused",
"numbarrow/core/mapinarrow_factory.py",
" if names.count(col) > 1:",
" if False:",
),
(
"an adapter failure stops naming the column",
"numbarrow/core/mapinarrow_factory.py",
' raise renamed(exc, f"column {col!r}") from exc',
" raise",
),
(
"a record array stops becoming a struct",
"numbarrow/core/mapinarrow_factory.py",
" if value.dtype.names is not None:",
" if False:",
),
(
"a non-dict UDF result stops being refused",
"numbarrow/core/mapinarrow_factory.py",
" if not isinstance(outputs, Mapping):",
" if False:",
),
(
"an output key the schema does not name stops being refused",
"numbarrow/core/mapinarrow_factory.py",
" if extra:",
" if False:",
),
(
"a non-boolean cache option stops being refused",
"numbarrow/core/configurations.py",
' if "cache" in as_json and not isinstance(as_json["cache"], bool):',
" if False:",
),
(
"is_null_struct stops being compiled with one signature",
"numbarrow/core/is_null.py",
'@njit(boolean(int64, Optional(Array(uint8, 1, "C", readonly=True)),\n'
' Optional(Array(uint8, 1, "C", readonly=True))), **jit_options)',
"@njit(**jit_options)",
),
(
"viewers stop getting a cache name of their own",
"numbarrow/utils/utils.py",
' viewer.__qualname__ = f"{numpy_array_from_ptr_factory.__qualname__}.<locals>.{name}"',
" pass",
),
(
"uniform view stops being read-only at the buffer",
"numbarrow/utils/arrow_array_utils.py",
" memoryview(data_buf).toreadonly(),",
" memoryview(data_buf),",
),
(
"empty string result stops being read-only",
Expand All @@ -88,14 +253,161 @@
" pass",
),
(
"key collision stops raising",
"a Nullable stops being split into data and bitmap",
"numbarrow/core/mapinarrow_factory.py",
" if isinstance(value, Nullable):",
" if False:",
),
(
"a Nullable's bitmap stops being folded in",
"numbarrow/core/mapinarrow_factory.py",
" return _with_validity(array, bitmap)",
" return array",
),
(
"a handed-out bitmap stops being checked against the rows it covers",
"numbarrow/core/mapinarrow_factory.py",
" if covers is not None and len(array) != covers:",
" if False:",
),
(
"a handed-out bitmap's count stops coming from the data beside it",
"numbarrow/core/mapinarrow_factory.py",
" handed[id(bitmap)] = (bitmap, len(datas if field is None else datas[field]))",
" handed[id(bitmap)] = (bitmap, len(datas))",
),
(
"a handed-out bitmap stops being kept alive for the batch",
"numbarrow/core/mapinarrow_factory.py",
" handed[id(bitmap)] = (bitmap, len(datas if field is None else datas[field]))",
" handed[id(bitmap)] = (None, len(datas if field is None else datas[field]))",
),
(
"a Nullable's bitmap of the wrong length stops being refused",
"numbarrow/core/mapinarrow_factory.py",
" if len(bitmap) != (rows + 7) // 8:",
" if False:",
),
(
"a Nullable's bitmap that is not packed uint8 stops being refused",
"numbarrow/core/mapinarrow_factory.py",
" if bitmap.dtype != np.uint8 or bitmap.ndim != 1:",
" if False:",
),
(
"a Nullable's bitmap that is not an ndarray stops being refused",
"numbarrow/core/mapinarrow_factory.py",
" if not isinstance(bitmap, np.ndarray):",
" if False:",
),
(
"a dictionary column stops being kept off the flat path",
"numbarrow/core/mapinarrow_factory.py",
" if owner is not None:",
"and not pa.types.is_dictionary(array.type)",
"and True",
),
(
"a string for a JIT option other than cache stops being refused",
"numbarrow/core/configurations.py",
' if isinstance(value, str) and name not in ("error_model", "inline"):',
" if False:",
),
(
"a generator output stops being read into a list before the key check",
"numbarrow/core/mapinarrow_factory.py",
' if not hasattr(value, "__len__"):',
" if False:",
),
(
"a record array with no fields stops keeping its rows",
"numbarrow/core/mapinarrow_factory.py",
" if not children:",
" if False:",
),
(
"a viewer built on request stops being kept",
"numbarrow/utils/utils.py",
" viewer = self[dtype_] = numpy_array_from_ptr_factory(dtype_)",
" viewer = numpy_array_from_ptr_factory(dtype_)",
),
(
'a struct field inside a struct field stops being checked in a ready-built array',
'numbarrow/core/mapinarrow_factory.py',
' found.extend(_unexpected_fields(field.type, declared[field.name]))',
' pass',
),
(
'a struct field inside a list stops being checked in a ready-built array',
'numbarrow/core/mapinarrow_factory.py',
" if _is_list_like(source_type) and _is_list_like(declared_type):\n"
" return _unexpected_fields(source_type.value_type, declared_type.value_type)",
" if _is_list_like(source_type) and _is_list_like(declared_type):\n"
" return []",
),
(
'a struct field inside a map stops being checked in a ready-built array',
'numbarrow/core/mapinarrow_factory.py',
" return (_unexpected_fields(source_type.key_type, declared_type.key_type)\n"
" + _unexpected_fields(source_type.item_type, declared_type.item_type))",
' return []',
),
(
'a day-unit datetime64 under a declared type stops being inferred first',
'numbarrow/core/mapinarrow_factory.py',
' if value.dtype == np.dtype("datetime64[D]") and arrow_type is not None:',
' if False:',
),
(
'an output key that is not a str stops being refused by name',
'numbarrow/core/mapinarrow_factory.py',
' if not isinstance(name, str):',
' if False:',
),
(
'a struct field inside a dictionary stops being checked in a ready-built array',
'numbarrow/core/mapinarrow_factory.py',
' if pa.types.is_dictionary(source_type) and pa.types.is_dictionary(declared_type):',
' if False:',
),
(
'a record array under a non-struct declared type stops being refused',
'numbarrow/core/mapinarrow_factory.py',
" if not pa.types.is_struct(arrow_type):\n"
" raise TypeError",
" if False:\n"
" raise TypeError",
),
(
'a ChunkedArray output stops being combined',
'numbarrow/core/mapinarrow_factory.py',
" if isinstance(value, pa.ChunkedArray):\n"
" value = value.combine_chunks()",
" if False:\n"
" value = value.combine_chunks()",
),
(
'renamed() stops falling back to ValueError',
'numbarrow/utils/arrow_array_utils.py',
" if not (isinstance(exc, pa.ArrowException) or cls in kept):\n"
" cls = ValueError",
" if False:\n"
" cls = ValueError",
),
(
"map entries stop checking a pair's shape",
'numbarrow/core/mapinarrow_factory.py',
' if isinstance(pair, (tuple, list)) and len(pair) == 2:',
' if True:',
),
(
'a record array field failure stops naming the field',
'numbarrow/core/mapinarrow_factory.py',
' raise renamed(exc, f"field {field.name!r}") from exc',
' raise',
),
]

COPY = ["numbarrow", "test", "README.md", "docs"]
COPY = ["numbarrow", "test", "README.md", "pyproject.toml", "docs"]


def build_tree(repo: Path, dest: Path):
Expand All @@ -110,10 +422,13 @@ def build_tree(repo: Path, dest: Path):
shutil.copy2(src, dest / name)


def run_suite(tree: Path, neutral_cwd: Path, cache_dir: Path) -> bool:
"""True when the suite passes. Run from a cwd outside the tree, or the
real installed package lands on sys.path[0] and shadows this copy, which
is how a mutation can appear to survive when it was never even loaded."""
def run_suite(tree: Path, neutral_cwd: Path, cache_dir: Path) -> tuple[bool, str]:
"""Whether the suite passes, and the tail of what it printed.

Run from a cwd outside the tree, or the real installed package lands on
sys.path[0] and shadows this copy, which is how a mutation can appear to
survive when it was never even loaded. The tail is what a red job has to
show: without it a failing baseline named nothing."""
env = dict(os.environ)
env["PYTHONPATH"] = str(tree)
# Own cache dir, so this never disturbs a numba cache shared with other work.
Expand All @@ -122,7 +437,8 @@ def run_suite(tree: Path, neutral_cwd: Path, cache_dir: Path) -> bool:
[sys.executable, "-m", "pytest", str(tree / "test"), "-x", "-q",
"-p", "no:cacheprovider"],
cwd=str(neutral_cwd), env=env, capture_output=True, text=True)
return proc.returncode == 0
tail = "\n".join((proc.stdout + proc.stderr).splitlines()[-25:])
return proc.returncode == 0, tail


def main(argv=None):
Expand All @@ -142,10 +458,12 @@ def main(argv=None):
baseline.mkdir()
build_tree(repo, baseline)
print("baseline: ", end="", flush=True)
if not run_suite(baseline, neutral, cache):
passes, tail = run_suite(baseline, neutral, cache)
if not passes:
print("FAILS")
print("The unmutated suite does not pass, so mutation results would be "
"meaningless. Fix the suite first.")
"meaningless. Fix the suite first. The suite's last lines:")
print(tail)
return 1
print("passes")

Expand All @@ -163,7 +481,7 @@ def main(argv=None):
print(f" [{i + 1}/{len(MUTATIONS)}] STALE {label}")
continue
target.write_text(text.replace(old, new))
survived = run_suite(tree, neutral, cache)
survived, _tail = run_suite(tree, neutral, cache)
if survived:
failures.append(
f"SURVIVED {label}\n"
Expand Down
Loading
Loading