From 6584701e14274aebc2e04926ee55c70d4bcf1dd1 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sun, 22 Mar 2026 06:12:21 -0700 Subject: [PATCH] feat: add Router.query, deprecate Router.search Add Router.query property that returns the query string without the leading '?'. Turn Router.search into a deprecated property that emits a DeprecationWarning and delegates to Router.query. Update internal usages and tests to use .query. Fixes #524 --- ...feat-router-query-deprecate-search-plan.md | 34 +++++++++++++++++++ solara/routing.py | 27 +++++++++++++-- solara/test/pytest_plugin.py | 2 +- tests/unit/router_test.py | 4 ++- 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 docs/plans/2026-03-22-002-feat-router-query-deprecate-search-plan.md diff --git a/docs/plans/2026-03-22-002-feat-router-query-deprecate-search-plan.md b/docs/plans/2026-03-22-002-feat-router-query-deprecate-search-plan.md new file mode 100644 index 000000000..0051dc992 --- /dev/null +++ b/docs/plans/2026-03-22-002-feat-router-query-deprecate-search-plan.md @@ -0,0 +1,34 @@ +--- +title: "feat: add Router.query, deprecate Router.search" +type: feat +status: active +date: 2026-03-22 +upstream_issue: https://github.com/widgetti/solara/issues/524 +repo: widgetti/solara +merge_confidence: 9 +confidence_factors: + implementability: 3 + scope: 2 + maintainer_activity: 2 + label_quality: 1 + recency: 0.5 + engagement: 0.5 +--- + +# feat: add Router.query, deprecate Router.search + +## Issue +Router.search returns the query string without the leading "?" which is inconsistent +with the URL spec (Location.search should include "?"). Rather than break existing +behavior, add Router.query (without "?") and deprecate Router.search with a warning. + +## Implementation +- `solara/routing.py`: Rename internal `self.search` to `self.query`, add deprecated + `.search` property with DeprecationWarning +- `solara/test/pytest_plugin.py`: Update internal usage from `.search` to `.query` +- `tests/unit/router_test.py`: Update tests to use `.query`, add coverage for None case + +## Evidence +- Maintainer (@maartenbreddels) explicitly specified the API: "router.query == 'a=1&b=2'" + and "turn .search into a property with deprecation warning" +- Issue comment: https://github.com/widgetti/solara/issues/524#issuecomment-2 diff --git a/solara/routing.py b/solara/routing.py index d393bd9c1..3c27dbc81 100644 --- a/solara/routing.py +++ b/solara/routing.py @@ -1,5 +1,6 @@ import abc import logging +import warnings from typing import Callable, List, Optional, Tuple, Union, cast import solara @@ -39,15 +40,15 @@ def pathname(self, value): class Router: - search: Optional[str] + query: Optional[str] def __init__(self, path: str, routes: List[solara.Route], set_path: Callable[[str], None] = None): # see https://developer.mozilla.org/en-US/docs/Web/API/Location for anatomy/nomenclature if "?" in path: - self.path, self.search = path.split("?", 1) + self.path, self.query = path.split("?", 1) else: self.path = path - self.search = None + self.query = None del path self.set_path = set_path self.parts = (self.path or "").strip("/").split("/") @@ -82,6 +83,26 @@ def __init__(self, path: str, routes: List[solara.Route], set_path: Callable[[st assert len(self.path_routes) == len(self.path_routes_siblings) self.possible_match = (len(self.path_routes[-1].children) == 0) if self.path_routes else False + @property + def search(self) -> Optional[str]: + warnings.warn( + "Router.search is deprecated. Use Router.query instead. " + "Note: Router.search returned the query string without the leading '?' " + "(inconsistent with the URL spec). Router.query has the same behavior.", + DeprecationWarning, + stacklevel=2, + ) + return self.query + + @search.setter + def search(self, value: Optional[str]): + warnings.warn( + "Router.search is deprecated. Use Router.query instead.", + DeprecationWarning, + stacklevel=2, + ) + self.query = value + def push(self, path: str): assert self.set_path is not None self.set_path(path) diff --git a/solara/test/pytest_plugin.py b/solara/test/pytest_plugin.py index 04db6d2d5..7f080d291 100644 --- a/solara/test/pytest_plugin.py +++ b/solara/test/pytest_plugin.py @@ -251,7 +251,7 @@ def run(app: Union[solara.server.app.AppScript, str], init=True): def SyncWrapper(): global run_calls router = solara.use_router() - values = urllib.parse.parse_qs(router.search, keep_blank_values=True) + values = urllib.parse.parse_qs(router.query, keep_blank_values=True) id = values.get("id", [None])[0] # type: ignore if id is None: solara.Error("No id found in url") diff --git a/tests/unit/router_test.py b/tests/unit/router_test.py index 270009751..ebbce016c 100644 --- a/tests/unit/router_test.py +++ b/tests/unit/router_test.py @@ -44,8 +44,10 @@ def test_router(): assert solara.routing.Router("/doesnotexist", routes).path_routes == [] assert solara.routing.Router("?a=1", routes).path_routes == [routes[0]] - assert solara.routing.Router("?a=1", routes).search == "a=1" + assert solara.routing.Router("?a=1", routes).query == "a=1" + assert solara.routing.Router("/fruit?b=1&c=3", routes).query == "b=1&c=3" assert solara.routing.Router("/fruit?b=1&c=3", routes).path_routes == [routes[1]] + assert solara.routing.Router("/fruit", routes).query is None # non-existing routes, as leafs are fine, since they can do 'subrouting' assert solara.routing.Router("/fruit/kiwi/sub", routes).path_routes == [routes[1], routes[1].children[0]]