From 632123173502474a95753ff2af6b33b6114be011 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 12 Jan 2025 01:33:01 -0800 Subject: [PATCH 1/8] Better return type for Resolver().resolve --- CHANGELOG.md | 1 + src/reactpy_router/resolvers.py | 7 ++-- src/reactpy_router/routers.py | 64 ++++++++++++--------------------- src/reactpy_router/types.py | 24 ++++++++++--- tests/test_resolver.py | 5 ++- 5 files changed, 51 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fced87..a14d9a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Don't forget to remove deprecated code on each major release! - Set minimum ReactPy version to `1.1.0`. - `link` element now calculates URL changes using the client. - Refactoring related to `reactpy>=1.1.0` changes. +- Determination of the browser's initial URL is now deterministic. ### Fixed diff --git a/src/reactpy_router/resolvers.py b/src/reactpy_router/resolvers.py index 48de28f..b1586a7 100644 --- a/src/reactpy_router/resolvers.py +++ b/src/reactpy_router/resolvers.py @@ -1,9 +1,10 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from reactpy_router.converters import CONVERTERS +from reactpy_router.types import MatchedRoute if TYPE_CHECKING: from reactpy_router.types import ConversionInfo, ConverterMapping, Route @@ -70,7 +71,7 @@ def parse_path(self, path: str) -> re.Pattern[str]: return re.compile(pattern) - def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: + def resolve(self, path: str) -> MatchedRoute | None: match = self.pattern.match(path) if match: # Convert the matched groups to the correct types @@ -80,5 +81,5 @@ def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: else parameter_name: self.converter_mapping[parameter_name](value) for parameter_name, value in match.groupdict().items() } - return (self.element, params) + return MatchedRoute(self.element, params, path) return None diff --git a/src/reactpy_router/routers.py b/src/reactpy_router/routers.py index 25c37b4..b73fd0b 100644 --- a/src/reactpy_router/routers.py +++ b/src/reactpy_router/routers.py @@ -4,7 +4,7 @@ from dataclasses import replace from logging import getLogger -from typing import TYPE_CHECKING, Any, Literal, cast +from typing import TYPE_CHECKING, Any, cast from reactpy import component, use_memo, use_state from reactpy.backend.types import Connection, Location @@ -14,6 +14,7 @@ from reactpy_router.components import History from reactpy_router.hooks import RouteState, _route_state_context from reactpy_router.resolvers import StarletteResolver +from reactpy_router.types import MatchedRoute if TYPE_CHECKING: from collections.abc import Iterator, Sequence @@ -57,36 +58,27 @@ def router( *routes: Route, resolver: Resolver[Route], ) -> VdomDict | None: - """A component that renders matching route(s) using the given resolver. + """A component that renders matching route using the given resolver. - This typically should never be used by a user. Instead, use `create_router` if creating + User notice: This component typically should never be used. Instead, use `create_router` if creating a custom routing engine.""" - old_conn = use_connection() - location, set_location = use_state(old_conn.location) - first_load, set_first_load = use_state(True) - + old_connection = use_connection() + location, set_location = use_state(cast(Location | None, None)) resolvers = use_memo( lambda: tuple(map(resolver, _iter_routes(routes))), dependencies=(resolver, hash(routes)), ) - - match = use_memo(lambda: _match_route(resolvers, location, select="first")) + route_element = None + match = use_memo(lambda: _match_route(resolvers, location or old_connection.location)) if match: - if first_load: - # We need skip rendering the application on 'first_load' to avoid - # rendering it twice. The second render follows the on_history_change event - route_elements = [] - set_first_load(False) - else: - route_elements = [ - _route_state_context( - element, - value=RouteState(set_location, params), - ) - for element, params in match - ] + # Skip rendering until ReactPy-Router knows what URL the page is on. + if location: + route_element = _route_state_context( + match.element, + value=RouteState(set_location, match.params), + ) def on_history_change(event: dict[str, Any]) -> None: """Callback function used within the JavaScript `History` component.""" @@ -96,8 +88,8 @@ def on_history_change(event: dict[str, Any]) -> None: return ConnectionContext( History({"onHistoryChangeCallback": on_history_change}), # type: ignore[return-value] - *route_elements, - value=Connection(old_conn.scope, location, old_conn.carrier), + route_element, + value=Connection(old_connection.scope, location or old_connection.location, old_connection.carrier), ) return None @@ -110,9 +102,9 @@ def _iter_routes(routes: Sequence[Route]) -> Iterator[Route]: yield parent -def _add_route_key(match: tuple[Any, dict[str, Any]], key: str | int) -> Any: +def _add_route_key(match: MatchedRoute, key: str | int) -> Any: """Add a key to the VDOM or component on the current route, if it doesn't already have one.""" - element, _params = match + element = match.element if hasattr(element, "render") and not element.key: element = cast(ComponentType, element) element.key = key @@ -125,24 +117,12 @@ def _add_route_key(match: tuple[Any, dict[str, Any]], key: str | int) -> Any: def _match_route( compiled_routes: Sequence[CompiledRoute], location: Location, - select: Literal["first", "all"], -) -> list[tuple[Any, dict[str, Any]]]: - matches = [] - +) -> MatchedRoute | None: for resolver in compiled_routes: match = resolver.resolve(location.pathname) if match is not None: - if select == "first": - return [_add_route_key(match, resolver.key)] + return _add_route_key(match, resolver.key) - # Matching multiple routes is disabled since `react-router` no longer supports multiple - # matches via the `Route` component. However, it's kept here to support future changes - # or third-party routers. - # TODO: The `resolver.key` value has edge cases where it is not unique enough to use as - # a key here. We can potentially fix this by throwing errors for duplicate identical routes. - matches.append(_add_route_key(match, resolver.key)) # pragma: no cover + _logger.debug("No matching route found for %s", location.pathname) - if not matches: - _logger.debug("No matching route found for %s", location.pathname) - - return matches + return None diff --git a/src/reactpy_router/types.py b/src/reactpy_router/types.py index ca2c913..6d3a5a3 100644 --- a/src/reactpy_router/types.py +++ b/src/reactpy_router/types.py @@ -67,11 +67,11 @@ def __call__(self, *routes: RouteType_contra) -> Component: class Resolver(Protocol[RouteType_contra]): - """Compile a route into a resolver that can be matched against a given path.""" + """A class, that when instantiated, can match routes against a given path.""" def __call__(self, route: RouteType_contra) -> CompiledRoute: """ - Compile a route into a resolver that can be matched against a given path. + Compile a route into a resolver that can be match routes against a given path. Args: route: The route to compile. @@ -87,13 +87,13 @@ class CompiledRoute(Protocol): A protocol for a compiled route that can be matched against a path. Attributes: - key (Key): A property that uniquely identifies this resolver. + key: A property that uniquely identifies this resolver. """ @property def key(self) -> Key: ... - def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: + def resolve(self, path: str) -> MatchedRoute | None: """ Return the path's associated element and path parameters or None. @@ -106,6 +106,22 @@ def resolve(self, path: str) -> tuple[Any, dict[str, Any]] | None: ... +@dataclass(frozen=True) +class MatchedRoute: + """ + Represents a matched route. + + Attributes: + element (Any): The element to render. + params (dict[str, Any]): The parameters extracted from the path. + path (str): The path that was matched. + """ + + element: Any + params: dict[str, Any] + path: str + + class ConversionInfo(TypedDict): """ A TypedDict that holds information about a conversion type. diff --git a/tests/test_resolver.py b/tests/test_resolver.py index a5dad38..dcfeb9c 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -5,13 +5,16 @@ from reactpy_router import route from reactpy_router.resolvers import StarletteResolver +from reactpy_router.types import MatchedRoute def test_resolve_any(): resolver = StarletteResolver(route("{404:any}", "Hello World")) assert resolver.parse_path("{404:any}") == re.compile("^(?P<_numeric_404>.*)$") assert resolver.converter_mapping == {"_numeric_404": str} - assert resolver.resolve("/hello/world") == ("Hello World", {"404": "/hello/world"}) + assert resolver.resolve("/hello/world") == MatchedRoute( + element="Hello World", params={"404": "/hello/world"}, path="/hello/world" + ) def test_parse_path(): From ee99f681af879869eb8905c7cc0422666c42a7a6 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 12 Jan 2025 01:41:04 -0800 Subject: [PATCH 2/8] Rename `StarletteResolver` to `ReactPyResolver`. --- CHANGELOG.md | 1 + src/reactpy_router/resolvers.py | 8 ++++---- src/reactpy_router/routers.py | 6 +++--- tests/test_resolver.py | 10 +++++----- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a14d9a0..654b87b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ Don't forget to remove deprecated code on each major release! - `link` element now calculates URL changes using the client. - Refactoring related to `reactpy>=1.1.0` changes. - Determination of the browser's initial URL is now deterministic. +- Rename `StarletteResolver` to `ReactPyResolver`. ### Fixed diff --git a/src/reactpy_router/resolvers.py b/src/reactpy_router/resolvers.py index b1586a7..4aa21ae 100644 --- a/src/reactpy_router/resolvers.py +++ b/src/reactpy_router/resolvers.py @@ -9,13 +9,13 @@ if TYPE_CHECKING: from reactpy_router.types import ConversionInfo, ConverterMapping, Route -__all__ = ["StarletteResolver"] +__all__ = ["ReactPyResolver"] -class StarletteResolver: - """URL resolver that matches routes using starlette's URL routing syntax. +class ReactPyResolver: + """URL resolver that can match a path against any given routes. - However, this resolver adds a few additional parameter types on top of Starlette's syntax.""" + URL routing syntax for this resolver is based on Starlette, and supports a mixture of Starlette and Django parameter types.""" def __init__( self, diff --git a/src/reactpy_router/routers.py b/src/reactpy_router/routers.py index b73fd0b..9c05696 100644 --- a/src/reactpy_router/routers.py +++ b/src/reactpy_router/routers.py @@ -13,7 +13,7 @@ from reactpy_router.components import History from reactpy_router.hooks import RouteState, _route_state_context -from reactpy_router.resolvers import StarletteResolver +from reactpy_router.resolvers import ReactPyResolver from reactpy_router.types import MatchedRoute if TYPE_CHECKING: @@ -36,7 +36,7 @@ def wrapper(*routes: Route) -> Component: return wrapper -_starlette_router = create_router(StarletteResolver) +_router = create_router(ReactPyResolver) def browser_router(*routes: Route) -> Component: @@ -50,7 +50,7 @@ def browser_router(*routes: Route) -> Component: Returns: A router component that renders the given routes. """ - return _starlette_router(*routes) + return _router(*routes) @component diff --git a/tests/test_resolver.py b/tests/test_resolver.py index dcfeb9c..de1167a 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -4,12 +4,12 @@ import pytest from reactpy_router import route -from reactpy_router.resolvers import StarletteResolver +from reactpy_router.resolvers import ReactPyResolver from reactpy_router.types import MatchedRoute def test_resolve_any(): - resolver = StarletteResolver(route("{404:any}", "Hello World")) + resolver = ReactPyResolver(route("{404:any}", "Hello World")) assert resolver.parse_path("{404:any}") == re.compile("^(?P<_numeric_404>.*)$") assert resolver.converter_mapping == {"_numeric_404": str} assert resolver.resolve("/hello/world") == MatchedRoute( @@ -18,7 +18,7 @@ def test_resolve_any(): def test_parse_path(): - resolver = StarletteResolver(route("/", None)) + resolver = ReactPyResolver(route("/", None)) assert resolver.parse_path("/a/b/c") == re.compile("^/a/b/c$") assert resolver.converter_mapping == {} @@ -48,13 +48,13 @@ def test_parse_path(): def test_parse_path_unkown_conversion(): - resolver = StarletteResolver(route("/", None)) + resolver = ReactPyResolver(route("/", None)) with pytest.raises(ValueError, match="Unknown conversion type 'unknown' in '/a/{b:unknown}/c'"): resolver.parse_path("/a/{b:unknown}/c") def test_parse_path_re_escape(): """Check that we escape regex characters in the path""" - resolver = StarletteResolver(route("/", None)) + resolver = ReactPyResolver(route("/", None)) assert resolver.parse_path("/a/{b:int}/c.d") == re.compile(r"^/a/(?P\d+)/c\.d$") assert resolver.converter_mapping == {"b": int} From ae400a6d13a45d34a5d0e761d9728ccdecf0feb1 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 12 Jan 2025 01:50:14 -0800 Subject: [PATCH 3/8] python 3.9 compat --- src/reactpy_router/routers.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/reactpy_router/routers.py b/src/reactpy_router/routers.py index 9c05696..fad94eb 100644 --- a/src/reactpy_router/routers.py +++ b/src/reactpy_router/routers.py @@ -4,7 +4,7 @@ from dataclasses import replace from logging import getLogger -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Union, cast from reactpy import component, use_memo, use_state from reactpy.backend.types import Connection, Location @@ -14,14 +14,13 @@ from reactpy_router.components import History from reactpy_router.hooks import RouteState, _route_state_context from reactpy_router.resolvers import ReactPyResolver -from reactpy_router.types import MatchedRoute if TYPE_CHECKING: from collections.abc import Iterator, Sequence from reactpy.core.component import Component - from reactpy_router.types import CompiledRoute, Resolver, Route, Router + from reactpy_router.types import CompiledRoute, MatchedRoute, Resolver, Route, Router __all__ = ["browser_router", "create_router"] _logger = getLogger(__name__) @@ -64,7 +63,7 @@ def router( a custom routing engine.""" old_connection = use_connection() - location, set_location = use_state(cast(Location | None, None)) + location, set_location = use_state(cast(Union[Location, None], None)) resolvers = use_memo( lambda: tuple(map(resolver, _iter_routes(routes))), dependencies=(resolver, hash(routes)), From dce3804e0a2faf61bb0f78118e27d19a95be2f4a Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 12 Jan 2025 02:39:23 -0800 Subject: [PATCH 4/8] Add docs for custom routers --- CHANGELOG.md | 4 +++ .../python/custom_router_easy_resolver.py | 16 ++++++++++ .../python/custom_router_easy_router.py | 6 ++++ docs/examples/python/example/resolvers.py | 4 +++ docs/mkdocs.yml | 2 +- docs/src/learn/custom-router.md | 29 +++++++++++++++++-- src/reactpy_router/resolvers.py | 2 +- 7 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 docs/examples/python/custom_router_easy_resolver.py create mode 100644 docs/examples/python/custom_router_easy_router.py create mode 100644 docs/examples/python/example/resolvers.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 654b87b..bfcb642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ Don't forget to remove deprecated code on each major release! ## [Unreleased] +### Added + +- Support for custom routers. + ### Changed - Set maximum ReactPy version to `<2.0.0`. diff --git a/docs/examples/python/custom_router_easy_resolver.py b/docs/examples/python/custom_router_easy_resolver.py new file mode 100644 index 0000000..cb48ce0 --- /dev/null +++ b/docs/examples/python/custom_router_easy_resolver.py @@ -0,0 +1,16 @@ +from reactpy_router.resolvers import ConversionInfo, ReactPyResolver + + +# Create a custom resolver that uses the following pattern: "{name:type}" +class CustomResolver(ReactPyResolver): + def __init__( + self, + route, + param_pattern=r"{(?P\w+)(?P:\w+)?}", # Match parameters that use the "{name:type}" format + converters={ # Enable matching for the following types: int, str, any + "int": ConversionInfo(regex=r"\d+", func=int), + "str": ConversionInfo(regex=r"[^/]+", func=str), + "any": ConversionInfo(regex=r".*", func=str), + }, + ) -> None: + super().__init__(route, param_pattern, converters) diff --git a/docs/examples/python/custom_router_easy_router.py b/docs/examples/python/custom_router_easy_router.py new file mode 100644 index 0000000..7457138 --- /dev/null +++ b/docs/examples/python/custom_router_easy_router.py @@ -0,0 +1,6 @@ +from example.resolvers import CustomResolver + +from reactpy_router.routers import create_router + +# This can be used in any location where `browser_router` was previously used +custom_router = create_router(CustomResolver) diff --git a/docs/examples/python/example/resolvers.py b/docs/examples/python/example/resolvers.py new file mode 100644 index 0000000..ad328cb --- /dev/null +++ b/docs/examples/python/example/resolvers.py @@ -0,0 +1,4 @@ +from reactpy_router.resolvers import ReactPyResolver + + +class CustomResolver(ReactPyResolver): ... diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 28df470..ad4ab0f 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -6,7 +6,7 @@ nav: - Advanced Topics: - Routers, Routes, and Links: learn/routers-routes-and-links.md - Hooks: learn/hooks.md - - Creating a Custom Router 🚧: learn/custom-router.md + - Creating a Custom Router: learn/custom-router.md - Reference: - Routers: reference/routers.md - Components: reference/components.md diff --git a/docs/src/learn/custom-router.md b/docs/src/learn/custom-router.md index fa03675..c0b1bac 100644 --- a/docs/src/learn/custom-router.md +++ b/docs/src/learn/custom-router.md @@ -1,3 +1,28 @@ -# Custom Router +Custom routers can be used to define custom routing logic for your application. This is useful when you need to implement a custom routing algorithm or when you need to integrate with an existing URL routing system. -Under construction 🚧 +--- + +## Step 1: Creating a custom resolver + +You may want to create a custom resolver to allow ReactPy to utilize an existing routing syntax. + +To start off, you will need to create a subclass of `#!python ReactPyResolver`. Within this subclass, you have two attributes which you can modify to support your custom routing syntax: + +- `#!python param_pattern`: A regular expression pattern that matches the parameters in your URL. This pattern must contain the regex named groups `name` and `type`. +- `#!python converters`: A dictionary that maps a `type` to it's respective `regex` pattern and a converter `func`. + +=== "resolver.py" + + ```python + {% include "../../examples/python/custom_router_easy_resolver.py" %} + ``` + +## Step 2: Creating a custom router + +Then, you can use this resolver to create your custom router... + +=== "resolver.py" + + ```python + {% include "../../examples/python/custom_router_easy_router.py" %} + ``` diff --git a/src/reactpy_router/resolvers.py b/src/reactpy_router/resolvers.py index 4aa21ae..347170c 100644 --- a/src/reactpy_router/resolvers.py +++ b/src/reactpy_router/resolvers.py @@ -20,7 +20,7 @@ class ReactPyResolver: def __init__( self, route: Route, - param_pattern=r"{(?P\w+)(?P:\w+)?}", + param_pattern: str = r"{(?P\w+)(?P:\w+)?}", converters: dict[str, ConversionInfo] | None = None, ) -> None: self.element = route.element From b35e82c7fd5bdb8d8644f549b121ad7bb96c1324 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 12 Jan 2025 02:54:51 -0800 Subject: [PATCH 5/8] better user API --- .../python/custom_router_easy_resolver.py | 20 +++++++++---------- docs/examples/python/example/__init__.py | 0 src/reactpy_router/resolvers.py | 17 +++++++--------- tests/test_resolver.py | 12 +++++++++++ 4 files changed, 28 insertions(+), 21 deletions(-) create mode 100644 docs/examples/python/example/__init__.py diff --git a/docs/examples/python/custom_router_easy_resolver.py b/docs/examples/python/custom_router_easy_resolver.py index cb48ce0..c3513f8 100644 --- a/docs/examples/python/custom_router_easy_resolver.py +++ b/docs/examples/python/custom_router_easy_resolver.py @@ -3,14 +3,12 @@ # Create a custom resolver that uses the following pattern: "{name:type}" class CustomResolver(ReactPyResolver): - def __init__( - self, - route, - param_pattern=r"{(?P\w+)(?P:\w+)?}", # Match parameters that use the "{name:type}" format - converters={ # Enable matching for the following types: int, str, any - "int": ConversionInfo(regex=r"\d+", func=int), - "str": ConversionInfo(regex=r"[^/]+", func=str), - "any": ConversionInfo(regex=r".*", func=str), - }, - ) -> None: - super().__init__(route, param_pattern, converters) + # Match parameters that use the "" format + param_pattern = r"<(?P\w+)(?P:\w+)?>" + + # Enable matching for the following types: int, str, any + converters = { + "int": ConversionInfo(regex=r"\d+", func=int), + "str": ConversionInfo(regex=r"[^/]+", func=str), + "any": ConversionInfo(regex=r".*", func=str), + } diff --git a/docs/examples/python/example/__init__.py b/docs/examples/python/example/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/reactpy_router/resolvers.py b/src/reactpy_router/resolvers.py index 347170c..58e7b7f 100644 --- a/src/reactpy_router/resolvers.py +++ b/src/reactpy_router/resolvers.py @@ -1,7 +1,7 @@ from __future__ import annotations import re -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar from reactpy_router.converters import CONVERTERS from reactpy_router.types import MatchedRoute @@ -17,16 +17,13 @@ class ReactPyResolver: URL routing syntax for this resolver is based on Starlette, and supports a mixture of Starlette and Django parameter types.""" - def __init__( - self, - route: Route, - param_pattern: str = r"{(?P\w+)(?P:\w+)?}", - converters: dict[str, ConversionInfo] | None = None, - ) -> None: + param_pattern: str = r"{(?P\w+)(?P:\w+)?}" + converters: ClassVar[dict[str, ConversionInfo]] = CONVERTERS + + def __init__(self, route: Route) -> None: self.element = route.element - self.registered_converters = converters or CONVERTERS self.converter_mapping: ConverterMapping = {} - self.param_regex = re.compile(param_pattern) + self.param_regex = re.compile(self.param_pattern) self.pattern = self.parse_path(route.path) self.key = self.pattern.pattern # Unique identifier for ReactPy rendering @@ -49,7 +46,7 @@ def parse_path(self, path: str) -> re.Pattern[str]: # Check if a converter exists for the type try: - conversion_info = self.registered_converters[param_type] + conversion_info = self.converters[param_type] except KeyError as e: msg = f"Unknown conversion type {param_type!r} in {path!r}" raise ValueError(msg) from e diff --git a/tests/test_resolver.py b/tests/test_resolver.py index de1167a..cf1a17e 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -17,6 +17,18 @@ def test_resolve_any(): ) +def test_custom_resolver(): + class CustomResolver(ReactPyResolver): + param_pattern = r"<(?P\w+)(?P:\w+)?>" + + resolver = CustomResolver(route("<404:any>", "Hello World")) + assert resolver.parse_path("<404:any>") == re.compile("^(?P<_numeric_404>.*)$") + assert resolver.converter_mapping == {"_numeric_404": str} + assert resolver.resolve("/hello/world") == MatchedRoute( + element="Hello World", params={"404": "/hello/world"}, path="/hello/world" + ) + + def test_parse_path(): resolver = ReactPyResolver(route("/", None)) assert resolver.parse_path("/a/b/c") == re.compile("^/a/b/c$") From e1f6f4765c7c90c5ea7d1f401f569a59bb0f052b Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 12 Jan 2025 02:57:40 -0800 Subject: [PATCH 6/8] Fix lint --- docs/examples/python/custom_router_easy_resolver.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/examples/python/custom_router_easy_resolver.py b/docs/examples/python/custom_router_easy_resolver.py index c3513f8..322cce3 100644 --- a/docs/examples/python/custom_router_easy_resolver.py +++ b/docs/examples/python/custom_router_easy_resolver.py @@ -1,13 +1,15 @@ +from typing import ClassVar + from reactpy_router.resolvers import ConversionInfo, ReactPyResolver # Create a custom resolver that uses the following pattern: "{name:type}" class CustomResolver(ReactPyResolver): # Match parameters that use the "" format - param_pattern = r"<(?P\w+)(?P:\w+)?>" + param_pattern: str = r"<(?P\w+)(?P:\w+)?>" # Enable matching for the following types: int, str, any - converters = { + converters: ClassVar[dict[str, ConversionInfo]] = { "int": ConversionInfo(regex=r"\d+", func=int), "str": ConversionInfo(regex=r"[^/]+", func=str), "any": ConversionInfo(regex=r".*", func=str), From 43dcbcc4b08bc7c4315f8e160f80a27861934f34 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 12 Jan 2025 03:04:07 -0800 Subject: [PATCH 7/8] Removed unneeded types from docstrings --- src/reactpy_router/types.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/reactpy_router/types.py b/src/reactpy_router/types.py index 6d3a5a3..755e244 100644 --- a/src/reactpy_router/types.py +++ b/src/reactpy_router/types.py @@ -28,9 +28,9 @@ class Route: A class representing a route that can be matched against a path. Attributes: - path (str): The path to match against. - element (Any): The element to render if the path matches. - routes (Sequence[Self]): Child routes. + path: The path to match against. + element: The element to render if the path matches. + routes: Child routes. Methods: __hash__() -> int: Returns a hash value for the route based on its path, element, and child routes. @@ -98,7 +98,7 @@ def resolve(self, path: str) -> MatchedRoute | None: Return the path's associated element and path parameters or None. Args: - path (str): The path to resolve. + path: The path to resolve. Returns: A tuple containing the associated element and a dictionary of path parameters, or None if the path cannot be resolved. @@ -112,9 +112,9 @@ class MatchedRoute: Represents a matched route. Attributes: - element (Any): The element to render. - params (dict[str, Any]): The parameters extracted from the path. - path (str): The path that was matched. + element: The element to render. + params: The parameters extracted from the path. + path: The path that was matched. """ element: Any @@ -127,8 +127,8 @@ class ConversionInfo(TypedDict): A TypedDict that holds information about a conversion type. Attributes: - regex (str): The regex to match the conversion type. - func (ConversionFunc): The function to convert the matched string to the expected type. + regex: The regex to match the conversion type. + func: The function to convert the matched string to the expected type. """ regex: str From 7af11b24bcc9366aa9acec3e07b33b73e7d58839 Mon Sep 17 00:00:00 2001 From: Archmonger <16909269+Archmonger@users.noreply.github.com> Date: Sun, 12 Jan 2025 03:04:14 -0800 Subject: [PATCH 8/8] bump changelog --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfcb642..7fc4f03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,9 +29,13 @@ Don't forget to remove deprecated code on each major release! - Set minimum ReactPy version to `1.1.0`. - `link` element now calculates URL changes using the client. - Refactoring related to `reactpy>=1.1.0` changes. -- Determination of the browser's initial URL is now deterministic. +- Changed ReactPy-Router's method of waiting for the initial URL to be deterministic. - Rename `StarletteResolver` to `ReactPyResolver`. +### Removed + +- `StarletteResolver` is removed in favor of `ReactPyResolver`. + ### Fixed - Fixed bug where `link` element sometimes would sometimes not retrieve the correct `href` attribute.