dirsql 0.3.16__tar.gz → 0.3.18__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dirsql-0.3.16 → dirsql-0.3.18}/Cargo.lock +1 -1
- {dirsql-0.3.16 → dirsql-0.3.18}/PKG-INFO +1 -1
- dirsql-0.3.18/dirsql/cli/interpret/__init__.py +17 -0
- dirsql-0.3.18/dirsql/cli/interpret/dispatch_extract.py +41 -0
- dirsql-0.3.18/dirsql/cli/interpret/dispatch_extract_test.py +92 -0
- dirsql-0.3.18/dirsql/cli/interpret/load_app.py +32 -0
- dirsql-0.3.18/dirsql/cli/interpret/load_app_test.py +85 -0
- dirsql-0.3.18/dirsql/cli/interpret/run.py +66 -0
- dirsql-0.3.18/dirsql/cli/interpret/run_test.py +172 -0
- dirsql-0.3.18/dirsql/cli/interpret/write_message.py +18 -0
- dirsql-0.3.18/dirsql/cli/interpret/write_message_test.py +30 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/cli/main.py +15 -3
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/cli/main_test.py +45 -2
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/Cargo.toml +1 -1
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/src/lib.rs +15 -1
- dirsql-0.3.18/packages/python/tests/integration/__fixtures__/data/a/meta.json +1 -0
- dirsql-0.3.18/packages/python/tests/integration/__fixtures__/data/b/meta.json +1 -0
- dirsql-0.3.18/packages/python/tests/integration/__fixtures__/dirsql.config.py +29 -0
- dirsql-0.3.18/packages/python/tests/integration/__fixtures__/interpret/data/a/meta.json +1 -0
- dirsql-0.3.18/packages/python/tests/integration/__fixtures__/interpret/data/b/meta.json +1 -0
- dirsql-0.3.18/packages/python/tests/integration/__fixtures__/interpret/dirsql.config.py +27 -0
- dirsql-0.3.18/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_no_app.py +3 -0
- dirsql-0.3.18/packages/python/tests/integration/__fixtures__/interpret/dirsql.config_raises.py +21 -0
- dirsql-0.3.18/packages/python/tests/integration/interpret_subprocess.py +96 -0
- dirsql-0.3.18/packages/python/tests/integration/test_interpret.py +145 -0
- dirsql-0.3.18/packages/python/tests/integration/test_native_config.py +100 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/bin/dirsql.rs +67 -1
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/cli/mod.rs +1 -0
- dirsql-0.3.18/packages/rust/src/cli/native_config.rs +694 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/pyproject.toml +4 -5
- {dirsql-0.3.16 → dirsql-0.3.18}/Cargo.toml +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/README.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/__init__.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/_async.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/_dirsql.pyi +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/cli/__init__.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/cli/binary_path.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/cli/binary_path_test.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/cli/is_windows.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/cli/is_windows_test.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/py.typed +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/resolve_config.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/resolve_config_test.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/dirsql/test_async.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/AGENTS.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/api/index.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/cli/config.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/cli/http-api.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/cli/index.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/cli/init.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/cli/server.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/getting-started.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/guide/async.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/guide/crdt.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/guide/persistence.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/guide/querying.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/guide/tables.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/guide/watching.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/index.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/migrations.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/package.json +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/playwright.config.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/docs/vitest.config.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/README.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/conftest.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/.claude/CLAUDE.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/.vitepress/config.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/.vitepress/theme/index.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/.vitepress/theme/lang.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/AGENTS.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/api/index.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/cli/config.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/cli/http-api.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/cli/index.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/cli/init.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/cli/server.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/getting-started.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/guide/async.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/guide/crdt.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/guide/persistence.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/guide/querying.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/guide/tables.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/guide/watching.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/index.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/migrations.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/package.json +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/playwright.config.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/pnpm-lock.yaml +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/pnpm-workspace.yaml +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/tests/integration/home.spec.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/tests/integration/language-flag.spec.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/tests/unit/config.test.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/tests/unit/lang.test.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/docs/vitest.config.ts +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/tests/__init__.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/tests/conftest.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/tests/e2e/__init__.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/tests/integration/__init__.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/tests/integration/test_async_dirsql.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/tests/integration/test_binding.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/tests/integration/test_dirsql.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/tests/integration/test_docs_examples.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/tests/integration/test_docs_gaps.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/tests/integration/test_from_config.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/tests/integration/test_persist.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/python/tests/integration/test_serialization.py +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/Cargo.toml +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/README.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/benches/db_bench.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/benches/differ_bench.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/benches/matcher_bench.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/benches/scanner_bench.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/api/index.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/cli/config.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/cli/http-api.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/cli/index.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/cli/init.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/cli/server.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/getting-started.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/guide/async.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/guide/crdt.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/guide/persistence.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/guide/querying.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/guide/tables.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/guide/watching.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/index.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/docs/migrations.md +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/cli/init.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/cli/router.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/cli/serialize.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/cli/server.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/config.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/db.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/differ.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/lib.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/matcher.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/persist.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/scanner.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/src/watcher.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/tests/async_sdk.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/tests/cli_e2e.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/tests/cli_integration.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/tests/docs_examples.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/tests/docs_gaps.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/tests/from_config.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/tests/init_e2e.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/tests/init_integration.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/tests/persist.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/tests/readonly_query.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/tests/sdk.rs +0 -0
- {dirsql-0.3.16 → dirsql-0.3.18}/packages/rust/tests/serialization.rs +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""`dirsql interpret <config>` -- long-running native config helper (#196).
|
|
2
|
+
|
|
3
|
+
Loads a Python config file, takes its module-level ``app = DirSQL(...)``,
|
|
4
|
+
and serves ``extract`` requests over NDJSON on stdin/stdout. One line in,
|
|
5
|
+
one line out, sequential.
|
|
6
|
+
|
|
7
|
+
See the individual submodules for behavior:
|
|
8
|
+
- ``run`` -- subcommand entry point invoked from ``dirsql.cli.main``
|
|
9
|
+
- ``load_app`` -- importlib-based config loader
|
|
10
|
+
- ``write_message`` -- single-line NDJSON writer
|
|
11
|
+
- ``dispatch_extract`` -- per-request handler
|
|
12
|
+
|
|
13
|
+
No top-level re-exports: the function is at ``.run.run``. Re-exporting
|
|
14
|
+
``run`` at the package level would shadow the submodule (so
|
|
15
|
+
``from . import run`` in tests would resolve to the function and break
|
|
16
|
+
``patch.object(run_module, "load_app", ...)``).
|
|
17
|
+
"""
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Single-request handler for the `dirsql interpret` helper.
|
|
2
|
+
|
|
3
|
+
Given a parsed request line and a name->table lookup, call the user's
|
|
4
|
+
``extract`` callback and return the NDJSON response payload the loop
|
|
5
|
+
will write back to stdout.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def dispatch_extract(
|
|
14
|
+
req: dict[str, Any],
|
|
15
|
+
tables: dict[str, Any],
|
|
16
|
+
) -> dict[str, Any]:
|
|
17
|
+
"""Build the `{"type": "result", ...}` response for one extract request.
|
|
18
|
+
|
|
19
|
+
- Unknown table name -> ``ok: false`` with a name-bearing error.
|
|
20
|
+
- User's ``extract`` raising -> ``ok: false`` with ``str(exc)``.
|
|
21
|
+
- Success -> ``ok: true`` with the row list the callback returned.
|
|
22
|
+
|
|
23
|
+
``req["id"]`` is echoed back unchanged so the caller can correlate
|
|
24
|
+
request and response.
|
|
25
|
+
"""
|
|
26
|
+
rid = req.get("id")
|
|
27
|
+
name = req.get("table")
|
|
28
|
+
path = req.get("path")
|
|
29
|
+
table = tables.get(name) if isinstance(name, str) else None
|
|
30
|
+
if table is None:
|
|
31
|
+
return {
|
|
32
|
+
"type": "result",
|
|
33
|
+
"id": rid,
|
|
34
|
+
"ok": False,
|
|
35
|
+
"error": f"unknown table: {name!r}",
|
|
36
|
+
}
|
|
37
|
+
try:
|
|
38
|
+
rows = table.extract(path)
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
return {"type": "result", "id": rid, "ok": False, "error": str(exc)}
|
|
41
|
+
return {"type": "result", "id": rid, "ok": True, "rows": rows}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Unit tests for `dispatch_extract`."""
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
|
|
5
|
+
from .dispatch_extract import dispatch_extract
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _table(extract):
|
|
9
|
+
return SimpleNamespace(extract=extract)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def describe_dispatch_extract():
|
|
13
|
+
def it_returns_ok_true_with_the_rows_extract_produced():
|
|
14
|
+
tables = {"papers": _table(lambda p: [{"title": p}])}
|
|
15
|
+
out = dispatch_extract(
|
|
16
|
+
{"type": "extract", "id": 1, "table": "papers", "path": "/x.json"},
|
|
17
|
+
tables,
|
|
18
|
+
)
|
|
19
|
+
assert out == {
|
|
20
|
+
"type": "result",
|
|
21
|
+
"id": 1,
|
|
22
|
+
"ok": True,
|
|
23
|
+
"rows": [{"title": "/x.json"}],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def it_echoes_the_request_id_verbatim_on_success():
|
|
27
|
+
tables = {"papers": _table(lambda _p: [])}
|
|
28
|
+
out = dispatch_extract(
|
|
29
|
+
{"type": "extract", "id": 99, "table": "papers", "path": "/x.json"},
|
|
30
|
+
tables,
|
|
31
|
+
)
|
|
32
|
+
assert out["id"] == 99
|
|
33
|
+
|
|
34
|
+
def it_returns_ok_false_with_the_table_name_when_the_table_is_unknown():
|
|
35
|
+
out = dispatch_extract(
|
|
36
|
+
{"type": "extract", "id": 5, "table": "ghost", "path": "/x"},
|
|
37
|
+
tables={"papers": _table(lambda _p: [])},
|
|
38
|
+
)
|
|
39
|
+
assert out["type"] == "result"
|
|
40
|
+
assert out["id"] == 5
|
|
41
|
+
assert out["ok"] is False
|
|
42
|
+
assert "ghost" in out["error"]
|
|
43
|
+
|
|
44
|
+
def it_echoes_the_request_id_verbatim_on_unknown_table():
|
|
45
|
+
out = dispatch_extract(
|
|
46
|
+
{"type": "extract", "id": 17, "table": "ghost", "path": "/x"},
|
|
47
|
+
tables={},
|
|
48
|
+
)
|
|
49
|
+
assert out["id"] == 17
|
|
50
|
+
|
|
51
|
+
def it_returns_ok_false_with_the_exception_message_when_extract_raises():
|
|
52
|
+
def boom(_p):
|
|
53
|
+
raise ValueError("synthetic")
|
|
54
|
+
|
|
55
|
+
out = dispatch_extract(
|
|
56
|
+
{"type": "extract", "id": 7, "table": "papers", "path": "/x"},
|
|
57
|
+
{"papers": _table(boom)},
|
|
58
|
+
)
|
|
59
|
+
assert out == {
|
|
60
|
+
"type": "result",
|
|
61
|
+
"id": 7,
|
|
62
|
+
"ok": False,
|
|
63
|
+
"error": "synthetic",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def it_passes_the_request_path_to_the_extract_callback():
|
|
67
|
+
seen: list[str] = []
|
|
68
|
+
|
|
69
|
+
def capture(p):
|
|
70
|
+
seen.append(p)
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
dispatch_extract(
|
|
74
|
+
{"type": "extract", "id": 1, "table": "t", "path": "/abs/x.json"},
|
|
75
|
+
{"t": _table(capture)},
|
|
76
|
+
)
|
|
77
|
+
assert seen == ["/abs/x.json"]
|
|
78
|
+
|
|
79
|
+
def it_treats_a_missing_table_field_as_unknown_table():
|
|
80
|
+
out = dispatch_extract(
|
|
81
|
+
{"type": "extract", "id": 1, "path": "/x"},
|
|
82
|
+
tables={"t": _table(lambda _p: [])},
|
|
83
|
+
)
|
|
84
|
+
assert out["ok"] is False
|
|
85
|
+
assert "None" in out["error"] or "none" in out["error"].lower()
|
|
86
|
+
|
|
87
|
+
def it_passes_through_a_missing_id_field_as_none():
|
|
88
|
+
out = dispatch_extract(
|
|
89
|
+
{"type": "extract", "table": "ghost", "path": "/x"},
|
|
90
|
+
tables={},
|
|
91
|
+
)
|
|
92
|
+
assert out["id"] is None
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""importlib-based config loader for the `dirsql interpret` helper.
|
|
2
|
+
|
|
3
|
+
The user's config is a regular Python module that constructs a
|
|
4
|
+
``DirSQL`` instance and assigns it to a top-level ``app`` name. We
|
|
5
|
+
import it via ``importlib.util.spec_from_file_location`` so the user
|
|
6
|
+
doesn't have to put their config on ``sys.path``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import importlib.util
|
|
12
|
+
import os
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_app(config_path: str) -> Any:
|
|
17
|
+
"""Import ``config_path`` and return its ``app`` attribute.
|
|
18
|
+
|
|
19
|
+
Raises ``ImportError`` if the file can't be loaded as a module, and
|
|
20
|
+
``AttributeError`` if the module loads but defines no ``app``.
|
|
21
|
+
"""
|
|
22
|
+
abs_path = os.path.abspath(config_path)
|
|
23
|
+
spec = importlib.util.spec_from_file_location("_dirsql_user_config", abs_path)
|
|
24
|
+
if spec is None or spec.loader is None:
|
|
25
|
+
raise ImportError(f"could not load config: {config_path}")
|
|
26
|
+
module = importlib.util.module_from_spec(spec)
|
|
27
|
+
spec.loader.exec_module(module)
|
|
28
|
+
if not hasattr(module, "app"):
|
|
29
|
+
raise AttributeError(
|
|
30
|
+
f"{config_path}: module must define a top-level `app = DirSQL(...)`"
|
|
31
|
+
)
|
|
32
|
+
return module.app
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Unit tests for `load_app`."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from . import load_app as load_app_mod
|
|
11
|
+
from .load_app import load_app
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def describe_load_app():
|
|
15
|
+
def it_returns_the_app_attribute_of_the_loaded_module(tmp_path: Path):
|
|
16
|
+
config = tmp_path / "config.py"
|
|
17
|
+
config.write_text("app = {'sentinel': 'value'}\n")
|
|
18
|
+
result = load_app(str(config))
|
|
19
|
+
assert result == {"sentinel": "value"}
|
|
20
|
+
|
|
21
|
+
def it_resolves_relative_paths_against_the_cwd(tmp_path: Path):
|
|
22
|
+
config = tmp_path / "config.py"
|
|
23
|
+
config.write_text("app = 42\n")
|
|
24
|
+
prev = os.getcwd()
|
|
25
|
+
try:
|
|
26
|
+
os.chdir(tmp_path)
|
|
27
|
+
assert load_app("config.py") == 42
|
|
28
|
+
finally:
|
|
29
|
+
os.chdir(prev)
|
|
30
|
+
|
|
31
|
+
def it_raises_import_error_when_the_file_does_not_exist(tmp_path: Path):
|
|
32
|
+
missing = tmp_path / "nope.py"
|
|
33
|
+
with pytest.raises((ImportError, FileNotFoundError)):
|
|
34
|
+
load_app(str(missing))
|
|
35
|
+
|
|
36
|
+
def it_raises_attribute_error_with_a_path_aware_message_when_app_is_missing(
|
|
37
|
+
tmp_path: Path,
|
|
38
|
+
):
|
|
39
|
+
config = tmp_path / "config.py"
|
|
40
|
+
config.write_text("not_the_app = 1\n")
|
|
41
|
+
with pytest.raises(AttributeError, match="must define a top-level `app"):
|
|
42
|
+
load_app(str(config))
|
|
43
|
+
|
|
44
|
+
def it_surfaces_exceptions_raised_during_module_exec(tmp_path: Path):
|
|
45
|
+
config = tmp_path / "config.py"
|
|
46
|
+
config.write_text("raise RuntimeError('synthetic boom')\n")
|
|
47
|
+
with pytest.raises(RuntimeError, match="synthetic boom"):
|
|
48
|
+
load_app(str(config))
|
|
49
|
+
|
|
50
|
+
def it_raises_import_error_when_spec_from_file_location_returns_none(
|
|
51
|
+
tmp_path: Path,
|
|
52
|
+
):
|
|
53
|
+
with patch.object(
|
|
54
|
+
load_app_mod.importlib.util,
|
|
55
|
+
"spec_from_file_location",
|
|
56
|
+
return_value=None,
|
|
57
|
+
):
|
|
58
|
+
with pytest.raises(ImportError, match="could not load config"):
|
|
59
|
+
load_app(str(tmp_path / "anything.py"))
|
|
60
|
+
|
|
61
|
+
def it_raises_import_error_when_spec_has_no_loader(tmp_path: Path):
|
|
62
|
+
class _SpecNoLoader:
|
|
63
|
+
loader = None
|
|
64
|
+
|
|
65
|
+
with patch.object(
|
|
66
|
+
load_app_mod.importlib.util,
|
|
67
|
+
"spec_from_file_location",
|
|
68
|
+
return_value=_SpecNoLoader(),
|
|
69
|
+
):
|
|
70
|
+
with pytest.raises(ImportError, match="could not load config"):
|
|
71
|
+
load_app(str(tmp_path / "anything.py"))
|
|
72
|
+
|
|
73
|
+
def it_absolutizes_the_passed_path_before_loading(tmp_path: Path):
|
|
74
|
+
config = tmp_path / "config.py"
|
|
75
|
+
config.write_text("app = 1\n")
|
|
76
|
+
with patch.object(
|
|
77
|
+
importlib.util,
|
|
78
|
+
"spec_from_file_location",
|
|
79
|
+
wraps=importlib.util.spec_from_file_location,
|
|
80
|
+
) as spec_call:
|
|
81
|
+
load_app(str(config))
|
|
82
|
+
# First positional arg after the synthetic module name is the
|
|
83
|
+
# absolute path the loader was asked to read.
|
|
84
|
+
called_path = spec_call.call_args.args[1]
|
|
85
|
+
assert called_path == os.path.abspath(str(config))
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Subcommand entry point for `dirsql interpret <config>`.
|
|
2
|
+
|
|
3
|
+
Glues the per-purpose helpers (`load_app`, `dispatch_extract`,
|
|
4
|
+
`write_message`) into the long-running NDJSON loop:
|
|
5
|
+
|
|
6
|
+
handshake (helper -> caller, once on startup):
|
|
7
|
+
{"type": "config", "state": <vars(app)>}
|
|
8
|
+
|
|
9
|
+
extract request (caller -> helper):
|
|
10
|
+
{"type": "extract", "id": <int>, "table": "<name>", "path": "<abs>"}
|
|
11
|
+
|
|
12
|
+
extract response (helper -> caller):
|
|
13
|
+
{"type": "result", "id": <int>, "ok": true, "rows": [...]}
|
|
14
|
+
{"type": "result", "id": <int>, "ok": false, "error": "<msg>"}
|
|
15
|
+
|
|
16
|
+
Exits 0 when stdin closes; non-zero with a single ``dirsql interpret:``
|
|
17
|
+
line on stderr if the config can't be loaded.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
from .dispatch_extract import dispatch_extract
|
|
26
|
+
from .load_app import load_app
|
|
27
|
+
from .write_message import write_message
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run(argv: list[str]) -> int:
|
|
31
|
+
if len(argv) != 1:
|
|
32
|
+
sys.stderr.write(
|
|
33
|
+
f"dirsql interpret: expected one config path, got {len(argv)}\n"
|
|
34
|
+
)
|
|
35
|
+
return 1
|
|
36
|
+
config_path = argv[0]
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
app = load_app(config_path)
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
sys.stderr.write(f"dirsql interpret: {exc}\n")
|
|
42
|
+
return 1
|
|
43
|
+
|
|
44
|
+
# Name comes from `dirsql::db::parse_table_name` -- the canonical
|
|
45
|
+
# core parser, surfaced via PyO3 on `Table.name` (#196). No regex
|
|
46
|
+
# duplication on the Python side. Tables with a name the parser
|
|
47
|
+
# couldn't extract are skipped here; the malformed DDL would also
|
|
48
|
+
# surface as a `DirSqlError::Ddl` during the SDK scan, which
|
|
49
|
+
# `await app.ready()` would re-raise -- not interpret's job to
|
|
50
|
+
# second-guess.
|
|
51
|
+
tables = {t.name: t for t in (app._tables or []) if t.name is not None}
|
|
52
|
+
write_message({"type": "config", "state": vars(app)})
|
|
53
|
+
|
|
54
|
+
for line in sys.stdin:
|
|
55
|
+
stripped = line.strip()
|
|
56
|
+
if not stripped:
|
|
57
|
+
continue
|
|
58
|
+
try:
|
|
59
|
+
req = json.loads(stripped)
|
|
60
|
+
except json.JSONDecodeError:
|
|
61
|
+
continue
|
|
62
|
+
if not isinstance(req, dict) or req.get("type") != "extract":
|
|
63
|
+
continue
|
|
64
|
+
write_message(dispatch_extract(req, tables))
|
|
65
|
+
|
|
66
|
+
return 0
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Unit tests for `run`."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from types import SimpleNamespace
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
from . import run as run_module
|
|
10
|
+
from .run import run
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _fake_table(name: str, extract):
|
|
14
|
+
return SimpleNamespace(name=name, extract=extract)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _fake_app(tables: list, state: dict):
|
|
18
|
+
"""Build a duck-typed `app` -- `_tables` for dispatch, `__dict__`
|
|
19
|
+
handled by overriding `vars`."""
|
|
20
|
+
|
|
21
|
+
class _App:
|
|
22
|
+
_tables = tables
|
|
23
|
+
|
|
24
|
+
a = _App()
|
|
25
|
+
# vars(a) needs to return `state` -- bind it through __dict__.
|
|
26
|
+
a.__dict__.update(state)
|
|
27
|
+
return a
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def describe_run():
|
|
31
|
+
def describe_argv_validation():
|
|
32
|
+
def it_returns_1_when_argv_has_no_config_path():
|
|
33
|
+
fake_stderr = io.StringIO()
|
|
34
|
+
with patch.object(sys, "stderr", fake_stderr):
|
|
35
|
+
assert run([]) == 1
|
|
36
|
+
assert fake_stderr.getvalue().startswith("dirsql interpret:")
|
|
37
|
+
assert "got 0" in fake_stderr.getvalue()
|
|
38
|
+
|
|
39
|
+
def it_returns_1_when_argv_has_multiple_paths():
|
|
40
|
+
fake_stderr = io.StringIO()
|
|
41
|
+
with patch.object(sys, "stderr", fake_stderr):
|
|
42
|
+
assert run(["a.py", "b.py"]) == 1
|
|
43
|
+
assert "got 2" in fake_stderr.getvalue()
|
|
44
|
+
|
|
45
|
+
def describe_load_failure():
|
|
46
|
+
def it_returns_1_and_writes_a_dirsql_prefixed_line_to_stderr():
|
|
47
|
+
fake_stderr = io.StringIO()
|
|
48
|
+
with (
|
|
49
|
+
patch.object(
|
|
50
|
+
run_module,
|
|
51
|
+
"load_app",
|
|
52
|
+
side_effect=AttributeError("missing app"),
|
|
53
|
+
),
|
|
54
|
+
patch.object(sys, "stderr", fake_stderr),
|
|
55
|
+
):
|
|
56
|
+
assert run(["bad.py"]) == 1
|
|
57
|
+
assert fake_stderr.getvalue() == "dirsql interpret: missing app\n"
|
|
58
|
+
|
|
59
|
+
def describe_handshake():
|
|
60
|
+
def it_writes_a_config_message_with_vars_app_as_state():
|
|
61
|
+
app = _fake_app(tables=[], state={"root": "/x", "ignore": []})
|
|
62
|
+
fake_stdin = io.StringIO("") # immediate EOF
|
|
63
|
+
written: list[dict] = []
|
|
64
|
+
with (
|
|
65
|
+
patch.object(run_module, "load_app", return_value=app),
|
|
66
|
+
patch.object(run_module, "write_message", side_effect=written.append),
|
|
67
|
+
patch.object(sys, "stdin", fake_stdin),
|
|
68
|
+
):
|
|
69
|
+
assert run(["good.py"]) == 0
|
|
70
|
+
assert written == [
|
|
71
|
+
{"type": "config", "state": {"root": "/x", "ignore": []}}
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
def describe_extract_loop():
|
|
75
|
+
def it_dispatches_one_extract_request_and_writes_the_response():
|
|
76
|
+
def extract(p):
|
|
77
|
+
return [{"row": p}]
|
|
78
|
+
|
|
79
|
+
app = _fake_app([_fake_table("papers", extract)], state={"x": 1})
|
|
80
|
+
req = {"type": "extract", "id": 1, "table": "papers", "path": "/a"}
|
|
81
|
+
fake_stdin = io.StringIO(json.dumps(req) + "\n")
|
|
82
|
+
written: list[dict] = []
|
|
83
|
+
with (
|
|
84
|
+
patch.object(run_module, "load_app", return_value=app),
|
|
85
|
+
patch.object(run_module, "write_message", side_effect=written.append),
|
|
86
|
+
patch.object(sys, "stdin", fake_stdin),
|
|
87
|
+
):
|
|
88
|
+
assert run(["good.py"]) == 0
|
|
89
|
+
# written[0] is the handshake; written[1] is the response.
|
|
90
|
+
assert written[1] == {
|
|
91
|
+
"type": "result",
|
|
92
|
+
"id": 1,
|
|
93
|
+
"ok": True,
|
|
94
|
+
"rows": [{"row": "/a"}],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
def it_skips_blank_lines_silently():
|
|
98
|
+
app = _fake_app([], state={})
|
|
99
|
+
fake_stdin = io.StringIO("\n\n \n")
|
|
100
|
+
written: list[dict] = []
|
|
101
|
+
with (
|
|
102
|
+
patch.object(run_module, "load_app", return_value=app),
|
|
103
|
+
patch.object(run_module, "write_message", side_effect=written.append),
|
|
104
|
+
patch.object(sys, "stdin", fake_stdin),
|
|
105
|
+
):
|
|
106
|
+
assert run(["good.py"]) == 0
|
|
107
|
+
assert written == [{"type": "config", "state": {}}] # only handshake
|
|
108
|
+
|
|
109
|
+
def it_skips_malformed_json_silently():
|
|
110
|
+
app = _fake_app([], state={})
|
|
111
|
+
fake_stdin = io.StringIO("not json\n{also not\n")
|
|
112
|
+
written: list[dict] = []
|
|
113
|
+
with (
|
|
114
|
+
patch.object(run_module, "load_app", return_value=app),
|
|
115
|
+
patch.object(run_module, "write_message", side_effect=written.append),
|
|
116
|
+
patch.object(sys, "stdin", fake_stdin),
|
|
117
|
+
):
|
|
118
|
+
assert run(["good.py"]) == 0
|
|
119
|
+
assert written == [{"type": "config", "state": {}}]
|
|
120
|
+
|
|
121
|
+
def it_skips_non_extract_messages_silently():
|
|
122
|
+
app = _fake_app([], state={})
|
|
123
|
+
fake_stdin = io.StringIO('{"type": "ping"}\n')
|
|
124
|
+
written: list[dict] = []
|
|
125
|
+
with (
|
|
126
|
+
patch.object(run_module, "load_app", return_value=app),
|
|
127
|
+
patch.object(run_module, "write_message", side_effect=written.append),
|
|
128
|
+
patch.object(sys, "stdin", fake_stdin),
|
|
129
|
+
):
|
|
130
|
+
assert run(["good.py"]) == 0
|
|
131
|
+
assert written == [{"type": "config", "state": {}}]
|
|
132
|
+
|
|
133
|
+
def it_skips_non_dict_json_silently():
|
|
134
|
+
app = _fake_app([], state={})
|
|
135
|
+
fake_stdin = io.StringIO("42\n[]\n")
|
|
136
|
+
written: list[dict] = []
|
|
137
|
+
with (
|
|
138
|
+
patch.object(run_module, "load_app", return_value=app),
|
|
139
|
+
patch.object(run_module, "write_message", side_effect=written.append),
|
|
140
|
+
patch.object(sys, "stdin", fake_stdin),
|
|
141
|
+
):
|
|
142
|
+
assert run(["good.py"]) == 0
|
|
143
|
+
assert written == [{"type": "config", "state": {}}]
|
|
144
|
+
|
|
145
|
+
def it_treats_a_none_tables_attribute_as_empty():
|
|
146
|
+
app = _fake_app(tables=None, state={}) # type: ignore[arg-type]
|
|
147
|
+
fake_stdin = io.StringIO(
|
|
148
|
+
'{"type":"extract","id":1,"table":"x","path":"/y"}\n'
|
|
149
|
+
)
|
|
150
|
+
written: list[dict] = []
|
|
151
|
+
with (
|
|
152
|
+
patch.object(run_module, "load_app", return_value=app),
|
|
153
|
+
patch.object(run_module, "write_message", side_effect=written.append),
|
|
154
|
+
patch.object(sys, "stdin", fake_stdin),
|
|
155
|
+
):
|
|
156
|
+
assert run(["good.py"]) == 0
|
|
157
|
+
# Handshake + unknown-table response.
|
|
158
|
+
assert written[1]["ok"] is False
|
|
159
|
+
assert "x" in written[1]["error"]
|
|
160
|
+
|
|
161
|
+
def describe_exit():
|
|
162
|
+
def it_returns_0_when_stdin_closes_after_a_run():
|
|
163
|
+
app = _fake_app([_fake_table("t", lambda _p: [])], state={})
|
|
164
|
+
fake_stdin = io.StringIO(
|
|
165
|
+
'{"type":"extract","id":1,"table":"t","path":"/a"}\n'
|
|
166
|
+
)
|
|
167
|
+
with (
|
|
168
|
+
patch.object(run_module, "load_app", return_value=app),
|
|
169
|
+
patch.object(run_module, "write_message"),
|
|
170
|
+
patch.object(sys, "stdin", fake_stdin),
|
|
171
|
+
):
|
|
172
|
+
assert run(["good.py"]) == 0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Single-line NDJSON writer for the `dirsql interpret` helper."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def write_message(msg: dict[str, Any]) -> None:
|
|
11
|
+
"""Write one NDJSON line to stdout and flush.
|
|
12
|
+
|
|
13
|
+
The flush is required so the orchestrator on the other side of the
|
|
14
|
+
pipe sees the message immediately; without it the buffered stream
|
|
15
|
+
would deliver lines in batches.
|
|
16
|
+
"""
|
|
17
|
+
sys.stdout.write(json.dumps(msg) + "\n")
|
|
18
|
+
sys.stdout.flush()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Unit tests for `write_message`."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import sys
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from .write_message import write_message
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def describe_write_message():
|
|
11
|
+
def it_serializes_a_dict_as_one_json_line():
|
|
12
|
+
buf = io.StringIO()
|
|
13
|
+
with patch.object(sys, "stdout", buf):
|
|
14
|
+
write_message({"type": "config", "state": {}})
|
|
15
|
+
assert buf.getvalue() == '{"type": "config", "state": {}}\n'
|
|
16
|
+
|
|
17
|
+
def it_appends_a_trailing_newline():
|
|
18
|
+
buf = io.StringIO()
|
|
19
|
+
with patch.object(sys, "stdout", buf):
|
|
20
|
+
write_message({"x": 1})
|
|
21
|
+
assert buf.getvalue().endswith("\n")
|
|
22
|
+
|
|
23
|
+
def it_flushes_stdout_after_writing():
|
|
24
|
+
fake_stdout = io.StringIO()
|
|
25
|
+
with (
|
|
26
|
+
patch.object(sys, "stdout", fake_stdout),
|
|
27
|
+
patch.object(fake_stdout, "flush") as flush,
|
|
28
|
+
):
|
|
29
|
+
write_message({"x": 1})
|
|
30
|
+
flush.assert_called_once()
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"""Console-script entry point. Execs the bundled binary on POSIX,
|
|
2
|
-
subprocesses it on Windows.""
|
|
2
|
+
subprocesses it on Windows. When ``argv[0] == "interpret"`` the
|
|
3
|
+
in-process Python helper handles the subcommand directly so a Rust
|
|
4
|
+
orchestrator can spawn this script for native-language configs (#196)
|
|
5
|
+
without depending on the bundled Rust binary."""
|
|
3
6
|
|
|
4
7
|
from __future__ import annotations
|
|
5
8
|
|
|
@@ -7,13 +10,22 @@ import os
|
|
|
7
10
|
import subprocess
|
|
8
11
|
import sys
|
|
9
12
|
|
|
10
|
-
from
|
|
11
|
-
from
|
|
13
|
+
from .binary_path import binary_path
|
|
14
|
+
from .is_windows import is_windows
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
def main(argv: list[str] | None = None) -> int:
|
|
15
18
|
if argv is None:
|
|
16
19
|
argv = sys.argv[1:]
|
|
20
|
+
|
|
21
|
+
if argv and argv[0] == "interpret":
|
|
22
|
+
from .interpret.run import run
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
return run(argv[1:])
|
|
26
|
+
except KeyboardInterrupt:
|
|
27
|
+
return 130
|
|
28
|
+
|
|
17
29
|
try:
|
|
18
30
|
binary = binary_path()
|
|
19
31
|
except FileNotFoundError as exc:
|
|
@@ -6,8 +6,8 @@ import subprocess
|
|
|
6
6
|
import sys
|
|
7
7
|
from unittest.mock import patch
|
|
8
8
|
|
|
9
|
-
from
|
|
10
|
-
from
|
|
9
|
+
from . import main as main_module
|
|
10
|
+
from .main import main
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class _Completed:
|
|
@@ -85,3 +85,46 @@ def describe_main():
|
|
|
85
85
|
):
|
|
86
86
|
main(argv=None)
|
|
87
87
|
run.assert_called_once_with(["C:/dirsql.exe", "--help"])
|
|
88
|
+
|
|
89
|
+
def describe_when_argv0_is_interpret():
|
|
90
|
+
"""`dirsql interpret <config>` is handled in-process so the Rust
|
|
91
|
+
orchestrator can spawn the launcher for native-language configs
|
|
92
|
+
(#196) without depending on the bundled Rust binary."""
|
|
93
|
+
|
|
94
|
+
def it_dispatches_to_interpret_run_and_returns_its_exit_code():
|
|
95
|
+
# main.py does `from .interpret.run import run` lazily, so
|
|
96
|
+
# patching the `run` function on the submodule is sufficient
|
|
97
|
+
# -- each call resolves the name freshly from the submodule.
|
|
98
|
+
from .interpret import run as run_submod
|
|
99
|
+
|
|
100
|
+
with (
|
|
101
|
+
patch.object(run_submod, "run", return_value=0) as interpret_run,
|
|
102
|
+
patch.object(main_module, "binary_path") as binary_path,
|
|
103
|
+
):
|
|
104
|
+
assert main(["interpret", "config.py"]) == 0
|
|
105
|
+
interpret_run.assert_called_once_with(["config.py"])
|
|
106
|
+
binary_path.assert_not_called()
|
|
107
|
+
|
|
108
|
+
def it_propagates_a_nonzero_interpret_exit():
|
|
109
|
+
from .interpret import run as run_submod
|
|
110
|
+
|
|
111
|
+
with patch.object(run_submod, "run", return_value=2):
|
|
112
|
+
assert main(["interpret", "bad.py"]) == 2
|
|
113
|
+
|
|
114
|
+
def it_returns_130_on_keyboard_interrupt():
|
|
115
|
+
from .interpret import run as run_submod
|
|
116
|
+
|
|
117
|
+
with patch.object(run_submod, "run", side_effect=KeyboardInterrupt()):
|
|
118
|
+
assert main(["interpret", "config.py"]) == 130
|
|
119
|
+
|
|
120
|
+
def it_does_not_intercept_when_interpret_is_not_argv_0():
|
|
121
|
+
# `dirsql --verbose interpret` is the binary's problem, not
|
|
122
|
+
# ours -- the in-process route only fires on the first arg.
|
|
123
|
+
with (
|
|
124
|
+
patch.object(main_module, "binary_path", return_value="/bin/dirsql"),
|
|
125
|
+
patch.object(main_module, "is_windows", return_value=True),
|
|
126
|
+
patch.object(subprocess, "run", return_value=_Completed(0)),
|
|
127
|
+
):
|
|
128
|
+
main(["--verbose", "interpret", "config.py"])
|
|
129
|
+
# binary_path was reached, meaning we did NOT take the
|
|
130
|
+
# interpret shortcut.
|