Skip to content

Commit e95bc43

Browse files
timsaucerclaude
andcommitted
Move session extension types into datafusion.extensions
QueryPlannerExportable, SessionExtensionComponents, and SessionExtensionExportable describe how an extension library plugs into a session, not how a SessionContext behaves. Give them their own module so context.py does not keep absorbing the extension surface as it grows. extensions.py imports SessionContext, the codec protocols, and CapsuleType under TYPE_CHECKING only, so context.py can import from it at runtime without a cycle. All three names remain importable from datafusion and datafusion.context; QueryPlannerExportable stays out of the top-level __all__ as before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 97ef958 commit e95bc43

3 files changed

Lines changed: 124 additions & 59 deletions

File tree

python/datafusion/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,6 @@
8080
RuntimeEnvBuilder,
8181
SessionConfig,
8282
SessionContext,
83-
SessionExtensionComponents,
84-
SessionExtensionExportable,
8583
SQLOptions,
8684
)
8785
from .dataframe import (
@@ -94,6 +92,10 @@
9492
)
9593
from .dataframe_formatter import configure_formatter
9694
from .expr import Expr, WindowFrame
95+
from .extensions import (
96+
SessionExtensionComponents,
97+
SessionExtensionExportable,
98+
)
9799
from .io import read_avro, read_csv, read_json, read_parquet
98100
from .options import CsvReadOptions
99101
from .plan import ExecutionPlan, LogicalPlan, Metric, MetricsSet
@@ -150,6 +152,7 @@
150152
"common",
151153
"configure_formatter",
152154
"expr",
155+
"extensions",
153156
"functions",
154157
"ipc",
155158
"lit",

python/datafusion/context.py

Lines changed: 5 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646

4747
import uuid
4848
import warnings
49-
from dataclasses import dataclass
5049
from typing import TYPE_CHECKING, Any, Protocol
5150

5251
try:
@@ -70,6 +69,11 @@
7069
)
7170
from datafusion.dataframe import DataFrame
7271
from datafusion.expr import sort_list_to_raw_sort_list
72+
from datafusion.extensions import (
73+
QueryPlannerExportable,
74+
SessionExtensionComponents,
75+
SessionExtensionExportable,
76+
)
7377
from datafusion.options import (
7478
DEFAULT_MAX_INFER_SCHEMA,
7579
CsvReadOptions,
@@ -146,62 +150,6 @@ class PhysicalOptimizerRuleExportable(Protocol):
146150
def __datafusion_physical_optimizer_rule__(self) -> object: ... # noqa: D105
147151

148152

149-
class QueryPlannerExportable(Protocol):
150-
"""Type hint for object that has a __datafusion_query_planner__ PyCapsule.
151-
152-
The method returns a PyCapsule wrapping an ``FFI_QueryPlanner``, typically
153-
produced by a separate compiled extension. ``session`` is the
154-
:py:class:`SessionContext` the planner is being installed on; take the
155-
extension codecs from it rather than building your own.
156-
"""
157-
158-
def __datafusion_query_planner__(self, session: Any) -> object: ... # noqa: D105
159-
160-
161-
@dataclass(frozen=True)
162-
class SessionExtensionComponents:
163-
"""Components an extension contributes to a session context.
164-
165-
Returned by :py:meth:`SessionExtensionExportable.__datafusion_session_extension__`
166-
and consumed by :py:meth:`SessionContext.with_extensions`. Every component
167-
must be created against the context passed to that method; components bound
168-
to any other context hold a task-context provider for the wrong session and
169-
cannot be rebound.
170-
"""
171-
172-
logical_extension_codecs: tuple[
173-
LogicalExtensionCodecExportable | _PyCapsule, ...
174-
] = ()
175-
"""Logical codecs to add to the session's codec chain, in declaration order."""
176-
177-
physical_extension_codecs: tuple[
178-
PhysicalExtensionCodecExportable | _PyCapsule, ...
179-
] = ()
180-
"""Physical codecs to add to the session's codec chain, in declaration order."""
181-
182-
query_planner: QueryPlannerExportable | _PyCapsule | None = None
183-
"""Optional query planner.
184-
185-
At most one extension per :py:meth:`SessionContext.with_extensions` call may
186-
supply one.
187-
"""
188-
189-
190-
class SessionExtensionExportable(Protocol):
191-
"""Type hint for extension bundles installable via ``with_extensions``.
192-
193-
Implementations are reusable configuration objects: they must not retain a
194-
:py:class:`SessionContext` and must create fresh components on every call
195-
using the context supplied by :py:meth:`SessionContext.with_extensions`.
196-
They should also avoid mutating global state during binding, since a
197-
failed installation discards the destination context.
198-
"""
199-
200-
def __datafusion_session_extension__( # noqa: D105
201-
self, ctx: SessionContext
202-
) -> SessionExtensionComponents: ...
203-
204-
205153
class SessionConfig:
206154
"""Session configuration options."""
207155

python/datafusion/extensions.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
"""Protocols and value types for installing extensions on a session context.
19+
20+
An *extension* is a reusable configuration object — typically shipped by a
21+
separate compiled library — that contributes components to a
22+
:py:class:`~datafusion.context.SessionContext`. It implements
23+
:py:class:`SessionExtensionExportable` by returning a
24+
:py:class:`SessionExtensionComponents` describing what it contributes, and is
25+
installed with :py:meth:`~datafusion.context.SessionContext.with_extensions`::
26+
27+
ctx = SessionContext().with_extensions(MyLibraryExtension())
28+
29+
Installing through ``with_extensions`` rather than by chaining the individual
30+
``with_*`` methods matters for components that hold a task-context provider:
31+
the extension is handed the destination context so every component binds to
32+
the session that is actually returned. See the FFI extensions guide in the
33+
contributor documentation for the full rationale.
34+
"""
35+
36+
from __future__ import annotations
37+
38+
from dataclasses import dataclass
39+
from typing import TYPE_CHECKING, Any, Protocol
40+
41+
if TYPE_CHECKING:
42+
from _typeshed import CapsuleType as _PyCapsule
43+
44+
from datafusion.context import SessionContext
45+
from datafusion.user_defined import (
46+
LogicalExtensionCodecExportable,
47+
PhysicalExtensionCodecExportable,
48+
)
49+
50+
__all__ = [
51+
"QueryPlannerExportable",
52+
"SessionExtensionComponents",
53+
"SessionExtensionExportable",
54+
]
55+
56+
57+
class QueryPlannerExportable(Protocol):
58+
"""Type hint for object that has a __datafusion_query_planner__ PyCapsule.
59+
60+
The method returns a PyCapsule wrapping an ``FFI_QueryPlanner``, typically
61+
produced by a separate compiled extension. ``session`` is the
62+
:py:class:`~datafusion.context.SessionContext` the planner is being
63+
installed on; take the extension codecs from it rather than building your
64+
own.
65+
"""
66+
67+
def __datafusion_query_planner__(self, session: Any) -> object: ... # noqa: D105
68+
69+
70+
@dataclass(frozen=True)
71+
class SessionExtensionComponents:
72+
"""Components an extension contributes to a session context.
73+
74+
Returned by :py:meth:`SessionExtensionExportable.__datafusion_session_extension__`
75+
and consumed by
76+
:py:meth:`~datafusion.context.SessionContext.with_extensions`. Every
77+
component must be created against the context passed to that method;
78+
components bound to any other context hold a task-context provider for the
79+
wrong session and cannot be rebound.
80+
"""
81+
82+
logical_extension_codecs: tuple[
83+
LogicalExtensionCodecExportable | _PyCapsule, ...
84+
] = ()
85+
"""Logical codecs to add to the session's codec chain, in declaration order."""
86+
87+
physical_extension_codecs: tuple[
88+
PhysicalExtensionCodecExportable | _PyCapsule, ...
89+
] = ()
90+
"""Physical codecs to add to the session's codec chain, in declaration order."""
91+
92+
query_planner: QueryPlannerExportable | _PyCapsule | None = None
93+
"""Optional query planner.
94+
95+
At most one extension per
96+
:py:meth:`~datafusion.context.SessionContext.with_extensions` call may
97+
supply one.
98+
"""
99+
100+
101+
class SessionExtensionExportable(Protocol):
102+
"""Type hint for extension bundles installable via ``with_extensions``.
103+
104+
Implementations are reusable configuration objects: they must not retain a
105+
:py:class:`~datafusion.context.SessionContext` and must create fresh
106+
components on every call using the context supplied by
107+
:py:meth:`~datafusion.context.SessionContext.with_extensions`. They should
108+
also avoid mutating global state during binding, since a failed
109+
installation discards the destination context.
110+
"""
111+
112+
def __datafusion_session_extension__( # noqa: D105
113+
self, ctx: SessionContext
114+
) -> SessionExtensionComponents: ...

0 commit comments

Comments
 (0)