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
13 changes: 9 additions & 4 deletions PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,16 @@ since [#1119](https://github.com/thekevinscott/dirsql/issues/1119) they live
once in `dirsql::extension_resolution` (`#[doc(hidden)]`, not a public Rust
API). Both SDKs reach it through their binding —
[#1120](https://github.com/thekevinscott/dirsql/issues/1120) for Python
(`dirsql._dirsql.plan_config_extensions` / `select_loadable` / `is_bare_name`),
(`dirsql._dirsql.plan_config_extensions` / `plan_extension_path` /
`select_loadable`),
[#1121](https://github.com/thekevinscott/dirsql/issues/1121) for TypeScript
(the addon's `planConfigExtensions` / `selectLoadable` / `isBareName`). Each
host keeps only what it alone can do: reading the config files, and locating an
installed package (`importlib.util.find_spec` / `require.resolve`).
(the addon's `planConfigExtensions` / `planExtensionPath` / `selectLoadable`).
The same probe covers a *programmatic* `extensions=[{path}]` entry, planned by
`plan_extension_path` since
[#1122](https://github.com/thekevinscott/dirsql/issues/1122); the `is_bare_name`
/ `isBareName` export it replaced is gone. Each host keeps only what it alone
can do: reading the config files, and locating an installed package
(`importlib.util.find_spec` / `require.resolve`).

**Config-flag scanning breadth
([#754](https://github.com/thekevinscott/dirsql/issues/754) /
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
**Changed**

- `resolve_extension_path` now plans through the core
(`_dirsql.plan_extension_path`) instead of re-implementing the ordered probe,
so a programmatic `extensions=[{path}]` entry is classified by the same code
as a config entry. `importlib` package lookup is unchanged, and so is the
resolved path. The internal `_dirsql.is_bare_name` binding export is gone,
replaced by `plan_extension_path`. No change to the published API. (#1122)
4 changes: 3 additions & 1 deletion packages/python/dirsql/_dirsql.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,6 @@ def plan_config_extensions(
configs: list[tuple[str, str | None]],
) -> list[ExtensionPlanEntry] | None: ...
def select_loadable(name: str, dirs: list[str], candidates: list[str]) -> str: ...
def is_bare_name(path: str) -> bool: ...
def plan_extension_path(
path: str, base: str, resolve_relative: bool
) -> ExtensionPlanEntry: ...
20 changes: 6 additions & 14 deletions packages/python/dirsql/resolve_extension.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Resolve an extension entry's ``path`` to a concrete loadable file.

Resolution is an ordered probe (file-first, then package), so only a bare
package name reaches the package machinery:
Resolution is an ordered probe (file-first, then package), planned by the core
(``_dirsql.plan_extension_path``) and carried out here: only locating an
installed package is host-specific.

1. **Path-looking** (contains a separator, or ends in ``.so`` / ``.dylib`` /
``.dll`` / ``.pyd``) -- returned as a file path: made absolute against
Expand All @@ -13,10 +14,8 @@
picked from inside it.
"""

import os

from ._dirsql import is_bare_name
from .resolve_package import _resolve_package
from ._dirsql import plan_extension_path
from .plan_entry_path import _plan_entry_path


def resolve_extension_path(path, base, resolve_relative):
Expand All @@ -28,11 +27,4 @@ def resolve_extension_path(path, base, resolve_relative):
against ``base`` (config-file semantics); when false it is returned verbatim
(programmatic semantics).
"""
if not is_bare_name(path):
if resolve_relative and not os.path.isabs(path):
return os.path.join(base, path)
return path
local = os.path.join(base, path)
if os.path.isfile(local):
return local
return _resolve_package(path)
return _plan_entry_path(plan_extension_path(path, base, resolve_relative))
62 changes: 18 additions & 44 deletions packages/python/dirsql/resolve_extension_test.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,34 @@
"""Unit tests for `resolve_extension_path`.

The effectful collaborators -- the filesystem probe (`os.path.isfile`) and the
package resolver -- are mocked, so these isolate the ordered probe from any
real package or disk. The core's pure `is_bare_name` stays real.
Both collaborators are mocked: the core's planner and the entry-to-path step
that owns the filesystem probe and the package lookup.
"""

from unittest import mock

import dirsql.resolve_extension as mod


def describe_path_looking_values():
def it_makes_a_relative_path_absolute_when_resolve_relative():
out = mod.resolve_extension_path("ext/a.so", base="/cfg", resolve_relative=True)
assert out == "/cfg/ext/a.so"

def it_preserves_an_absolute_path_when_resolve_relative():
out = mod.resolve_extension_path(
"/abs/a.so", base="/cfg", resolve_relative=True
)
assert out == "/abs/a.so"

def it_returns_a_path_verbatim_when_not_resolve_relative():
out = mod.resolve_extension_path(
"relative/a.so", base="/cfg", resolve_relative=False
)
assert out == "relative/a.so"


def describe_bare_names():
def it_uses_a_same_named_local_file_when_present():
def describe_resolve_extension_path():
def it_plans_through_the_core_and_resolves_the_planned_entry():
entry = object()
with (
mock.patch.object(mod.os.path, "isfile", return_value=True) as isfile,
mock.patch.object(mod, "_resolve_package") as resolve_package,
):
out = mod.resolve_extension_path("vec", base="/cfg", resolve_relative=True)
assert out == "/cfg/vec"
isfile.assert_called_once_with("/cfg/vec")
resolve_package.assert_not_called()

def it_resolves_the_package_when_no_local_file_shadows_it():
with (
mock.patch.object(mod.os.path, "isfile", return_value=False),
mock.patch.object(mod, "plan_extension_path", return_value=entry) as plan,
mock.patch.object(
mod, "_resolve_package", return_value="/site/vec/vec0.so"
) as resolve_package,
mod, "_plan_entry_path", return_value="/site/vec/vec0.so"
) as entry_path,
):
out = mod.resolve_extension_path("vec", base="/cfg", resolve_relative=True)
out = mod.resolve_extension_path("vec", base="/cfg", resolve_relative=False)

assert out == "/site/vec/vec0.so"
resolve_package.assert_called_once_with("vec")
plan.assert_called_once_with("vec", "/cfg", False)
entry_path.assert_called_once_with(entry)

def it_probes_the_shadow_file_even_when_not_resolve_relative():
def it_passes_resolve_relative_through_to_the_planner():
with (
mock.patch.object(mod.os.path, "isfile", return_value=True) as isfile,
mock.patch.object(mod, "_resolve_package"),
mock.patch.object(mod, "plan_extension_path") as plan,
mock.patch.object(mod, "_plan_entry_path"),
):
out = mod.resolve_extension_path("vec", base="/cfg", resolve_relative=False)
assert out == "/cfg/vec"
isfile.assert_called_once_with("/cfg/vec")
mod.resolve_extension_path("ext/a.so", base="/cfg", resolve_relative=True)

plan.assert_called_once_with("ext/a.so", "/cfg", True)
7 changes: 7 additions & 0 deletions packages/python/e2e-attestations/claude-1122-ext-cleanup.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"command": "just test-e2e",
"ran_at": 1789742374,
"exit_code": 0,
"commit": "c05317742ecadafeabc1f5de5ccffa75fa983756",
"branch": "claude/1122-ext-cleanup"
}
40 changes: 31 additions & 9 deletions packages/python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,10 +573,19 @@ mod python {
.map_err(|err| err.to_string())
}

/// Whether an extension entry names a package rather than a file.
/// Plan one extension path given outside a config file.
///
/// `base` is the directory a relative path and the bare-name shadow probe
/// resolve against; `resolve_relative` makes a relative path-looking value
/// absolute against it (config semantics) rather than verbatim.
#[pyfunction]
fn is_bare_name(path: &str) -> bool {
extension_resolution::is_bare_name(path, PY_SUFFIXES)
fn plan_extension_path(path: &str, base: &str, resolve_relative: bool) -> PyExtensionPlanEntry {
plan_entry_to_py(extension_resolution::plan_extension_path(
path,
Path::new(base),
resolve_relative,
PY_SUFFIXES,
))
}

#[pymodule]
Expand All @@ -591,7 +600,7 @@ mod python {
m.add_function(wrap_pyfunction!(config_paths_from_argv, m)?)?;
m.add_function(wrap_pyfunction!(plan_config_extensions, m)?)?;
m.add_function(wrap_pyfunction!(select_loadable, m)?)?;
m.add_function(wrap_pyfunction!(is_bare_name, m)?)?;
m.add_function(wrap_pyfunction!(plan_extension_path, m)?)?;
m.add_class::<PyExtensionPlanEntry>()?;
Ok(())
}
Expand Down Expand Up @@ -763,11 +772,24 @@ mod python {
}

#[test]
fn is_bare_name_applies_the_cpython_suffix_list() {
assert!(is_bare_name("sqlite_vec"));
assert!(!is_bare_name("vec0.pyd"));
assert!(is_bare_name("vec0.node"));
assert!(!is_bare_name("ext/vec0"));
fn a_programmatic_path_marshals_as_a_literal_entry() {
let entry = plan_extension_path("ext/vec0.so", "/cwd", false);
assert_eq!(entry.path.as_deref(), Some("ext/vec0.so"));
assert_eq!(entry.package, None);
assert_eq!(
plan_extension_path("ext/vec0.so", "/cwd", true)
.path
.as_deref(),
Some("/cwd/ext/vec0.so")
);
}

#[test]
fn a_programmatic_entry_applies_the_cpython_suffix_list() {
let entry = plan_extension_path("vec0.node", "/cwd", false);
assert_eq!(entry.package.as_deref(), Some("vec0.node"));
assert_eq!(entry.shadow.as_deref(), Some("/cwd/vec0.node"));
assert_eq!(plan_extension_path("vec0.pyd", "/cwd", false).package, None);
}

#[test]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
**Added**

`dirsql::extension_resolution::plan_extension_path` plans a single extension
path given outside a config file, the form a constructor's `extensions=[{path}]`
entry takes: path-looking values stay literal (made absolute against a base only
when asked), bare names become a package lookup with a shadow probe. The Python
and TypeScript SDKs each still carry a port of that probe; they migrate onto
this next.
99 changes: 98 additions & 1 deletion packages/rust/src/extension_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,38 @@ fn spec_is_bare_name(extension: &ExtensionSpec, suffixes: &[&str]) -> bool {
.is_some_and(|path| is_bare_name(path, suffixes))
}

/// Plan one extension path given outside a config file.
///
/// A constructor's `extensions=[{path}]` entry takes the same ordered probe as
/// a config entry: a path-looking value is literal, made absolute against
/// `base` only when `resolve_relative` is set (config semantics; programmatic
/// entries keep a relative path verbatim), and a bare name is a package whose
/// shadow probe sits under `base`. The entry's `entrypoint` travels with the
/// caller, so the returned entry carries none.
pub fn plan_extension_path(
path: &str,
base: &Path,
resolve_relative: bool,
suffixes: &[&str],
) -> PlanEntry {
if is_bare_name(path, suffixes) {
return PlanEntry::Package {
name: path.to_owned(),
shadow: base.join(path),
entrypoint: None,
};
}
PlanEntry::Literal {
// `join` yields an absolute path verbatim.
path: if resolve_relative {
base.join(path)
} else {
PathBuf::from(path)
},
entrypoint: None,
}
}

/// Pick the single loadable file for package `name` out of a host's listing.
///
/// `dirs` are the package's directories (used only for the not-found message);
Expand Down Expand Up @@ -209,7 +241,8 @@ pub fn select_loadable(
#[cfg(test)]
mod tests {
use super::{
ConfigSource, PlanEntry, SelectError, is_bare_name, plan_config_extensions, select_loadable,
ConfigSource, PlanEntry, SelectError, is_bare_name, plan_config_extensions,
plan_extension_path, select_loadable,
};
use std::path::{Path, PathBuf};

Expand Down Expand Up @@ -357,6 +390,70 @@ mod tests {
);
}

#[test]
fn a_programmatic_relative_path_is_made_absolute_only_when_asked() {
let base = Path::new("/cwd");
assert_eq!(
plan_extension_path("ext/vec0.so", base, true, PY),
PlanEntry::Literal {
path: PathBuf::from("/cwd/ext/vec0.so"),
entrypoint: None,
}
);
assert_eq!(
plan_extension_path("ext/vec0.so", base, false, PY),
PlanEntry::Literal {
path: PathBuf::from("ext/vec0.so"),
entrypoint: None,
}
);
}

#[test]
fn a_programmatic_absolute_path_is_left_alone() {
assert_eq!(
plan_extension_path("/opt/vec0.so", Path::new("/cwd"), true, PY),
PlanEntry::Literal {
path: PathBuf::from("/opt/vec0.so"),
entrypoint: None,
}
);
}

#[test]
fn a_programmatic_bare_name_plans_a_package_shadowed_by_base() {
for resolve_relative in [true, false] {
assert_eq!(
plan_extension_path("sqlite_vec", Path::new("/cwd"), resolve_relative, PY),
PlanEntry::Package {
name: "sqlite_vec".into(),
shadow: PathBuf::from("/cwd/sqlite_vec"),
entrypoint: None,
}
);
}
}

#[test]
fn a_programmatic_entry_uses_the_hosts_own_suffix_list() {
let base = Path::new("/cwd");
assert_eq!(
plan_extension_path("vec0.node", base, true, PY),
PlanEntry::Package {
name: "vec0.node".into(),
shadow: PathBuf::from("/cwd/vec0.node"),
entrypoint: None,
}
);
assert_eq!(
plan_extension_path("vec0.node", base, true, NODE),
PlanEntry::Literal {
path: PathBuf::from("/cwd/vec0.node"),
entrypoint: None,
}
);
}

#[test]
fn select_loadable_returns_the_single_matching_file() {
let dir = PathBuf::from("/site-packages/sqlite_vec");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
**Changed**

- `resolveExtensionPath` now plans through the core (the addon's
`planExtensionPath`) instead of re-implementing the ordered probe, so a
programmatic `extensions: [{ path }]` entry is classified by the same code as
a config entry. `require.resolve` package lookup is unchanged, and so is the
resolved path. The internal `isBareName` addon export is gone, replaced by
`planExtensionPath`. No change to the published API. (#1122)
7 changes: 7 additions & 0 deletions packages/ts/e2e-attestations/claude-1122-ext-cleanup.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"command": "pnpm test:e2e",
"ran_at": 1789742406,
"exit_code": 0,
"commit": "87bcdafb0c96eefcfb639f7fedb31b97a475c8f9",
"branch": "claude/1122-ext-cleanup"
}
Loading
Loading