You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by ti...@apache.org on 2026/10/08 15:43:12 UTC
(datafusion-python) 01/04: refactor: let _commit_extensions trust its caller's filter
This is an automated email from the ASF dual-hosted git repository.
timsaucer pushed a commit to branch feat/bundle-functions
in repository https://gitbox.apache.org/repos/asf/datafusion-python.git
commit 4f3089738a6bee357513a87f6a41745f5096bbb3
Author: Tim Saucer <ti...@gmail.com>
AuthorDate: Thu Oct 8 09:01:23 2026 -0400
refactor: let _commit_extensions trust its caller's filter
The Rust hook loop re-checked each entry with hasattr, which the Python
caller had already narrowed with isinstance. The two disagree on an
attribute set to None, so keeping both left a second, weaker predicate
that only stayed harmless because the stronger one ran first. Rust now
calls every entry it is handed, and the isinstance filter is the only
check.
Co-Authored-By: Claude Opus 5.5 (1M context) <no...@anthropic.com>
---
crates/core/src/context.rs | 14 ++++++++------
python/tests/test_context.py | 7 +++----
2 files changed, 11 insertions(+), 10 deletions(-)
diff --git a/crates/core/src/context.rs b/crates/core/src/context.rs
index 63b7a6c8..af627d5f 100644
--- a/crates/core/src/context.rs
+++ b/crates/core/src/context.rs
@@ -1720,9 +1720,14 @@ impl PySessionContext {
///
/// The second phase, run on the handle carrying the completed chains —
/// `session` is that same handle as the Python-level wrapper, which is
- /// what each `__datafusion_session_planner__` hook receives. The hooks
- /// run first, **in argument order**, each handed the planner built so
- /// far as a capsule; a hook may hand back an object exposing
+ /// what each `__datafusion_session_planner__` hook receives.
+ ///
+ /// `extensions` must already be narrowed to the planner hooks: every entry
+ /// is called, with no check of its own. The Python caller decides which
+ /// arguments are hooks.
+ ///
+ /// The hooks run first, **in argument order**, each handed the planner
+ /// built so far as a capsule; a hook may hand back an object exposing
/// `__datafusion_query_planner__` or a raw capsule, and each return is
/// imported here so a malformed planner surfaces at the hook that
/// produced it rather than at the install. Returning `None` contributes
@@ -1757,9 +1762,6 @@ impl PySessionContext {
// place rather than wrapping the session's default in an FFI hop.
let mut planner: Option<FFI_QueryPlanner> = None;
for extension in &extensions {
- if !extension.hasattr("__datafusion_session_planner__")? {
- continue;
- }
let fallback = match &planner {
Some(ffi) => create_query_planner_capsule(py, ffi)?,
None => slf.borrow().__datafusion_query_planner__(py, None)?,
diff --git a/python/tests/test_context.py b/python/tests/test_context.py
index f9575785..580ccea6 100644
--- a/python/tests/test_context.py
+++ b/python/tests/test_context.py
@@ -1216,10 +1216,9 @@ def test_with_extensions_ignores_a_planner_attribute_set_to_none(ctx):
with the other called the ``None``: ``'NoneType' object is not callable``,
naming neither the extension nor the hook.
- ``_commit_extensions`` still carries a Rust-side ``hasattr``, and the
- Python caller now hands it a list already narrowed by ``isinstance``. Two
- checks means one of them decides, and this pins which: a refactor that
- passes the unfiltered arguments through reintroduces the failure, and
+ ``_commit_extensions`` calls every entry it is handed, so the
+ ``isinstance`` filter in the Python caller is the only check. A refactor
+ that passes the unfiltered arguments through reintroduces the failure, and
nothing else in this file notices.
"""
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org