Skip to content

Commit

Permalink
fix: support Layout when specifying routes (#554)
Browse files Browse the repository at this point in the history
Before, if we specified

```python

routes = [
    solara.Route(path="/", component=Home, label="Home", layout=Layout),
    solara.Route(path="about", component=About, label="About"),
]
```


We would not render the the page and layout correcly.
  • Loading branch information
maartenbreddels committed Mar 19, 2024
1 parent 3aaa4b3 commit 4d17086
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 17 deletions.
47 changes: 35 additions & 12 deletions solara/autorouting.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,17 +347,22 @@ def get_title(module: ModuleType, required=True):
return title


def fix_route(route: solara.Route, new_file: Path) -> solara.Route:
def fix_route(route: solara.Route, new_file: Path, new_layout=None) -> solara.Route:
file = route.file or new_file
layout = route.layout or new_layout
children = fix_routes(route.children, new_file) if route.children else []

return dataclasses.replace(route, file=file, children=children)
return dataclasses.replace(route, file=file, children=children, layout=layout)


def fix_routes(routes: List[solara.Route], new_file: Path):
def fix_routes(routes: List[solara.Route], new_file: Path, new_layout=None):
new_routes = []
for route in routes:
new_routes.append(fix_route(route, new_file))
if route.path == "/":
route = fix_route(route, new_file, new_layout)
else:
route = fix_route(route, new_file)
new_routes.append(route)
return new_routes


Expand All @@ -378,6 +383,7 @@ def generate_routes(module: ModuleType) -> List[solara.Route]:

assert module.__file__ is not None
routes = []
children: List[solara.Route]
file = Path(module.__file__)

if module.__file__.endswith("__init__.py"):
Expand Down Expand Up @@ -429,11 +435,15 @@ def generate_routes(module: ModuleType) -> List[solara.Route]:
warnings.warn(f"Some routes are not in route_order: {set(lookup) - set(route_order)}")

else:
children = getattr(module, "routes", [])
children = fix_routes(children, file)
layout = getattr(module, "Layout", None)
# children = []
# single module, single route
children = []
if hasattr(module, "routes"):
children = getattr(module, "routes", [])
root = get_root(children)
if layout is not None and root is not None and root.layout is None:
warnings.warn(f'You defined routes in {file}, in this case, layout should be set on the root route (with path="/"), not on the module level')
children = fix_routes(children, file, layout)
layout = None
return [solara.Route(path="/", component=RenderPage, data=None, module=module, label=get_title(module), layout=layout, children=children, file=file)]

return routes
Expand Down Expand Up @@ -493,7 +503,7 @@ def _generate_route_path(subpath: Path, layout=None, first=False, has_index=Fals
route_path = "-".join([k.lower() for k in title_parts])
# used as a 'sentinel' to find the deepest level of the route tree we need to render in 'RenderPage'
component = RenderPage
children = []
children: List[solara.Route] = []
module: Optional[ModuleType] = None
data: Any = None
module_layout = layout if first else None
Expand All @@ -506,8 +516,21 @@ def _generate_route_path(subpath: Path, layout=None, first=False, has_index=Fals
reload.reloader.watcher.add_file(subpath)
module = source_to_module(subpath, initial_namespace=initial_namespace)
title = get_title(module)
children = getattr(module, "routes", children)
layout = getattr(module, "Layout", module_layout)
if hasattr(module, "routes"):
children = getattr(module, "routes", [])
root = get_root(children)
if layout is not None and root is not None and root.layout is None:
warnings.warn(f'You defined routes in {subpath}, in this case, layout should be set on the root route (with path="/"), not on the module level')
children = fix_routes(children, subpath, layout)
layout = None
children = fix_routes(children, subpath)
module_layout = getattr(module, "Layout", module_layout)
route = solara.Route(route_path, component=component, module=module, label=title, children=children, data=data, layout=module_layout, file=subpath)
route = solara.Route(route_path, component=component, module=module, label=title, children=children, data=data, layout=layout, file=subpath)
return route


def get_root(routes: List[solara.Route]) -> Optional[solara.Route]:
for route in routes:
if route.path == "/":
return route
return None
12 changes: 10 additions & 2 deletions solara/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys
import threading
import traceback
import warnings
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional, cast
Expand Down Expand Up @@ -148,10 +149,17 @@ def add_path():
mod = importlib.import_module(self.name)
routes = solara.generate_routes(mod)

app = solara.autorouting.RenderPage(self.app_name)
if not hasattr(routes[0].module, self.app_name) and routes[0].children:
# when the root moduled defined routes, skip the enclosing route object
if len(routes) == 1 and routes[0].module and hasattr(routes[0].module, "routes"):
if routes[0].component:
warnings.warn(
f"{self.name} has a component defined, but you are also defining routes."
" To avoid confusing, consider renaming the {self.app_name} component."
)
routes = routes[0].children

app = solara.autorouting.RenderPage(self.app_name)

if settings.ssg.build_path is None:
settings.ssg.build_path = self.directory.parent.resolve() / "build"

Expand Down
51 changes: 48 additions & 3 deletions tests/unit/autorouting_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
import solara.website.pages.examples
import solara.widgets
from solara.components.title import TitleWidget
from solara.server.app import AppScript

HERE = Path(__file__)
HERE = Path(__file__).parent


def test_count_arguments():
Expand Down Expand Up @@ -133,7 +134,7 @@ def test_routes_examples_docs():


def test_routes_directory():
routes = solara.autorouting.generate_routes_directory(HERE.parent / "solara_test_apps" / "multipage")
routes = solara.autorouting.generate_routes_directory(HERE / "solara_test_apps" / "multipage")
assert len(routes) == 8
assert routes[0].path == "/"
assert routes[0].label == "Home"
Expand Down Expand Up @@ -221,7 +222,7 @@ def test_routes_directory():

def test_routes_regular_widgets():
# routes = solara.autorouting.generate_routes_directory(HERE.parent / "solara_test_apps" / "multipage")
routes = solara.autorouting.generate_routes_directory(HERE.parent / "solara_test_apps" / "multipage-widgets")
routes = solara.autorouting.generate_routes_directory(HERE / "solara_test_apps" / "multipage-widgets")

main_object = solara.autorouting.RenderPage()
solara_context = solara.RoutingProvider(children=[main_object], routes=routes, pathname="/")
Expand Down Expand Up @@ -253,3 +254,47 @@ def test_routes_regular_widgets():
assert rc.find(widgets.Button).widget.description == "Viewed 1 times"
nav.location = "/volume"
assert rc.find(v.Slider).widget.v_model == 5


def test_single_file_routes_as_file(kernel_context, no_kernel_context):
name = str(HERE / "solara_test_apps" / "single_file_routes.py")
app = AppScript(name)
assert len(app.routes) == 2
assert app.routes[0].path == "/"
assert app.routes[0].layout is not None
assert app.routes[1].path == "page2"
assert app.routes[1].layout is None
try:
with kernel_context:
el = app.run()
root = solara.RoutingProvider(children=[el], routes=app.routes, pathname="/")
_box, rc = solara.render(root, handle_error=False)
rc.find(children=["Page 1"]).assert_single()
rc.find(children=["Custom layout"]).assert_single()
rc.render(solara.RoutingProvider(children=[el], routes=app.routes, pathname="/page2").key("2"))
rc.find(children=["Custom layout"]).assert_single()
rc.find(children=["Page 2"]).assert_single()
finally:
app.close()


def test_single_file_routes_as_module(kernel_context, no_kernel_context, extra_include_path):
with extra_include_path(HERE / "solara_test_apps"):
app = AppScript("single_file_routes")
assert len(app.routes) == 2
assert app.routes[0].path == "/"
assert app.routes[0].layout is not None
assert app.routes[1].path == "page2"
assert app.routes[1].layout is None
try:
with kernel_context:
el = app.run()
root = solara.RoutingProvider(children=[el], routes=app.routes, pathname="/")
_box, rc = solara.render(root, handle_error=False)
rc.find(children=["Page 1"]).assert_single()
rc.find(children=["Custom layout"]).assert_single()
rc.render(solara.RoutingProvider(children=[el], routes=app.routes, pathname="/page2").key("2"))
rc.find(children=["Custom layout"]).assert_single()
rc.find(children=["Page 2"]).assert_single()
finally:
app.close()
25 changes: 25 additions & 0 deletions tests/unit/solara_test_apps/single_file_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import solara


@solara.component
def Page1():
solara.Text("Page 1")


@solara.component
def Page2():
solara.Text("Page 2")


@solara.component
def Layout(children):
with solara.AppLayout():
with solara.Column():
solara.Text("Custom layout")
solara.display(*children)


routes = [
solara.Route("/", component=Page1, layout=Layout),
solara.Route("page2", component=Page2),
]

0 comments on commit 4d17086

Please sign in to comment.