diff --git a/CHANGES.txt b/CHANGES.txt index 2f454b5..d7376ab 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,7 +4,8 @@ CHANGES 1.0.1 (unreleased) ================== -- Nothing changed yet. +- Allow equals signs in command-line query filter values, such as URLs + containing query parameters. 1.0.0 (2026-10-10) diff --git a/dectate/tests/test_tool.py b/dectate/tests/test_tool.py index 1464605..b9b3d1b 100644 --- a/dectate/tests/test_tool.py +++ b/dectate/tests/test_tool.py @@ -74,6 +74,22 @@ def test_parse_filters_main() -> None: } +@pytest.mark.parametrize( + "value", + [ + "a=b", + "=leading", + "trailing=", + "a=b=c", + "https://example.test/search?q=python&lang=en", + "dGVzdA==", + " a = b ", + ], +) +def test_parse_filters_equals_in_value(value: str) -> None: + assert parse_filters([f"name={value}"]) == {"name": value.strip()} + + def test_parse_filters_error() -> None: with pytest.raises(ToolError): parse_filters(["a"]) @@ -360,6 +376,42 @@ def f() -> None: assert capsys.readouterr().out +def test_query_tool_equals_in_filter_value( + capsys: pytest.CaptureFixture[str], +) -> None: + class FooAction(Action): + def __init__(self, name: str) -> None: + self.name = name + + def identifier(self) -> str: + return self.name + + def perform(self, obj: Any) -> None: + pass + + class MyApp(App): + foo = directive(FooAction) + + @MyApp.foo("https://example.test/?q=python") + def f() -> None: + pass + + @MyApp.foo("https://example.test/?q=rust") + def g() -> None: + pass + + commit(MyApp) + filters = {"name": "https://example.test/?q=python"} + assert [obj for _action, obj in query_app(MyApp, "foo", **filters)] == [f] + + with patch.object(sys, "argv", ["decq", "foo", f"name={filters['name']}"]): + query_tool([MyApp]) + + output = capsys.readouterr().out + assert "https://example.test/?q=python" in output + assert "https://example.test/?q=rust" not in output + + def test_query_tool_with_app_arg() -> None: # Passing --app exercises the `app_classes = args.app` branch. # AnApp is not committed so ToolError is raised -> parser.error -> SystemExit. diff --git a/dectate/tool.py b/dectate/tool.py index 25b3375..2f6b5e3 100644 --- a/dectate/tool.py +++ b/dectate/tool.py @@ -39,6 +39,9 @@ def query_tool(app_classes: Iterable[type[App]]) -> None: $ decq --app=myproject.App foo :param app_classes: a list of :class:`App` subclasses to query by default. + + Filters use ``name=value`` syntax. Only the first equals sign separates + the name and value; additional equals signs are part of the value. """ parser = argparse.ArgumentParser(description="Query Dectate actions") parser.add_argument( @@ -175,7 +178,7 @@ def parse_filters(entries: Iterable[str]) -> dict[str, str]: result: dict[str, str] = {} for entry in entries: try: - name, value = entry.split("=") + name, value = entry.split("=", 1) except ValueError: msg = "Cannot parse query filter, no =." raise ToolError(msg)