diff --git a/pynecone/app.py b/pynecone/app.py index 70dfd2719..65a42d565 100644 --- a/pynecone/app.py +++ b/pynecone/app.py @@ -184,11 +184,13 @@ class App(Base): ), "Path must be set if component is not a callable." path = component.__name__ - # Get args from the path for dynamic routes. - args = utils.get_path_args(path) + # Check if the path given is valid + utils.verify_path_validity(path) + + self.state.setup_dynamic_args(utils.get_path_args(path)) # Generate the component if it is a callable. - component = component if isinstance(component, Component) else component(*args) + component = component if isinstance(component, Component) else component() # Add meta information to the component. compiler_utils.add_meta( diff --git a/pynecone/state.py b/pynecone/state.py index d01dd0095..84b923f18 100644 --- a/pynecone/state.py +++ b/pynecone/state.py @@ -279,6 +279,39 @@ class State(Base, ABC): """ return self.router_data.get("query", {}) + @classmethod + def setup_dynamic_args(cls, args: dict[str, str]): + """Set up args for easy access in renderer. + + Args: + args: a dict of args + """ + + def param_factory(param): + @ComputedVar + def inner_func(self) -> str: + return self.get_query_params().get(param, "") + + return inner_func + + def catchall_factory(param): + @ComputedVar + def inner_func(self) -> List: + return self.get_query_params().get(param, []) + + return inner_func + + for param, value in args.items(): + + if value == "catchall": + func = catchall_factory(param) + elif value == "patharg": + func = param_factory(param) + else: + continue + cls.computed_vars[param] = func.set_state(cls) # type: ignore + setattr(cls, param, func) + def __getattribute__(self, name: str) -> Any: """Get the state var. diff --git a/pynecone/utils.py b/pynecone/utils.py index c7f1a9e7b..ee5c37256 100644 --- a/pynecone/utils.py +++ b/pynecone/utils.py @@ -771,12 +771,34 @@ def indent(text: str, indent_level: int = 2) -> str: return os.linesep.join(f"{' ' * indent_level}{line}" for line in lines) + os.linesep -def get_path_args(path: str) -> List[str]: +def verify_path_validity(path: str) -> None: + """Verify if the path is valid, and throw an error if not. + + Args: + path: the path that need to be checked + + Raises: + ValueError: explains what is wrong with the path. + """ + check_catchall = re.compile(r"^\[\.\.\.(.+)\]$") + catchall_found = False + for part in path.split("/"): + if catchall_found: + raise ValueError(f"Catch-all must be the last part of the URL: {path}") + match = check_catchall.match(part) + if match: + catchall_found = True + + +def get_path_args(path: str) -> Dict[str, str]: """Get the path arguments for the given path. Args: path: The path to get the arguments for. + Raises: + ValueError: explains what is wrong with the path. + Returns: The path arguments. """ @@ -785,19 +807,27 @@ def get_path_args(path: str) -> List[str]: # Regex to check for path args. check = re.compile(r"^\[(.+)\]$") + check_catchall = re.compile(r"^\[\.\.\.(.+)\]$") # Iterate over the path parts and check for path args. - args = [] - for part in os.path.split(path): + args = {} + for part in path.split("/"): + match = check_catchall.match(part) + if match: + arg_name = match.groups()[0] + if arg_name in args: + raise ValueError(f"arg [{arg_name}] is used more than once in this URL") + + args[arg_name] = "catchall" + continue + match = check.match(part) if match: # Add the path arg to the list. - v = BaseVar( - name=match.groups()[0], - type_=str, - state=f"{constants.ROUTER}.query", - ) - args.append(v) + arg_name = match.groups()[0] + if arg_name in args: + raise ValueError(f"arg [{arg_name}] is used more than once in this URL") + args[arg_name] = "patharg" return args