Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -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
27 changes: 24 additions & 3 deletions solara/routing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import abc
import logging
import warnings
from typing import Callable, List, Optional, Tuple, Union, cast

import solara
Expand Down Expand Up @@ -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("/")
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion solara/test/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 3 additions & 1 deletion tests/unit/router_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down
Loading