diff --git a/packages/python/dirsql/_async.py b/packages/python/dirsql/_async.py index 3323509f5..71006a7bd 100644 --- a/packages/python/dirsql/_async.py +++ b/packages/python/dirsql/_async.py @@ -137,7 +137,7 @@ def _build_db(self): if config_extensions is not None: extensions = [*(extensions or []), *config_extensions] suppress = True - return _RustDirSQL( + return self._new_core( self._root, tables=self._tables, ignore=self._ignore, @@ -149,6 +149,9 @@ def _build_db(self): suppress_config_extensions=suppress, ) + def _new_core(self, root, **kwargs): + return _RustDirSQL(root, **kwargs) + def _resolved_extensions(self): """Resolve each programmatic extension's ``path`` to a loadable file. diff --git a/packages/python/dirsql/_async_test.py b/packages/python/dirsql/_async_test.py index be64a4ac8..68f8a9dc3 100644 --- a/packages/python/dirsql/_async_test.py +++ b/packages/python/dirsql/_async_test.py @@ -81,14 +81,18 @@ def describe_ready_and_query(): @pytest.mark.asyncio async def it_uses_the_background_db(): with ( - patch.object(async_mod, "_RustDirSQL", _FakeRustDirSQL), patch.object( async_mod, "resolve_extension_path", side_effect=lambda path, base, resolve_relative: f"R:{path}", ) as resolver, ): - db = async_mod.DirSQL( + + class InjectedDirSQL(async_mod.DirSQL): + def _new_core(self, root, **kwargs): + return _FakeRustDirSQL(root, **kwargs) + + db = InjectedDirSQL( "/tmp/root", tables=["table-a"], ignore=["**/*.tmp"], diff --git a/packages/python/e2e-attestations/claude-1330-python-dirsql-patch.json b/packages/python/e2e-attestations/claude-1330-python-dirsql-patch.json new file mode 100644 index 000000000..b12e4f4c8 --- /dev/null +++ b/packages/python/e2e-attestations/claude-1330-python-dirsql-patch.json @@ -0,0 +1,7 @@ +{ + "command": "uv run python -m pytest tests/e2e/capture_collision_test.py tests/e2e/fanout_test.py tests/e2e/cli_version_test.py tests/e2e/extension_package_test.py tests/e2e/on_file_query_test.py tests/e2e/sigint_interrupt_test.py tests/e2e/plugin_extension_discovery_test.py tests/e2e/plugin_discovery_test.py tests/e2e/query_contract_test.py -x -q", + "ran_at": 1791302200, + "exit_code": 0, + "commit": "1553e866cb8da7050a1c444833e269c831f6e0a1", + "branch": "claude/1330-python-dirsql-patch" +} diff --git a/packages/python/testing-conventions.toml b/packages/python/testing-conventions.toml index ece24940d..28308a5d6 100644 --- a/packages/python/testing-conventions.toml +++ b/packages/python/testing-conventions.toml @@ -14,17 +14,6 @@ extra_scope = ["packages/rust/src", "packages/python/src", "packages/python/test fail_under = 100 branch = true -# The hermetic integration tier is defined by patching the first-party core -# boundary: `dirsql._async._RustDirSQL` is the PyO3 class, and replacing it is -# what lets these tests run SQLite-free and native-build-free. There is no -# third-party target to re-root onto, so `no-first-party-patch` has nothing to -# say about this seam. Every other patch in both files targets stdlib and stays -# gated. -[[python.exempt]] -path = "tests/integration/hermetic/dirsql_test.py" -rules = ["no-first-party-patch"] -reason = "the hermetic tier IS the _RustDirSQL core-boundary patch; no third-party target exists to move it to" - [[python.exempt]] path = "tests/integration/hermetic/extensions_test.py" rules = ["no-first-party-patch"] diff --git a/packages/python/tests/integration/hermetic/dirsql_test.py b/packages/python/tests/integration/hermetic/dirsql_test.py index ae3c3602b..911b491eb 100644 --- a/packages/python/tests/integration/hermetic/dirsql_test.py +++ b/packages/python/tests/integration/hermetic/dirsql_test.py @@ -85,9 +85,8 @@ def _reset_instances(): @pytest.fixture def mock_core(): - """Replace the Rust-backed ``_RustDirSQL`` alias in ``dirsql._async``.""" - with patch.object(async_mod, "_RustDirSQL", _FakeRustDirSQL): - yield _FakeRustDirSQL + """Build the wrapper with a fake core through its factory seam.""" + return _wrapper_with_core(_FakeRustDirSQL) @pytest.fixture @@ -110,7 +109,7 @@ async def spy(func, *args, **kwargs): @pytest.fixture def core_init_raises(request): - """Patch the Rust core with a constructor that raises ``request.param``. + """Build the wrapper with a core constructor that raises ``request.param``. Parametrize indirectly with the exception instance the core should raise on construction (init / config-load failure paths). @@ -121,8 +120,15 @@ class Boom(_FakeRustDirSQL): def __init__(self, *a, **kw): raise exc - with patch.object(async_mod, "_RustDirSQL", Boom): - yield exc + return _wrapper_with_core(Boom) + + +def _wrapper_with_core(core_factory): + class WrapperWithCore(async_mod.DirSQL): + def _new_core(self, root, **kwargs): + return core_factory(root, **kwargs) + + return WrapperWithCore def describe_binding_layer(): @@ -131,14 +137,14 @@ def describe_async_offloading(): # packages/python/README.md ("DirSQL is async by default"). @pytest.mark.asyncio async def it_offloads_init_via_to_thread(mock_core, to_thread_spy): - db = async_mod.DirSQL("/root", tables=["t"]) + db = mock_core("/root", tables=["t"]) await db.ready() assert "_build_db" in to_thread_spy, to_thread_spy @pytest.mark.asyncio async def it_offloads_query_via_to_thread(mock_core, to_thread_spy): - db = async_mod.DirSQL("/root", tables=["t"]) + db = mock_core("/root", tables=["t"]) await db.ready() await db.query("SELECT 1") @@ -152,13 +158,13 @@ def describe_ready(): "core_init_raises", [RuntimeError("init failed")], indirect=True ) async def it_surfaces_init_exceptions(core_init_raises): - db = async_mod.DirSQL("/root", tables=["t"]) + db = core_init_raises("/root", tables=["t"]) with pytest.raises(RuntimeError, match="init failed"): await db.ready() @pytest.mark.asyncio async def it_is_safe_to_call_repeatedly(mock_core): - db = async_mod.DirSQL("/root", tables=["t"]) + db = mock_core("/root", tables=["t"]) await db.ready() await db.ready() await db.ready() @@ -169,7 +175,7 @@ async def it_is_safe_to_call_repeatedly(mock_core): "core_init_raises", [ValueError("bad config")], indirect=True ) async def it_re_raises_init_error_on_every_ready_call(core_init_raises): - db = async_mod.DirSQL("/root", tables=["t"]) + db = core_init_raises("/root", tables=["t"]) with pytest.raises(ValueError): await db.ready() with pytest.raises(ValueError): @@ -180,7 +186,7 @@ def describe_query(): # docs/reference/sdk.md and packages/python/README.md. @pytest.mark.asyncio async def it_passes_sql_through_untouched(mock_core): - db = async_mod.DirSQL("/root", tables=["t"]) + db = mock_core("/root", tables=["t"]) await db.ready() sql = "SELECT name, age FROM users WHERE age > 30 -- comment" @@ -193,7 +199,7 @@ async def it_passes_sql_through_untouched(mock_core): async def it_passes_a_path_table_name_through_untouched(mock_core): # Path-table resolution lives in the core; the SDK must not # rewrite, quote, or normalize the name on the way down. - db = async_mod.DirSQL("/root", tables=["t"]) + db = mock_core("/root", tables=["t"]) await db.ready() sql = "SELECT basename FROM './docs/*.md'" @@ -206,7 +212,7 @@ def describe_watch(): # docs/reference/sdk.md and packages/python/README.md. @pytest.mark.asyncio async def it_lazily_starts_watcher_on_first_iteration(mock_core): - db = async_mod.DirSQL("/root", tables=["t"]) + db = mock_core("/root", tables=["t"]) await db.ready() stream = db.watch() @@ -220,7 +226,7 @@ async def it_lazily_starts_watcher_on_first_iteration(mock_core): @pytest.mark.asyncio async def it_drains_buffered_events_before_polling_again(mock_core): - db = async_mod.DirSQL("/root", tables=["t"]) + db = mock_core("/root", tables=["t"]) await db.ready() fake = _FakeRustDirSQL.instances[0] @@ -235,7 +241,7 @@ async def it_drains_buffered_events_before_polling_again(mock_core): @pytest.mark.asyncio async def it_polls_until_events_arrive(mock_core): - db = async_mod.DirSQL("/root", tables=["t"]) + db = mock_core("/root", tables=["t"]) await db.ready() fake = _FakeRustDirSQL.instances[0] @@ -251,7 +257,7 @@ def describe_config_kwarg(): # docs/reference/config.md and packages/python/README.md. @pytest.mark.asyncio async def it_forwards_config_path_to_core(mock_core): - db = async_mod.DirSQL(config="/some/.dirsql.toml") + db = mock_core(config="/some/.dirsql.toml") await db.ready() inst = _FakeRustDirSQL.instances[-1] @@ -266,7 +272,7 @@ async def it_forwards_config_path_to_core(mock_core): "core_init_raises", [FileNotFoundError("/missing.toml")], indirect=True ) async def it_surfaces_config_load_errors(core_init_raises): - db = async_mod.DirSQL(config="/missing.toml") + db = core_init_raises(config="/missing.toml") with pytest.raises(FileNotFoundError): await db.ready() @@ -276,7 +282,7 @@ async def it_forwards_construction_without_root_or_config_to_the_core( ): # With neither root nor config the core roots at the cwd; the # wrapper forwards both as None. - db = async_mod.DirSQL() + db = mock_core() await db.ready() inst = _FakeRustDirSQL.instances[-1] @@ -289,7 +295,7 @@ def describe_ignore_kwarg(): @pytest.mark.asyncio async def it_forwards_ignore_to_core(mock_core): ignore = ["**/node_modules/**", ".git"] - db = async_mod.DirSQL("/root", tables=["t"], ignore=ignore) + db = mock_core("/root", tables=["t"], ignore=ignore) await db.ready() inst = _FakeRustDirSQL.instances[0] @@ -299,7 +305,7 @@ async def it_forwards_ignore_to_core(mock_core): @pytest.mark.asyncio async def it_defaults_ignore_to_none(mock_core): - db = async_mod.DirSQL("/root", tables=["t"]) + db = mock_core("/root", tables=["t"]) await db.ready() assert _FakeRustDirSQL.instances[0].ignore is None @@ -308,14 +314,14 @@ def describe_no_ignore_kwarg(): # docs/reference/path-tables.md. @pytest.mark.asyncio async def it_forwards_no_ignore_to_core(mock_core): - db = async_mod.DirSQL("/root", no_ignore=True) + db = mock_core("/root", no_ignore=True) await db.ready() assert _FakeRustDirSQL.instances[0].no_ignore is True @pytest.mark.asyncio async def it_defaults_no_ignore_to_false(mock_core): - db = async_mod.DirSQL("/root") + db = mock_core("/root") await db.ready() assert _FakeRustDirSQL.instances[0].no_ignore is False @@ -324,7 +330,7 @@ def describe_persist_kwargs(): # Feature: persist / persist_path. See docs/howto/persist.md. @pytest.mark.asyncio async def it_forwards_persist_kwargs_to_core(mock_core): - db = async_mod.DirSQL( + db = mock_core( "/root", tables=["t"], persist=True, @@ -337,7 +343,7 @@ async def it_forwards_persist_kwargs_to_core(mock_core): @pytest.mark.asyncio async def it_defaults_persist_to_false(mock_core): - db = async_mod.DirSQL("/root", tables=["t"]) + db = mock_core("/root", tables=["t"]) await db.ready() inst = _FakeRustDirSQL.instances[0] assert inst.persist is False