diff --git a/reflex/.templates/web/app/routes.js b/reflex/.templates/web/app/routes.js index ce0541e6ce0..bc4c0310d45 100644 --- a/reflex/.templates/web/app/routes.js +++ b/reflex/.templates/web/app/routes.js @@ -2,9 +2,9 @@ import { route } from "@react-router/dev/routes"; import { flatRoutes } from "@react-router/fs-routes"; export default [ - route("404", "routes/[404]_._index.jsx", { id: "404" }), + route("404", "routes/[404]._index.jsx", { id: "404" }), ...(await flatRoutes({ - ignoredRouteFiles: ["routes/\\[404\\]_._index.jsx"], + ignoredRouteFiles: ["routes/\\[404\\]._index.jsx"], })), - route("*", "routes/[404]_._index.jsx"), + route("*", "routes/[404]._index.jsx"), ]; diff --git a/reflex/app.py b/reflex/app.py index bcd3b9eff43..b658f2b9b38 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -765,9 +765,9 @@ def add_page( msg = "Route must be set if component is not a callable." raise exceptions.RouteValueError(msg) # Format the route. - route = format.format_route(component.__name__) + route = format.format_route(format.to_kebab_case(component.__name__)) else: - route = format.format_route(route, format_case=False) + route = format.format_route(route) if route == constants.Page404.SLUG: if component is None: @@ -822,10 +822,11 @@ def add_page( state = self._state if self._state else State state.setup_dynamic_args(get_route_args(route)) - if on_load: - self._load_events[route] = ( - on_load if isinstance(on_load, list) else [on_load] - ) + self._load_events[route] = ( + (on_load if isinstance(on_load, list) else [on_load]) + if on_load is not None + else [] + ) self._unevaluated_pages[route] = unevaluated_page @@ -854,48 +855,35 @@ def _compile_page(self, route: str, save_page: bool = True): if save_page: self._pages[route] = component - def get_load_events(self, route: str) -> list[IndividualEventType[()]]: + @functools.cached_property + def router(self) -> Callable[[str], str | None]: + """Get the route computer function. + + Returns: + The route computer function. + """ + from reflex.route import get_router + + return get_router(list(self._pages)) + + def get_load_events(self, path: str) -> list[IndividualEventType[()]]: """Get the load events for a route. Args: - route: The route to get the load events for. + path: The route to get the load events for. Returns: The load events for the route. """ - route = route.lstrip("/").rstrip("/") - if route == "": - return self._load_events.get(constants.PageNames.INDEX_ROUTE, []) - - # Separate the pages by route type. - static_page_paths_to_page_route = {} - dynamic_page_paths_to_page_route = {} - for page_route in list(self._pages) + list(self._unevaluated_pages): - page_path = page_route.lstrip("/").rstrip("/") - if "[" in page_path and "]" in page_path: - dynamic_page_paths_to_page_route[page_path] = page_route - else: - static_page_paths_to_page_route[page_path] = page_route - - # Check for static routes. - if (page_route := static_page_paths_to_page_route.get(route)) is not None: - return self._load_events.get(page_route, []) - - # Check for dynamic routes. - parts = route.split("/") - for page_path, page_route in dynamic_page_paths_to_page_route.items(): - page_parts = page_path.split("/") - if len(page_parts) != len(parts): - continue - if all( - part == page_part - or (page_part.startswith("[") and page_part.endswith("]")) - for part, page_part in zip(parts, page_parts, strict=False) - ): - return self._load_events.get(page_route, []) - - # Default to 404 page load events if no match found. - return self._load_events.get("404", []) + four_oh_four_load_events = self._load_events.get("404", []) + route = self.router(path) + if not route: + # If the path is not a valid route, return the 404 page load events. + return four_oh_four_load_events + return self._load_events.get( + route, + four_oh_four_load_events, + ) def _check_routes_conflict(self, new_route: str): """Verify if there is any conflict between the new route and any existing route. @@ -916,7 +904,6 @@ def _check_routes_conflict(self, new_route: str): segments = ( constants.RouteRegex.SINGLE_SEGMENT, constants.RouteRegex.DOUBLE_SEGMENT, - constants.RouteRegex.SINGLE_CATCHALL_SEGMENT, constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT, ) for route in self._pages: @@ -1742,6 +1729,11 @@ async def process( # assignment will recurse into substates and force recalculation of # dependent ComputedVar (dynamic route variables) state.router_data = router_data + router_data[constants.RouteVar.PATH] = "/" + ( + app.router(path) or "404" + if (path := router_data.get(constants.RouteVar.PATH)) + else "404" + ).removeprefix("/") state.router = RouterData(router_data) # Preprocess the event. diff --git a/reflex/compiler/utils.py b/reflex/compiler/utils.py index 48cb6d4f96f..22a176e93c3 100644 --- a/reflex/compiler/utils.py +++ b/reflex/compiler/utils.py @@ -417,9 +417,11 @@ def _format_route_part(part: str) -> str: if part.startswith("[") and part.endswith("]"): if part.startswith(("[...", "[[...")): return "$" + if part.startswith("[["): + return "($" + part.removeprefix("[[").removesuffix("]]") + ")" # We don't add [] here since we are reusing them from the input - return "$" + part + "_" - return "[" + part + "]_" + return "$" + part + return "[" + part + "]" def _path_to_file_stem(path: str) -> str: diff --git a/reflex/constants/route.py b/reflex/constants/route.py index accc3d8f5d7..30e7b32170e 100644 --- a/reflex/constants/route.py +++ b/reflex/constants/route.py @@ -41,23 +41,30 @@ class RouteRegex(SimpleNamespace): _CLOSING_BRACKET = r"\]" _ARG_NAME = r"[a-zA-Z_]\w*" + # The regex for a valid arg name, e.g. "slug" in "[slug]" + _ARG_NAME_PATTERN = re.compile(_ARG_NAME) + + SLUG = re.compile(r"[a-zA-Z0-9_-]+") # match a single arg (i.e. "[slug]"), returns the name of the arg ARG = re.compile(rf"{_OPENING_BRACKET}({_ARG_NAME}){_CLOSING_BRACKET}") - # match a single catch-all arg (i.e. "[...slug]" or "[[...slug]]"), returns the name of the arg - CATCHALL = re.compile( - rf"({_OPENING_BRACKET}?{_OPENING_BRACKET}{_DOT_DOT_DOT}[^[{_CLOSING_BRACKET}]*{_CLOSING_BRACKET}?{_CLOSING_BRACKET})" + # match a single optional arg (i.e. "[[slug]]"), returns the name of the arg + OPTIONAL_ARG = re.compile( + rf"{_OPENING_BRACKET * 2}({_ARG_NAME}){_CLOSING_BRACKET * 2}" ) + # match a single non-optional catch-all arg (i.e. "[...slug]"), returns the name of the arg STRICT_CATCHALL = re.compile( rf"{_OPENING_BRACKET}{_DOT_DOT_DOT}({_ARG_NAME}){_CLOSING_BRACKET}" ) - # match a snigle optional catch-all arg (i.e. "[[...slug]]"), returns the name of the arg - OPT_CATCHALL = re.compile( + + # match a single optional catch-all arg (i.e. "[[...slug]]"), returns the name of the arg + OPTIONAL_CATCHALL = re.compile( rf"{_OPENING_BRACKET * 2}{_DOT_DOT_DOT}({_ARG_NAME}){_CLOSING_BRACKET * 2}" ) + + SPLAT_CATCHALL = "[[...splat]]" SINGLE_SEGMENT = "__SINGLE_SEGMENT__" DOUBLE_SEGMENT = "__DOUBLE_SEGMENT__" - SINGLE_CATCHALL_SEGMENT = "__SINGLE_CATCHALL_SEGMENT__" DOUBLE_CATCHALL_SEGMENT = "__DOUBLE_CATCHALL_SEGMENT__" diff --git a/reflex/route.py b/reflex/route.py index 67bac39c590..4cf7c175771 100644 --- a/reflex/route.py +++ b/reflex/route.py @@ -3,6 +3,7 @@ from __future__ import annotations import re +from collections.abc import Callable from reflex import constants @@ -16,13 +17,39 @@ def verify_route_validity(route: str) -> None: Raises: ValueError: If the route is invalid. """ - pattern = catchall_in_route(route) - if pattern: - if pattern != "[[...splat]]": - msg = f"Catchall pattern `{pattern}` is not valid. Only `[[...splat]]` is allowed." + route_parts = route.removeprefix("/").split("/") + for i, part in enumerate(route_parts): + if constants.RouteRegex.SLUG.fullmatch(part): + continue + if not part.startswith("[") or not part.endswith("]"): + msg = ( + f"Route part `{part}` is not valid. Reflex only supports " + "alphabetic characters, underscores, and hyphens in route parts. " + ) raise ValueError(msg) - if not route.endswith(pattern): - msg = f"Catchall pattern `{pattern}` must be at the end of the route." + if part.startswith(("[[...", "[...")): + if part != constants.RouteRegex.SPLAT_CATCHALL: + msg = f"Catchall pattern `{part}` is not valid. Only `{constants.RouteRegex.SPLAT_CATCHALL}` is allowed." + raise ValueError(msg) + if i != len(route_parts) - 1: + msg = f"Catchall pattern `{part}` must be at the end of the route." + raise ValueError(msg) + continue + if part.startswith("[["): + if constants.RouteRegex.OPTIONAL_ARG.fullmatch(part): + continue + msg = ( + f"Route part `{part}` with optional argument is not valid. " + "Reflex only supports optional arguments that start with an alphabetic character or underscore, " + "followed by alphanumeric characters or underscores." + ) + raise ValueError(msg) + if not constants.RouteRegex.ARG.fullmatch(part): + msg = ( + f"Route part `{part}` with argument is not valid. " + "Reflex only supports argument names that start with an alphabetic character or underscore, " + "followed by alphanumeric characters or underscores." + ) raise ValueError(msg) @@ -37,98 +64,159 @@ def get_route_args(route: str) -> dict[str, str]: """ args = {} - def add_route_arg(match: re.Match[str], type_: str): - """Add arg from regex search result. - - Args: - match: Result of a regex search - type_: The assigned type for this arg - - Raises: - ValueError: If the route is invalid. - """ - arg_name = match.groups()[0] + def _add_route_arg(arg_name: str, type_: str): if arg_name in args: - msg = f"Arg name [{arg_name}] is used more than once in this URL" + msg = ( + f"Arg name `{arg_name}` is used more than once in the route `{route}`." + ) raise ValueError(msg) args[arg_name] = type_ # Regex to check for route args. - check = constants.RouteRegex.ARG - check_strict_catchall = constants.RouteRegex.STRICT_CATCHALL - check_opt_catchall = constants.RouteRegex.OPT_CATCHALL + argument_regex = constants.RouteRegex.ARG + optional_argument_regex = constants.RouteRegex.OPTIONAL_ARG # Iterate over the route parts and check for route args. for part in route.split("/"): - match_opt = check_opt_catchall.match(part) - if match_opt: - add_route_arg(match_opt, constants.RouteArgType.LIST) + if part == constants.RouteRegex.SPLAT_CATCHALL: + _add_route_arg("splat", constants.RouteArgType.LIST) break - match_strict = check_strict_catchall.match(part) - if match_strict: - add_route_arg(match_strict, constants.RouteArgType.LIST) - break + optional_argument = optional_argument_regex.match(part) + if optional_argument: + _add_route_arg(optional_argument.group(1), constants.RouteArgType.SINGLE) + continue + + argument = argument_regex.match(part) + if argument: + _add_route_arg(argument.group(1), constants.RouteArgType.SINGLE) + continue - match = check.match(part) - if match: - # Add the route arg to the list. - add_route_arg(match, constants.RouteArgType.SINGLE) return args -def catchall_in_route(route: str) -> str: - """Extract the catchall part from a route. +def replace_brackets_with_keywords(input_string: str) -> str: + """Replace brackets and everything inside it in a string with a keyword. Example: - >>> catchall_in_route("/posts/[...slug]") - '[...slug]' - >>> catchall_in_route("/posts/[[...slug]]") - '[[...slug]]' - >>> catchall_in_route("/posts/[slug]") - '' + >>> replace_brackets_with_keywords("/posts") + '/posts' + >>> replace_brackets_with_keywords("/posts/[slug]") + '/posts/__SINGLE_SEGMENT__' + >>> replace_brackets_with_keywords("/posts/[slug]/comments") + '/posts/__SINGLE_SEGMENT__/comments' + >>> replace_brackets_with_keywords("/posts/[[slug]]") + '/posts/__DOUBLE_SEGMENT__' + >>> replace_brackets_with_keywords("/posts/[[...splat]]") + '/posts/__DOUBLE_CATCHALL_SEGMENT__' Args: - route: the route from which to extract + input_string: String to replace. Returns: - str: the catchall part of the URI + new string containing keywords. """ - match_ = constants.RouteRegex.CATCHALL.search(route) - return match_.group() if match_ else "" + # Replace [] with __SINGLE_SEGMENT__ + return constants.RouteRegex.ARG.sub( + constants.RouteRegex.SINGLE_SEGMENT, + # Replace [[slug]] with __DOUBLE_SEGMENT__ + constants.RouteRegex.OPTIONAL_ARG.sub( + constants.RouteRegex.DOUBLE_SEGMENT, + # Replace [[...splat]] with __DOUBLE_CATCHALL_SEGMENT__ + input_string.replace( + constants.RouteRegex.SPLAT_CATCHALL, + constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT, + ), + ), + ) -def replace_brackets_with_keywords(input_string: str) -> str: - """Replace brackets and everything inside it in a string with a keyword. +def route_specifity(keyworded_route: str) -> tuple[int, int, int]: + """Get the specificity of a route with keywords. + + The smaller the number, the more specific the route is. Args: - input_string: String to replace. + keyworded_route: The route with keywords. Returns: - new string containing keywords. + A tuple containing the counts of double catchall segments, + double segments, and single segments in the route. """ - # /posts -> /post - # /posts/[slug] -> /posts/__SINGLE_SEGMENT__ - # /posts/[slug]/comments -> /posts/__SINGLE_SEGMENT__/comments - # /posts/[[slug]] -> /posts/__DOUBLE_SEGMENT__ - # / posts/[[...slug2]]-> /posts/__DOUBLE_CATCHALL_SEGMENT__ - # /posts/[...slug3]-> /posts/__SINGLE_CATCHALL_SEGMENT__ - - # Replace [[...]] with __DOUBLE_CATCHALL_SEGMENT__ - output_string = re.sub( - r"\[\[\.\.\..+?\]\]", - constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT, - input_string, - ) - # Replace [...] with __SINGLE_CATCHALL_SEGMENT__ - output_string = re.sub( - r"\[\.\.\..+?\]", - constants.RouteRegex.SINGLE_CATCHALL_SEGMENT, - output_string, + return ( + keyworded_route.count(constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT), + keyworded_route.count(constants.RouteRegex.DOUBLE_SEGMENT), + keyworded_route.count(constants.RouteRegex.SINGLE_SEGMENT), ) - # Replace [[]] with __DOUBLE_SEGMENT__ - output_string = re.sub( - r"\[\[.+?\]\]", constants.RouteRegex.DOUBLE_SEGMENT, output_string + + +def get_route_regex(keyworded_route: str) -> re.Pattern: + """Get the regex pattern for a route with keywords. + + Args: + keyworded_route: The route with keywords. + + Returns: + A compiled regex pattern for the route. + """ + if keyworded_route == "index": + return re.compile(re.escape("/")) + path_parts = keyworded_route.split("/") + regex_parts = [] + for part in path_parts: + if part == constants.RouteRegex.SINGLE_SEGMENT: + # Match a single segment (/slug) + regex_parts.append(r"/[^/]*") + elif part == constants.RouteRegex.DOUBLE_SEGMENT: + # Match a single optional segment (/slug or nothing) + regex_parts.append(r"(/[^/]+)?") + elif part == constants.RouteRegex.DOUBLE_CATCHALL_SEGMENT: + regex_parts.append(".*") + else: + regex_parts.append(re.escape("/" + part)) + # Join the regex parts and compile the regex + regex_pattern = "".join(regex_parts) + regex_pattern = f"^{regex_pattern}/?$" + return re.compile(regex_pattern) + + +def get_router(routes: list[str]) -> Callable[[str], str | None]: + """Get a function that computes the route for a given path. + + Args: + routes: A list of routes to match against. + + Returns: + A function that takes a path and returns the first matching route, + or None if no match is found. + """ + keyworded_routes = { + replace_brackets_with_keywords(route): route for route in routes + } + sorted_routes_by_specifity = sorted( + keyworded_routes.items(), + key=lambda item: route_specifity(item[0]), ) - # Replace [] with __SINGLE_SEGMENT__ - return re.sub(r"\[.+?\]", constants.RouteRegex.SINGLE_SEGMENT, output_string) + regexed_routes = [ + (get_route_regex(keyworded_route), original_route) + for keyworded_route, original_route in sorted_routes_by_specifity + ] + + def get_route(path: str) -> str | None: + """Get the first matching route for a given path. + + Args: + path: The path to match against the routes. + + Returns: + The first matching route, or None if no match is found. + """ + path = "/" + path.removeprefix("/").removesuffix("/") + if path == "/index": + path = "/" + for regex, original_route in regexed_routes: + if regex.fullmatch(path): + return original_route + return None + + return get_route diff --git a/reflex/utils/format.py b/reflex/utils/format.py index 22c1347098f..2045dea9d70 100644 --- a/reflex/utils/format.py +++ b/reflex/utils/format.py @@ -310,20 +310,16 @@ def format_var(var: Var) -> str: return str(var) -def format_route(route: str, format_case: bool = True) -> str: +def format_route(route: str) -> str: """Format the given route. Args: route: The route to format. - format_case: whether to format case to kebab case. Returns: The formatted route. """ route = route.strip("/") - # Strip the route and format casing. - if format_case: - route = to_kebab_case(route) # If the route is empty, return the index route. if route == "": diff --git a/tests/integration/test_dynamic_routes.py b/tests/integration/test_dynamic_routes.py index a66983f8f86..5c562d17f5f 100644 --- a/tests/integration/test_dynamic_routes.py +++ b/tests/integration/test_dynamic_routes.py @@ -269,7 +269,7 @@ async def test_on_load_navigate( link = driver.find_element(By.ID, "link_page_next") assert link - exp_order = [f"/page/{ix}-{ix}" for ix in range(10)] + exp_order = [f"/page/[page_id]-{ix}" for ix in range(10)] # click the link a few times for ix in range(10): # wait for navigation, then assert on url @@ -297,20 +297,20 @@ async def test_on_load_navigate( frontend_url = frontend_url.removesuffix("/") # manually load the next page to trigger client side routing in prod mode - exp_order += ["/page/10-10"] + exp_order += ["/page/[page_id]-10"] with poll_for_navigation(driver): driver.get(f"{frontend_url}/page/10") await poll_for_order(exp_order) # make sure internal nav still hydrates after redirect - exp_order += ["/page/11-11"] + exp_order += ["/page/[page_id]-11"] link = driver.find_element(By.ID, "link_page_next") with poll_for_navigation(driver): link.click() await poll_for_order(exp_order) # load same page with a query param and make sure it passes through - exp_order += ["/page/11-11"] + exp_order += ["/page/[page_id]-11"] with poll_for_navigation(driver): driver.get(f"{driver.current_url}?foo=bar") await poll_for_order(exp_order) @@ -319,26 +319,26 @@ async def test_on_load_navigate( ).router.page.params["foo"] == "bar" # hit a 404 and ensure we still hydrate - exp_order += ["/missing-no page id"] + exp_order += ["/404-no page id"] with poll_for_navigation(driver): driver.get(f"{frontend_url}/missing") await poll_for_order(exp_order) # browser nav should still trigger hydration - exp_order += ["/page/11-11"] + exp_order += ["/page/[page_id]-11"] with poll_for_navigation(driver): driver.back() await poll_for_order(exp_order) # next/link to a 404 and ensure we still hydrate - exp_order += ["/missing-no page id"] + exp_order += ["/404-no page id"] link = driver.find_element(By.ID, "link_missing") with poll_for_navigation(driver): link.click() await poll_for_order(exp_order) # hit a page that redirects back to dynamic page - exp_order += ["on_load_redir-{'foo': 'bar', 'page_id': '0'}", "/page/0-0"] + exp_order += ["on_load_redir-{'foo': 'bar', 'page_id': '0'}", "/page/[page_id]-0"] with poll_for_navigation(driver): driver.get(f"{frontend_url}/redirect-page/0/?foo=bar") await poll_for_order(exp_order) diff --git a/tests/units/test_app.py b/tests/units/test_app.py index fec3c758dce..19255508471 100644 --- a/tests/units/test_app.py +++ b/tests/units/test_app.py @@ -2,7 +2,6 @@ import functools import io -import os.path import unittest.mock import uuid from collections.abc import Generator @@ -241,27 +240,25 @@ def test_add_page_default_route(app: App, index_page, about_page): assert app._pages.keys() == {"index", "about"} -def test_add_page_set_route(app: App, index_page, windows_platform: bool): +def test_add_page_set_route(app: App, index_page): """Test adding a page to an app. Args: app: The app to test. index_page: The index page. - windows_platform: Whether the system is windows. """ - route = "test" if windows_platform else "/test" + route = "/test" assert app._unevaluated_pages == {} app.add_page(index_page, route=route) app._compile_page("test") assert app._pages.keys() == {"test"} -def test_add_page_set_route_dynamic(index_page, windows_platform: bool): +def test_add_page_set_route_dynamic(index_page): """Test adding a page with dynamic route variable to an app. Args: index_page: The index page. - windows_platform: Whether the system is windows. """ app = App(_state=EmptyState) assert app._state is not None @@ -277,18 +274,17 @@ def test_add_page_set_route_dynamic(index_page, windows_platform: bool): assert constants.ROUTER in app._state()._var_dependencies -def test_add_page_set_route_nested(app: App, index_page, windows_platform: bool): +def test_add_page_set_route_nested(app: App, index_page): """Test adding a page to an app. Args: app: The app to test. index_page: The index page. - windows_platform: Whether the system is windows. """ - route = "test\\nested" if windows_platform else "/test/nested" + route = "test/nested" assert app._unevaluated_pages == {} app.add_page(index_page, route=route) - assert app._unevaluated_pages.keys() == {route.strip(os.path.sep)} + assert app._unevaluated_pages.keys() == {route} def test_add_page_invalid_api_route(app: App, index_page): @@ -976,7 +972,6 @@ def comp_dynamic(self) -> str: def test_dynamic_arg_shadow( index_page: ComponentCallable, - windows_platform: bool, token: str, app_module_mock: unittest.mock.Mock, mocker: MockerFixture, @@ -985,7 +980,6 @@ def test_dynamic_arg_shadow( Args: index_page: The index page. - windows_platform: Whether the system is windows. token: a Token. app_module_mock: Mocked app module. mocker: pytest mocker object. @@ -1000,7 +994,6 @@ def test_dynamic_arg_shadow( def test_multiple_dynamic_args( index_page: ComponentCallable, - windows_platform: bool, token: str, app_module_mock: unittest.mock.Mock, mocker: MockerFixture, @@ -1009,7 +1002,6 @@ def test_multiple_dynamic_args( Args: index_page: The index page. - windows_platform: Whether the system is windows. token: a Token. app_module_mock: Mocked app module. mocker: pytest mocker object. @@ -1025,7 +1017,6 @@ def test_multiple_dynamic_args( @pytest.mark.asyncio async def test_dynamic_route_var_route_change_completed_on_load( index_page: ComponentCallable, - windows_platform: bool, token: str, app_module_mock: unittest.mock.Mock, mocker: MockerFixture, @@ -1037,17 +1028,17 @@ async def test_dynamic_route_var_route_change_completed_on_load( Args: index_page: The index page. - windows_platform: Whether the system is windows. token: a Token. app_module_mock: Mocked app module. mocker: pytest mocker object. """ arg_name = "dynamic" - route = f"/test/[{arg_name}]" + route = f"test/[{arg_name}]" app = app_module_mock.app = App(_state=DynamicState) assert app._state is not None assert arg_name not in app._state.vars app.add_page(index_page, route=route, on_load=DynamicState.on_load) + app._compile_page(route) assert arg_name in app._state.vars assert arg_name in app._state.computed_vars assert app._state.computed_vars[arg_name]._deps(objclass=DynamicState) == { @@ -1068,7 +1059,7 @@ def _event(name, val, **kwargs): token=kwargs.pop("token", token), name=name, router_data=kwargs.pop( - "router_data", {"pathname": route, "query": {arg_name: val}} + "router_data", {"pathname": "/" + route, "query": {arg_name: val}} ), payload=kwargs.pop("payload", {}), **kwargs, diff --git a/tests/units/test_route.py b/tests/units/test_route.py index 9a14d1a67c4..db095b3c802 100644 --- a/tests/units/test_route.py +++ b/tests/units/test_route.py @@ -3,7 +3,7 @@ from reflex import constants from reflex.app import App -from reflex.route import catchall_in_route, get_route_args, verify_route_validity +from reflex.route import get_route_args, verify_route_validity @pytest.mark.parametrize( @@ -35,17 +35,6 @@ def test_invalid_route_args(route_name): get_route_args(route_name) -@pytest.mark.parametrize( - ("route_name", "expected"), - [ - ("/events/[year]/[month]/[...slug]", "[...slug]"), - ("pages/shop/[[...slug]]", "[[...slug]]"), - ], -) -def test_catchall_in_route(route_name, expected): - assert catchall_in_route(route_name) == expected - - @pytest.mark.parametrize( "route_name", [ @@ -85,11 +74,12 @@ def app(): ("/posts/[slug]/info", "/posts/[slug1]/info1"), ("/posts/[slug]/info/[[slug1]]", "/posts/[slug1]/info1/[[slug2]]"), ("/posts/[slug]/info/[[slug1]]", "/posts/[slug]/info/[[slug2]]"), - ("/posts/[slug]/info/[[...slug1]]", "/posts/[slug1]/info/[[...slug2]]"), - ("/posts/[slug]/info/[[...slug1]]", "/posts/[slug]/info/[[...slug2]]"), + ("/posts/[slug]/info/[[...splat]]", "/posts/[slug1]/info/[[...splat]]"), ], ) -def test_check_routes_conflict_invalid(mocker: MockerFixture, app, route1, route2): +def test_check_routes_conflict_invalid( + mocker: MockerFixture, app: App, route1: str, route2: str +): mocker.patch.object(app, "_pages", {route1: []}) with pytest.raises(ValueError): app._check_routes_conflict(route2) @@ -111,8 +101,7 @@ def test_check_routes_conflict_invalid(mocker: MockerFixture, app, route1, route "/posts/[slug]/info/[slug1]/random1/[slug2]/x", "/posts/[slug]/info/[slug1]/random/[slug4]/x1", ), - ("/posts/[slug]/info/[[...slug1]]", "/posts/[slug]/info1/[[...slug1]]"), - ("/posts/[slug]/info/[[...slug1]]", "/posts/[slug]/info1/[[...slug2]]"), + ("/posts/[slug]/info/[[...splat]]", "/posts/[slug]/info1/[[...splat]]"), ("/posts/[slug]/info/[...slug1]", "/posts/[slug]/info1/[...slug1]"), ("/posts/[slug]/info/[...slug1]", "/posts/[slug]/info1/[...slug2]"), ], diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 7eceee8a343..3d486018bec 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -2947,9 +2947,14 @@ async def test_preprocess( mocker.patch( "reflex.state.State.class_subclasses", {test_state, OnLoadInternalState} ) - app = app_module_mock.app = App( - _state=State, _load_events={"index": [test_state.test_handler]} - ) + app = app_module_mock.app = App(_state=State) + + def index(): + return "hello" + + app.add_page(index, on_load=test_state.test_handler) + app._compile_page("index") + async with app.state_manager.modify_state(_substate_key(token, State)) as state: state.router_data = {"simulate": "hydrate"} @@ -2996,10 +3001,13 @@ async def test_preprocess_multiple_load_events( mocker.patch( "reflex.state.State.class_subclasses", {OnLoadState, OnLoadInternalState} ) - app = app_module_mock.app = App( - _state=State, - _load_events={"index": [OnLoadState.test_handler, OnLoadState.test_handler]}, - ) + app = app_module_mock.app = App(_state=State) + + def index(): + return "hello" + + app.add_page(index, on_load=[OnLoadState.test_handler, OnLoadState.test_handler]) + app._compile_page("index") async with app.state_manager.modify_state(_substate_key(token, State)) as state: state.router_data = {"simulate": "hydrate"} diff --git a/tests/units/utils/test_format.py b/tests/units/utils/test_format.py index d8b425e845e..cb99bd874bf 100644 --- a/tests/units/utils/test_format.py +++ b/tests/units/utils/test_format.py @@ -286,31 +286,25 @@ def test_format_var(input: Var, output: str): @pytest.mark.parametrize( - ("route", "format_case", "expected"), + ("route", "expected"), [ - ("", True, "index"), - ("/", True, "index"), - ("custom-route", True, "custom-route"), - ("custom-route", False, "custom-route"), - ("custom-route/", True, "custom-route"), - ("custom-route/", False, "custom-route"), - ("/custom-route", True, "custom-route"), - ("/custom-route", False, "custom-route"), - ("/custom_route", True, "custom-route"), - ("/custom_route", False, "custom_route"), - ("/CUSTOM_route", True, "custom-route"), - ("/CUSTOM_route", False, "CUSTOM_route"), + ("", "index"), + ("/", "index"), + ("custom-route", "custom-route"), + ("custom-route/", "custom-route"), + ("/custom-route", "custom-route"), + ("/custom_route", "custom_route"), + ("/CUSTOM_route", "CUSTOM_route"), ], ) -def test_format_route(route: str, format_case: bool, expected: bool): +def test_format_route(route: str, expected: str): """Test formatting a route. Args: route: The route to format. - format_case: Whether to change casing to snake_case. expected: The expected formatted route. """ - assert format.format_route(route, format_case=format_case) == expected + assert format.format_route(route) == expected @pytest.mark.parametrize(