Source code for qtframework.navigation.router

"""Router for navigation between views.

This module provides a powerful routing system for managing navigation between
different views in a Qt application. It supports route parameters, guards,
nested routes, and navigation hooks.

Example:
    Basic router setup with routes and navigation::

        from qtframework.navigation.router import Router, Route
        from PySide6.QtWidgets import QWidget, QLabel


        # Define view components
        class HomeView(QWidget):
            def __init__(self):
                super().__init__()
                layout = QVBoxLayout(self)
                layout.addWidget(QLabel("Home Page"))


        class UserView(QWidget):
            def __init__(self, user_id=None):
                super().__init__()
                layout = QVBoxLayout(self)
                layout.addWidget(QLabel(f"User Profile: {user_id}"))


        # Define routes
        routes = [
            Route(path="/", component=HomeView, name="home"),
            Route(path="/user/:id", component=UserView, name="user_profile"),
            Route(
                path="/admin",
                component=AdminView,
                name="admin",
                guards=[lambda route: check_admin_permission()],
            ),
        ]

        # Create router
        router = Router(routes)


        # Connect to route changes
        def on_route_change(path, params):
            print(f"Navigated to: {path} with params: {params}")
            component = router.get_route_component()
            # Add component to your UI container


        router.route_changed.connect(on_route_change)

        # Navigate to routes
        router.navigate("/")  # Go to home
        router.navigate("/user/123")  # Go to user with id=123
        router.navigate_by_name("home")  # Navigate by route name

See Also:
    :class:`Route`: Route definition with parameters and guards
    :mod:`qtframework.core.window`: Window system that integrates with router
"""

from __future__ import annotations

import collections
import re
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, cast

from PySide6.QtCore import QObject, Signal

from qtframework.utils.logger import get_logger


if TYPE_CHECKING:
    from PySide6.QtWidgets import QWidget


logger = get_logger(__name__)


[docs] @dataclass class Route: """Route definition. A route maps a URL path pattern to a widget component, with support for parameters, guards, and nested routes. Example: Define routes with parameter extraction:: # Simple route home_route = Route(path="/", component=HomeView, name="home") # Route with parameters user_route = Route(path="/user/:id", component=UserView, name="user_profile") # Route with guard for authentication def auth_guard(route: Route) -> bool: return current_user.is_authenticated admin_route = Route( path="/admin", component=AdminView, name="admin", guards=[auth_guard] ) # Nested routes settings_route = Route( path="/settings", component=SettingsLayout, children=[ Route(path="/profile", component=ProfileSettings), Route(path="/preferences", component=PreferencesSettings), ], ) # Route with redirect old_route = Route(path="/old-path", redirect="/new-path") """ path: str component: type[QWidget] | collections.abc.Callable[..., QWidget] name: str | None = None params: dict[str, Any] = field(default_factory=dict) meta: dict[str, Any] = field(default_factory=dict) guards: list[collections.abc.Callable[[Route], bool]] = field(default_factory=list) children: list[Route] = field(default_factory=list) redirect: str | None = None
[docs] def matches(self, path: str) -> tuple[bool, dict[str, str]]: """Check if path matches this route. Args: path: Path to check Returns: Tuple of (matches, params) """ pattern = self._path_to_pattern() match = pattern.match(path) if match: return True, match.groupdict() return False, {}
def _path_to_pattern(self) -> re.Pattern[str]: """Convert path to regex pattern. Transforms path patterns like '/user/:id' into regex patterns with named capture groups for parameter extraction. Returns: Compiled pattern """ pattern = self.path pattern = re.sub(r":(\w+)", r"(?P<\1>[^/]+)", pattern) pattern = re.sub(r"\*", r".*", pattern) return re.compile(f"^{pattern}$")
[docs] def can_activate(self) -> bool: """Check if route can be activated. Returns: True if all guards pass """ return all(guard(self) for guard in self.guards)
[docs] class Router(QObject): """Application router for managing navigation.""" route_changed = Signal(str, dict) navigation_blocked = Signal(str, str) def __init__(self, routes: list[Route] | None = None) -> None: """Initialize router. Args: routes: List of routes """ super().__init__() self._routes: list[Route] = routes or [] self._current_path: str = "/" self._current_route: Route | None = None self._history: list[str] = [] self._future: list[str] = [] self._route_map: dict[str, Route] = {} self._before_hooks: list[collections.abc.Callable[[str, str], bool]] = [] self._after_hooks: list[collections.abc.Callable[[Route], None]] = [] self._build_route_map() def _build_route_map(self) -> None: """Build route name map.""" def add_to_map(route: Route, parent_path: str = "") -> None: """Recursively add route and children to map. Args: route: Route to add parent_path: Path prefix from parent routes """ full_path = parent_path + route.path if route.name: self._route_map[route.name] = route for child in route.children: add_to_map(child, full_path) for route in self._routes: add_to_map(route)
[docs] def add_route(self, route: Route) -> None: """Add a route. Args: route: Route to add """ self._routes.append(route) if route.name: self._route_map[route.name] = route
[docs] def remove_route(self, path: str) -> None: """Remove a route. Args: path: Route path """ self._routes = [r for r in self._routes if r.path != path] self._route_map = {n: r for n, r in self._route_map.items() if r.path != path}
[docs] def navigate( self, path: str, params: dict[str, Any] | None = None, _internal: bool = False ) -> bool: """Navigate to a path. Args: path: Path to navigate to params: Optional parameters _internal: Internal flag for history navigation Returns: True if navigation successful Raises: ValueError: If path is invalid or malformed RuntimeError: If navigation fails due to circular redirects Note: Navigation can fail in several ways: - Route not found (returns False, logs error) - Guard blocks navigation (returns False, emits navigation_blocked) - Before hook blocks navigation (returns False, emits navigation_blocked) """ logger.info("Navigating to: %s", path) for hook in self._before_hooks: if not hook(self._current_path, path): logger.warning("Navigation blocked by hook: %s", path) self.navigation_blocked.emit(self._current_path, path) return False route = self._find_route(path) if not route: logger.error("No route found for path: %s", path) return False if not route.can_activate(): logger.warning("Route guard blocked: %s", path) self.navigation_blocked.emit(self._current_path, path) return False if route.redirect: return self.navigate(route.redirect, params) # Preserve history for back/forward navigation if not _internal and self._current_path and self._current_path != path: self._history.append(self._current_path) self._future.clear() self._current_path = path self._current_route = route route_params = params or {} _, extracted_params = route.matches(path) route_params.update(extracted_params) self.route_changed.emit(path, route_params) for after_hook in self._after_hooks: after_hook(route) return True
def _find_route(self, path: str) -> Route | None: """Find route for path. Args: path: Path to find Returns: Matching route or None """ def check_route(route: Route, parent_path: str = "") -> Route | None: """Recursively check route and children for match. Args: route: Route to check parent_path: Path prefix from parent routes Returns: Matching route or None """ full_path = parent_path + route.path matches, _ = route.matches(path) if matches: return route # Check children for child in route.children: result = check_route(child, full_path) if result: return result return None for route in self._routes: result = check_route(route) if result: return result return None
[docs] def navigate_by_name(self, name: str, params: dict[str, Any] | None = None) -> bool: """Navigate to named route. Args: name: Route name params: Route parameters Returns: True if navigation successful """ route = self._route_map.get(name) if not route: logger.error("No route found with name: %s", name) return False # Build path from params path = route.path if params: for key, value in params.items(): path = path.replace(f":{key}", str(value)) return self.navigate(path, params)
[docs] def back(self) -> bool: """Navigate back in history. Returns: True if navigation successful """ if not self._history: return False # Save current path to future if self._current_path: self._future.append(self._current_path) # Get previous path and navigate path = self._history.pop() return self.navigate(path, _internal=True)
[docs] def forward(self) -> bool: """Navigate forward in history. Returns: True if navigation successful """ if not self._future: return False # Save current path to history if self._current_path: self._history.append(self._current_path) # Get next path and navigate path = self._future.pop() return self.navigate(path, _internal=True)
[docs] def reload(self) -> bool: """Reload current route. Returns: True if reload successful """ if self._current_path: return self.navigate(self._current_path) return False
@property def current_path(self) -> str: """Get current path.""" return self._current_path @property def current_route(self) -> Route | None: """Get current route.""" return self._current_route
[docs] def get_route_component(self) -> QWidget | None: """Get current route's component. Returns: Component widget or None """ if not self._current_route: return None component_callable = cast( "collections.abc.Callable[[], QWidget]", self._current_route.component ) return component_callable()
[docs] def add_before_hook(self, hook: collections.abc.Callable[[str, str], bool]) -> None: """Add before navigation hook. Args: hook: Hook function (from_path, to_path) -> bool """ if not callable(hook): raise TypeError("Before hook must be callable") self._before_hooks.append(hook)
[docs] def add_after_hook(self, hook: collections.abc.Callable[[Route], None]) -> None: """Add after navigation hook. Args: hook: Hook function """ if not callable(hook): raise TypeError("After hook must be callable") self._after_hooks.append(hook)
[docs] def get_history(self) -> list[str]: """Get navigation history. Returns: List of paths """ return self._history.copy()
[docs] def clear_history(self) -> None: """Clear navigation history.""" self._history.clear() self._future.clear()