Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**Changed**

- **The config-extension resolver split into three modules.** `_resolve_entries` moved to `dirsql/resolve_entries.py` and `resolve_configs_extension_specs` to `dirsql/resolve_configs_extension_specs.py`; `dirsql/resolve_config_extensions.py` keeps the TOML loading and the single-config `resolve_config_extension_specs`. Every function keeps its name and behavior — internal reorganisation with no change to the published API or CLI behavior. (#1056)
2 changes: 1 addition & 1 deletion packages/python/dirsql/_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os

from dirsql._dirsql import DirSQL as _RustDirSQL
from dirsql.resolve_config_extensions import resolve_configs_extension_specs
from dirsql.resolve_configs_extension_specs import resolve_configs_extension_specs
from dirsql.resolve_extension import resolve_extension_path


Expand Down
6 changes: 6 additions & 0 deletions packages/python/dirsql/_async_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@ async def it_passes_no_extensions_through_unresolved():
resolver.assert_not_called()

def describe_config_file_extensions():
def it_imports_the_config_resolver_from_the_plural_module():
assert (
async_mod.resolve_configs_extension_specs.__module__
== "dirsql.resolve_configs_extension_specs"
)

@pytest.mark.asyncio
async def it_appends_resolved_config_extensions_and_suppresses_the_core():
with (
Expand Down
10 changes: 5 additions & 5 deletions packages/python/dirsql/cli/resolve_config_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
The compiled ``dirsql`` binary loads a config's extensions literally -- it
has no ``importlib``, so it cannot resolve a bare **package name**. When any
TOML config in argv names an extension by package name, the shared SDK
resolver (:mod:`dirsql.resolve_config_extensions`) resolves every config's
extensions and this launcher passes the resolved literal paths to the binary
via repeatable ``--extension`` flags; the binary then loads those and ignores
the configs' own extension entries.
resolver (:mod:`dirsql.resolve_configs_extension_specs`) resolves every
config's extensions and this launcher passes the resolved literal paths to
the binary via repeatable ``--extension`` flags; the binary then loads those
and ignores the configs' own extension entries.

Native-language configs (``.py`` / ``.js`` / ``.mjs`` / ``.cjs``) are untouched:
the binary dispatches those to ``dirsql interpret``, whose handshake already
Expand All @@ -15,7 +15,7 @@

from __future__ import annotations

from ..resolve_config_extensions import resolve_configs_extension_specs
from ..resolve_configs_extension_specs import resolve_configs_extension_specs
from .config_paths_from_argv import config_paths_from_argv

# Config extensions the binary dispatches to `dirsql interpret`; never
Expand Down
6 changes: 6 additions & 0 deletions packages/python/dirsql/cli/resolve_config_extensions_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ def _patch(specs, paths=("/x/.dirsql.toml",)):


def describe_with_resolved_extensions():
def it_imports_the_shared_resolver_from_the_plural_module():
assert (
rce.resolve_configs_extension_specs.__module__
== "dirsql.resolve_configs_extension_specs"
)

def it_passes_init_through_untouched():
argv = ["init", "--root", "."]
with _patch([{"path": "R:pkg", "entrypoint": None}]) as (scan, resolver):
Expand Down
45 changes: 4 additions & 41 deletions packages/python/dirsql/resolve_config_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
the config's entries are not loaded a second time.

Shared by the ``DirSQL`` constructor (``config=`` path) and the CLI launcher
(which converts the resolved specs into ``--extension`` flags).
(which converts the resolved specs into ``--extension`` flags), both of which
reach it through :mod:`dirsql.resolve_configs_extension_specs`.
"""

from __future__ import annotations
Expand All @@ -17,7 +18,8 @@
import sys
from importlib import import_module

from .resolve_extension import is_bare_name, resolve_extension_path
from .resolve_entries import _resolve_entries
from .resolve_extension import is_bare_name


def _load_toml_module():
Expand Down Expand Up @@ -67,21 +69,6 @@ def _has_bare_name(entries):
)


def _resolve_entries(entries, base):
specs = []
for e in entries:
entrypoint = e.get("entrypoint")
specs.append(
{
"path": resolve_extension_path(
e["path"], base=base, resolve_relative=True
),
"entrypoint": entrypoint if isinstance(entrypoint, str) else None,
}
)
return specs


def resolve_config_extension_specs(config_path):
"""Resolve a TOML config's ``[[dirsql.extension]]`` entries to literal paths.

Expand All @@ -100,27 +87,3 @@ def resolve_config_extension_specs(config_path):
if not _has_bare_name(entries):
return None
return _resolve_entries(entries, base)


def resolve_configs_extension_specs(config_paths):
"""Resolve the ``[[dirsql.extension]]`` entries of several configs, in order.

The SDK intervenes for the whole set only when **some** config names an
extension by bare package name (the core can resolve neither package names
nor -- once globally suppressed -- the literal entries of the other
configs). When it intervenes it resolves **every** config's entries, each
against that config's own parent directory, concatenated in ``config_paths``
order; the caller suppresses the core's config-extension loading and passes
the resolved list. Returns ``None`` when no config uses a package name,
leaving every config's loading to the core.
"""
loaded = [_load_extension_entries(p) for p in config_paths]
if not any(item is not None and _has_bare_name(item[0]) for item in loaded):
return None
specs = []
for item in loaded:
if item is None:
continue
entries, base = item
specs.extend(_resolve_entries(entries, base))
return specs
99 changes: 16 additions & 83 deletions packages/python/dirsql/resolve_config_extensions_test.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Unit tests for `resolve_config_extension_specs`.

Real `.dirsql.toml` files are parsed; the only mocked collaborator is
`resolve_extension_path` (the effectful file-vs-package resolver). The pure
`is_bare_name` stays real.
Real `.dirsql.toml` files are parsed; `_resolve_entries` (which owns the
effectful per-entry resolution) is mocked, returning a `base:path` sentinel
per entry. The pure `is_bare_name` stays real.
"""

import os
Expand All @@ -29,8 +29,8 @@ def _write(path, body):
def _patch():
return mock.patch.object(
rce,
"resolve_extension_path",
side_effect=lambda path, base, resolve_relative: f"R:{path}",
"_resolve_entries",
side_effect=lambda entries, base: [f"{base}:{e['path']}" for e in entries],
)


Expand Down Expand Up @@ -93,9 +93,9 @@ def it_returns_none_when_extension_entries_are_not_tables(cfg_dir):
def it_returns_none_when_all_paths_are_literal(cfg_dir):
path = os.path.join(cfg_dir, ".dirsql.toml")
_write(path, '[[dirsql.extension]]\npath = "ext/a.so"\n')
with _patch() as resolver:
with _patch() as resolve_entries:
assert rce.resolve_config_extension_specs(path) is None
resolver.assert_not_called()
resolve_entries.assert_not_called()

def it_resolves_every_entry_when_a_path_is_a_bare_package_name(cfg_dir):
path = os.path.join(cfg_dir, ".dirsql.toml")
Expand All @@ -107,24 +107,16 @@ def it_resolves_every_entry_when_a_path_is_a_bare_package_name(cfg_dir):
"[[dirsql.extension]]\n"
'path = "ext/local.so"\n',
)
with _patch() as resolver:
with _patch() as resolve_entries:
specs = rce.resolve_config_extension_specs(path)
assert specs == [
{"path": "R:sqlite_vec", "entrypoint": "sqlite3_vec_init"},
{"path": "R:ext/local.so", "entrypoint": None},
]
resolver.assert_any_call("sqlite_vec", base=cfg_dir, resolve_relative=True)
resolver.assert_any_call("ext/local.so", base=cfg_dir, resolve_relative=True)

def it_normalizes_a_non_string_entrypoint_to_none(cfg_dir):
path = os.path.join(cfg_dir, ".dirsql.toml")
_write(
path,
'[[dirsql.extension]]\npath = "sqlite_vec"\nentrypoint = 42\n',
assert specs == [f"{cfg_dir}:sqlite_vec", f"{cfg_dir}:ext/local.so"]
resolve_entries.assert_called_once_with(
[
{"path": "sqlite_vec", "entrypoint": "sqlite3_vec_init"},
{"path": "ext/local.so"},
],
cfg_dir,
)
with _patch():
specs = rce.resolve_config_extension_specs(path)
assert specs == [{"path": "R:sqlite_vec", "entrypoint": None}]

def it_skips_a_non_string_path_in_the_package_name_probe(cfg_dir):
# A dict entry whose `path` isn't a string is not treated as a package
Expand All @@ -137,63 +129,4 @@ def it_skips_a_non_string_path_in_the_package_name_probe(cfg_dir):
)
with _patch():
specs = rce.resolve_config_extension_specs(path)
assert specs == [
{"path": "R:42", "entrypoint": None},
{"path": "R:sqlite_vec", "entrypoint": None},
]


def describe_resolve_configs_extension_specs():
def it_returns_none_when_no_config_uses_a_bare_name(cfg_dir):
a = os.path.join(cfg_dir, "a.toml")
b = os.path.join(cfg_dir, "b.toml")
_write(a, '[[dirsql.extension]]\npath = "ext/a.so"\n')
_write(
b, '[[table]]\nname = "t"\nddl = "CREATE TABLE t (x TEXT)"\nglob = "*"\n'
)
with _patch() as resolver:
assert rce.resolve_configs_extension_specs([a, b]) is None
resolver.assert_not_called()

def it_returns_none_for_an_empty_list():
assert rce.resolve_configs_extension_specs([]) is None

def it_resolves_every_config_in_order_when_one_uses_a_bare_name(cfg_dir):
a = os.path.join(cfg_dir, "a.toml")
b = os.path.join(cfg_dir, "b.toml")
# a: only a literal; b: a bare package name -> intervene for both.
_write(a, '[[dirsql.extension]]\npath = "ext/a.so"\n')
_write(b, '[[dirsql.extension]]\npath = "sqlite_vec"\n')
with _patch():
specs = rce.resolve_configs_extension_specs([a, b])
assert specs == [
{"path": "R:ext/a.so", "entrypoint": None},
{"path": "R:sqlite_vec", "entrypoint": None},
]

def it_skips_a_missing_config_but_resolves_the_rest(cfg_dir):
missing = os.path.join(cfg_dir, "nope.toml")
b = os.path.join(cfg_dir, "b.toml")
_write(b, '[[dirsql.extension]]\npath = "sqlite_vec"\n')
with _patch():
specs = rce.resolve_configs_extension_specs([missing, b])
assert specs == [{"path": "R:sqlite_vec", "entrypoint": None}]

def it_resolves_each_config_against_its_own_directory(cfg_dir):
import tempfile

with tempfile.TemporaryDirectory() as other:
a = os.path.join(cfg_dir, "a.toml")
b = os.path.join(other, "b.toml")
_write(a, '[[dirsql.extension]]\npath = "sqlite_vec"\n')
_write(b, '[[dirsql.extension]]\npath = "ext/b.so"\n')
with mock.patch.object(
rce,
"resolve_extension_path",
side_effect=lambda path, base, resolve_relative: base,
):
specs = rce.resolve_configs_extension_specs([a, b])
assert specs == [
{"path": cfg_dir, "entrypoint": None},
{"path": other, "entrypoint": None},
]
assert specs == [f"{cfg_dir}:42", f"{cfg_dir}:sqlite_vec"]
35 changes: 35 additions & 0 deletions packages/python/dirsql/resolve_configs_extension_specs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Resolution of several TOML configs' ``[[dirsql.extension]]`` entries, in order.

The plural counterpart to :func:`dirsql.resolve_config_extensions
.resolve_config_extension_specs`, used by the SDK constructor and the CLI
launcher when argv carries more than one ``--config``.
"""

from __future__ import annotations

from .resolve_config_extensions import _has_bare_name, _load_extension_entries
from .resolve_entries import _resolve_entries


def resolve_configs_extension_specs(config_paths):
"""Resolve the ``[[dirsql.extension]]`` entries of several configs, in order.

The SDK intervenes for the whole set only when **some** config names an
extension by bare package name (the core can resolve neither package names
nor -- once globally suppressed -- the literal entries of the other
configs). When it intervenes it resolves **every** config's entries, each
against that config's own parent directory, concatenated in ``config_paths``
order; the caller suppresses the core's config-extension loading and passes
the resolved list. Returns ``None`` when no config uses a package name,
leaving every config's loading to the core.
"""
loaded = [_load_extension_entries(p) for p in config_paths]
if not any(item is not None and _has_bare_name(item[0]) for item in loaded):
return None
specs = []
for item in loaded:
if item is None:
continue
entries, base = item
specs.extend(_resolve_entries(entries, base))
return specs
77 changes: 77 additions & 0 deletions packages/python/dirsql/resolve_configs_extension_specs_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
"""Unit tests for `resolve_configs_extension_specs`.

Real `.dirsql.toml` files are parsed; `_resolve_entries` (which owns the
effectful per-entry resolution) is mocked, returning a `base:path` sentinel
per entry so ordering and the per-config base directory are both observable.
"""

import os
import tempfile
from unittest import mock

import pytest

import dirsql.resolve_configs_extension_specs as mod


@pytest.fixture
def cfg_dir():
with tempfile.TemporaryDirectory() as d:
yield d


def _write(path, body):
with open(path, "w") as f:
f.write(body)


def _patch():
return mock.patch.object(
mod,
"_resolve_entries",
side_effect=lambda entries, base: [f"{base}:{e['path']}" for e in entries],
)


def describe_resolve_configs_extension_specs():
def it_returns_none_when_no_config_uses_a_bare_name(cfg_dir):
a = os.path.join(cfg_dir, "a.toml")
b = os.path.join(cfg_dir, "b.toml")
_write(a, '[[dirsql.extension]]\npath = "ext/a.so"\n')
_write(
b, '[[table]]\nname = "t"\nddl = "CREATE TABLE t (x TEXT)"\nglob = "*"\n'
)
with _patch() as resolve_entries:
assert mod.resolve_configs_extension_specs([a, b]) is None
resolve_entries.assert_not_called()

def it_returns_none_for_an_empty_list():
assert mod.resolve_configs_extension_specs([]) is None

def it_resolves_every_config_in_order_when_one_uses_a_bare_name(cfg_dir):
a = os.path.join(cfg_dir, "a.toml")
b = os.path.join(cfg_dir, "b.toml")
# a: only a literal; b: a bare package name -> intervene for both.
_write(a, '[[dirsql.extension]]\npath = "ext/a.so"\n')
_write(b, '[[dirsql.extension]]\npath = "sqlite_vec"\n')
with _patch():
specs = mod.resolve_configs_extension_specs([a, b])
assert specs == [f"{cfg_dir}:ext/a.so", f"{cfg_dir}:sqlite_vec"]

def it_skips_a_missing_config_but_resolves_the_rest(cfg_dir):
missing = os.path.join(cfg_dir, "nope.toml")
b = os.path.join(cfg_dir, "b.toml")
_write(b, '[[dirsql.extension]]\npath = "sqlite_vec"\n')
with _patch():
specs = mod.resolve_configs_extension_specs([missing, b])
assert specs == [f"{cfg_dir}:sqlite_vec"]

def it_resolves_each_config_against_its_own_directory(cfg_dir):
with tempfile.TemporaryDirectory() as other:
a = os.path.join(cfg_dir, "a.toml")
b = os.path.join(other, "b.toml")
_write(a, '[[dirsql.extension]]\npath = "sqlite_vec"\n')
_write(b, '[[dirsql.extension]]\npath = "ext/b.so"\n')
with _patch():
specs = mod.resolve_configs_extension_specs([a, b])
assert specs == [f"{cfg_dir}:sqlite_vec", f"{other}:ext/b.so"]
Loading
Loading