Skip to content

Commit ceaf752

Browse files
timsaucerclaude
andcommitted
Run the with_extensions docstring example in CI
The example was marked `+SKIP` because the main suite has no built FFI extension to import, which is exactly how such an example rots. Parse the statements out of the live docstring in the query-planner example suite, drop the skip, and execute each one against a real extension bundle. Only names are redirected: `my_extension` resolves to a stand-in combining this repository's provider codecs and planner, and `SessionContext` supplies the config that planner reads. A renamed method, a changed signature, or a wrong expected output now fails CI, which already runs this suite. Also drop the `extensions` Args entry's restatement of the type hint and say instead what the hint does not: install order is chain order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 8c9328a commit ceaf752

2 files changed

Lines changed: 95 additions & 4 deletions

File tree

examples/datafusion-ffi-query-planner-example/python/tests/_test_three_library_query_planner.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717

1818
from __future__ import annotations
1919

20+
import doctest
2021
import gc
22+
import inspect
23+
import io
24+
import sys
25+
import types
2126

2227
import pyarrow as pa
2328
import pytest
@@ -994,3 +999,81 @@ def test_composed_codecs_with_query_planner():
994999
assert logical_codec.table_provider_encode_calls() > 0
9951000
assert logical_codec.table_provider_decode_calls() > 0
9961001
assert physical_codec.execution_plan_decode_calls() > 0
1002+
1003+
1004+
class _DocstringExampleExtension:
1005+
"""Stand-in for the ``my_extension`` bundle named in the docstring.
1006+
1007+
The docstring shows a single engine bundle taking a scheduler address,
1008+
which is what a real distributed engine ships: one object contributing a
1009+
planner *and* the codecs that carry its plans. Here that is assembled from
1010+
this repository's two example libraries. The address is accepted and
1011+
ignored; everything else the example touches is the real API.
1012+
"""
1013+
1014+
def __init__(self, endpoint: str) -> None:
1015+
self.endpoint = endpoint
1016+
self._codecs = ProviderCodecsExtension()
1017+
self._planner = MyPlannerExtension()
1018+
1019+
def __datafusion_session_extension__(
1020+
self, ctx: SessionContext
1021+
) -> SessionExtensionComponents:
1022+
codecs = self._codecs.__datafusion_session_extension__(ctx)
1023+
planner = self._planner.__datafusion_session_extension__(ctx)
1024+
return SessionExtensionComponents(
1025+
logical_extension_codecs=(
1026+
*codecs.logical_extension_codecs,
1027+
*planner.logical_extension_codecs,
1028+
),
1029+
physical_extension_codecs=(
1030+
*codecs.physical_extension_codecs,
1031+
*planner.physical_extension_codecs,
1032+
),
1033+
query_planner=planner.query_planner,
1034+
)
1035+
1036+
1037+
def test_with_extensions_docstring_example_still_runs():
1038+
"""Run the ``with_extensions`` docstring example verbatim.
1039+
1040+
The example is marked ``+SKIP`` because the main suite has no built FFI
1041+
extension to import, which is exactly how such an example rots. Here the
1042+
statements are parsed out of the live docstring, the skip is dropped, and
1043+
each one is executed and its output compared.
1044+
1045+
Only names are redirected: ``my_extension`` resolves to the bundle above,
1046+
and ``SessionContext`` supplies the config this library's planner reads.
1047+
A renamed method, a changed signature, or a wrong expected output in the
1048+
docstring fails here.
1049+
"""
1050+
examples = doctest.DocTestParser().get_examples(
1051+
inspect.getdoc(SessionContext.with_extensions)
1052+
)
1053+
assert examples, "with_extensions docstring has no examples to check"
1054+
for example in examples:
1055+
example.options.pop(doctest.SKIP, None)
1056+
1057+
module = types.ModuleType("my_extension")
1058+
module.DistributedEngineExtension = _DocstringExampleExtension
1059+
1060+
def make_context() -> SessionContext:
1061+
return SessionContext(
1062+
SessionConfig().with_extension(MyPlannerConfig(max_rows=3))
1063+
)
1064+
1065+
test = doctest.DocTest(
1066+
examples,
1067+
{"SessionContext": make_context},
1068+
"SessionContext.with_extensions",
1069+
None,
1070+
None,
1071+
None,
1072+
)
1073+
output = io.StringIO()
1074+
sys.modules["my_extension"] = module
1075+
try:
1076+
results = doctest.DocTestRunner().run(test, out=output.write)
1077+
finally:
1078+
del sys.modules["my_extension"]
1079+
assert results.failed == 0, output.getvalue()

python/datafusion/context.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,9 +1845,8 @@ def with_extensions(
18451845
collected raise an error.
18461846
18471847
Args:
1848-
extensions: One or more objects implementing
1849-
``__datafusion_session_extension__`` (see
1850-
:py:class:`SessionExtensionExportable`).
1848+
extensions: Extension bundles to install, in the order their
1849+
codecs join the chain.
18511850
18521851
Returns:
18531852
A new context with all extension components installed.
@@ -1864,11 +1863,20 @@ def with_extensions(
18641863
least one of them.
18651864
18661865
Examples:
1866+
The example is skipped here because it needs a built FFI
1867+
extension library, which this package does not ship. It is run
1868+
verbatim against a real one by
1869+
``test_with_extensions_docstring_example_still_runs`` in
1870+
``examples/datafusion-ffi-query-planner-example``, so it cannot
1871+
drift from the API.
1872+
18671873
>>> from my_extension import DistributedEngineExtension # doctest: +SKIP
18681874
>>> ctx = SessionContext().with_extensions(
18691875
... DistributedEngineExtension("scheduler:50050")
18701876
... ) # doctest: +SKIP
1871-
>>> ctx.sql("SELECT 1").collect() # doctest: +SKIP
1877+
>>> batches = ctx.sql("SELECT 1 AS n").collect() # doctest: +SKIP
1878+
>>> batches[0].column(0).to_pylist() # doctest: +SKIP
1879+
[1]
18721880
"""
18731881
if not extensions:
18741882
msg = "with_extensions requires at least one extension"

0 commit comments

Comments
 (0)