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
8 changes: 7 additions & 1 deletion PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,13 @@ argv order and resolve the whole set via the plural shared resolver
(`resolve_configs_extension_specs` / `resolveConfigsExtensionSpecs`) — the
Python fix landed in [#756](https://github.com/thekevinscott/dirsql/pull/756),
the Node fix in [#758](https://github.com/thekevinscott/dirsql/pull/758)
(restoring parity). Python additionally feeds discovery-injected plugin
(restoring parity). Since
[#1078](https://github.com/thekevinscott/dirsql/issues/1078) the scan itself
is one core function, `dirsql::launcher::config_paths_from_argv`
(`#[doc(hidden)]`, not a public Rust API), reached through each binding
(`dirsql._dirsql.config_paths_from_argv` / the addon's `configPathsFromArgv`),
so the two launchers agree on flag spellings and the `./.dirsql.toml` default
by construction. Python additionally feeds discovery-injected plugin
fragments through this path; Node has no plugin discovery (intentional drift,
[#529](https://github.com/thekevinscott/dirsql/issues/529)), so its scan
covers user-passed flags only.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**Changed**

- **The launcher's config-path scan now runs in the shared core.** Before clap sees argv, the pip launcher still walks it for every `--config` / `-c` value (all five spellings) so a bare package-name `[[dirsql.extension]]` entry can be resolved through `importlib` — but the walk itself is now `dirsql::launcher::config_paths_from_argv`, reached through the private `dirsql._dirsql.config_paths_from_argv` binding; the Python copy is deleted. No user-observable change: same flags, same paths, same default of `./.dirsql.toml`. (#1078, #1072)
2 changes: 2 additions & 0 deletions packages/python/dirsql/_dirsql.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,5 @@ class DirSQL:
def query(self, sql: str) -> list[Row]: ...
def _start_watcher(self) -> None: ...
def _poll_events(self, timeout_ms: int) -> list[RowEvent]: ...

def config_paths_from_argv(argv: list[str]) -> list[str]: ...
28 changes: 0 additions & 28 deletions packages/python/dirsql/cli/config_paths_from_argv.py

This file was deleted.

70 changes: 0 additions & 70 deletions packages/python/dirsql/cli/config_paths_from_argv_test.py

This file was deleted.

2 changes: 1 addition & 1 deletion packages/python/dirsql/cli/resolve_config_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

from __future__ import annotations

from .._dirsql import config_paths_from_argv
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
# pre-resolved here (that path resolves via the handshake).
Expand Down
3 changes: 3 additions & 0 deletions packages/python/dirsql/cli/resolve_config_extensions_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ def it_imports_the_shared_resolver_from_the_plural_module():
== "dirsql.resolve_configs_extension_specs"
)

def it_reaches_the_argv_scan_through_the_core_binding():
assert rce.config_paths_from_argv.__module__ == "dirsql._dirsql"

def it_passes_init_through_untouched():
argv = ["init", "--root", "."]
with _patch([{"path": "R:pkg", "entrypoint": None}]) as (scan, resolver):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"command": "just test-e2e",
"ran_at": 1788979577,
"exit_code": 0,
"commit": "ae4f94c0b871cd766d45be67204c8a523a58c707",
"branch": "claude/1078-argv-config-paths-core"
}
16 changes: 16 additions & 0 deletions packages/python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,13 @@ mod python {
py.detach(|| dirsql::cli::run_cli(full))
}

/// The config paths a launcher must inspect for package-name extensions;
/// see [`dirsql::launcher::config_paths_from_argv`].
#[pyfunction]
fn config_paths_from_argv(argv: Vec<String>) -> Vec<String> {
dirsql::launcher::config_paths_from_argv(&argv)
}

#[pymodule]
#[pyo3(name = "_dirsql")]
fn py_dirsql_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
Expand All @@ -471,6 +478,7 @@ mod python {
m.add_class::<PyRowEvent>()?;
m.add_class::<PyScanFailure>()?;
m.add_function(wrap_pyfunction!(run_cli, m)?)?;
m.add_function(wrap_pyfunction!(config_paths_from_argv, m)?)?;
Ok(())
}

Expand Down Expand Up @@ -553,6 +561,14 @@ mod python {
assert_eq!(OnFileError("bad".to_string()).to_string(), "bad");
}

#[test]
fn config_paths_from_argv_forwards_to_the_core_scan() {
assert_eq!(
config_paths_from_argv(vec!["-c".into(), "a.toml".into()]),
["a.toml"]
);
}

#[test]
fn int_overflow_message_names_the_value() {
let m = int_overflow_message("9223372036854775808");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**Added**

- **The core now owns the argv scan that finds a launcher's config paths.** `dirsql::launcher::config_paths_from_argv` returns every `--config` / `-c` value an invocation names (all five spellings: `--config X`, `--config=X`, `-c X`, `-c=X`, `-cX`), or `./.dirsql.toml` when argv names none. The pip and npm launchers need those paths *before* the core parses argv, because a config's `[[dirsql.extension]]` entry may name an extension by bare package name and only the interpreter can resolve that — but locating the config files is language-neutral, so all three copies of the scan collapse into one. The module is `#[doc(hidden)]`: it exists for the bindings, not as a stable Rust API. (#1078, #1072)
168 changes: 168 additions & 0 deletions packages/rust/src/launcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//! Argv scanning the language launchers share.
//!
//! The pip and npm launchers must know which config files an invocation names
//! *before* the core parses argv: a config's `[[dirsql.extension]]` entry may
//! name an extension by bare package name, and resolving that needs the
//! interpreter the launcher has and this crate does not. Everything about
//! locating those config paths is language-neutral, so it lives here and each
//! launcher reaches it through its binding rather than reimplementing it.

/// The config a launcher inspects when argv names none.
const DEFAULT_CONFIG_PATH: &str = "./.dirsql.toml";

/// Every config value in `argv`, in order (`--config X`, `--config=X`,
/// `-c X`, `-c=X`, `-cX`), or [`DEFAULT_CONFIG_PATH`] when argv names none.
///
/// The scan is deliberately looser than clap: it runs before argv is parsed,
/// only to find files to read extensions out of, so a bare trailing `-c`
/// yields an empty string rather than an error. A malformed invocation is
/// clap's to reject once the launcher hands argv over.
pub fn config_paths_from_argv(argv: &[String]) -> Vec<String> {
let mut paths = Vec::new();
let mut args = argv.iter();
while let Some(arg) = args.next() {
if arg == "--config" || arg == "-c" {
paths.push(args.next().cloned().unwrap_or_default());
} else if let Some(value) = arg.strip_prefix("--config=") {
paths.push(value.to_string());
} else if let Some(value) = arg.strip_prefix("-c") {
paths.push(value.strip_prefix('=').unwrap_or(value).to_string());
}
}
if paths.is_empty() {
paths.push(DEFAULT_CONFIG_PATH.to_string());
}
paths
}

#[cfg(test)]
mod tests {
use super::config_paths_from_argv;

fn scan(argv: &[&str]) -> Vec<String> {
config_paths_from_argv(&argv.iter().map(|a| (*a).to_string()).collect::<Vec<_>>())
}

#[test]
fn reads_the_long_config_form() {
assert_eq!(
scan(&["--config", "/cfg/.dirsql.toml"]),
["/cfg/.dirsql.toml"]
);
}

#[test]
fn reads_the_long_config_equals_form() {
assert_eq!(scan(&["--config=/c/.dirsql.toml"]), ["/c/.dirsql.toml"]);
}

#[test]
fn reads_the_short_c_form() {
assert_eq!(scan(&["-c", "/frag/dirsql.toml"]), ["/frag/dirsql.toml"]);
}

#[test]
fn reads_the_short_c_equals_form() {
assert_eq!(scan(&["-c=/frag/dirsql.toml"]), ["/frag/dirsql.toml"]);
}

#[test]
fn reads_the_short_c_attached_form() {
assert_eq!(scan(&["-c/frag/dirsql.toml"]), ["/frag/dirsql.toml"]);
}

#[test]
fn collects_every_config_flag_in_argv_order() {
assert_eq!(
scan(&[
"-c",
"a.toml",
"--config",
"b.toml",
"--config=c.toml",
"-cd.toml"
]),
["a.toml", "b.toml", "c.toml", "d.toml"]
);
}

#[test]
fn reads_the_config_value_at_any_argv_position() {
assert_eq!(scan(&["-v", "--config", "/x/y", "tail"]), ["/x/y"]);
}

#[test]
fn collects_a_discovery_injected_fragment_rather_than_the_default() {
assert_eq!(
scan(&[
"query",
"SELECT 1",
"--include-default",
"-c",
"/frag/dirsql.toml"
]),
["/frag/dirsql.toml"]
);
}

#[test]
fn consumes_a_config_value_that_looks_like_a_flag() {
assert_eq!(scan(&["--config", "-cx.toml"]), ["-cx.toml"]);
}

#[test]
fn consumes_a_config_value_that_is_itself_a_config_flag() {
assert_eq!(scan(&["-c", "-c", "x"]), ["-c"]);
}

#[test]
fn does_not_mistake_other_short_flags_for_config() {
assert_eq!(scan(&["-v", "-x", "val"]), ["./.dirsql.toml"]);
}

#[test]
fn does_not_mistake_other_long_flags_for_config() {
assert_eq!(scan(&["--a", "val"]), ["./.dirsql.toml"]);
}

#[test]
fn does_not_mistake_a_config_prefixed_long_flag_for_config() {
assert_eq!(scan(&["--configuration", "val"]), ["./.dirsql.toml"]);
}

#[test]
fn defaults_when_no_config_flag_is_given() {
assert_eq!(scan(&["--port", "9000"]), ["./.dirsql.toml"]);
}

#[test]
fn defaults_for_an_empty_argv() {
assert_eq!(scan(&[]), ["./.dirsql.toml"]);
}

#[test]
fn treats_a_bare_trailing_long_config_as_an_empty_path() {
assert_eq!(scan(&["--config"]), [""]);
}

#[test]
fn treats_a_bare_trailing_short_c_as_an_empty_path() {
assert_eq!(scan(&["-c"]), [""]);
}

#[test]
fn keeps_a_bare_trailing_short_c_after_an_earlier_config() {
assert_eq!(scan(&["--config", "a.toml", "-c"]), ["a.toml", ""]);
}

#[test]
fn reads_an_empty_value_from_the_equals_forms() {
assert_eq!(scan(&["--config="]), [""]);
assert_eq!(scan(&["-c="]), [""]);
}

#[test]
fn does_not_treat_a_double_dash_as_end_of_options() {
assert_eq!(scan(&["--", "-c", "a.toml"]), ["a.toml"]);
}
}
2 changes: 2 additions & 0 deletions packages/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub mod functions;
#[doc(hidden)]
pub mod infer;
#[doc(hidden)]
pub mod launcher;
#[doc(hidden)]
pub mod matcher;
#[doc(hidden)]
pub mod parsed_cache;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
**Changed**

- **The launcher's config-path scan now runs in the shared core.** Before clap sees argv, the npm launcher still walks it for every `--config` / `-c` value (all five spellings) so a bare package-name `[[dirsql.extension]]` entry can be resolved through `require.resolve` — but the walk itself is now `dirsql::launcher::config_paths_from_argv`, reached through the native addon's `configPathsFromArgv`; the TypeScript copy is deleted. No user-observable change: same flags, same paths, same default of `./.dirsql.toml`. (#1078, #1072)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"command": "pnpm test:e2e",
"ran_at": 1788979666,
"exit_code": 0,
"commit": "8db47b0cdedb76fee4f120d7fe18bc39bbb405ab",
"branch": "claude/1078-argv-config-paths-core"
}
Loading
Loading