Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Working Jinja2 url_for with test #1004

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
14461b2
Initial static text url_for implementation
dave42w Oct 28, 2024
6b907b2
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 28, 2024
68d4f2e
Working url_for
dave42w Nov 3, 2024
d0d8e7b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 3, 2024
560dcd2
Merge branch 'sparckles:main' into url_for
dave42w Nov 3, 2024
2a02a6a
Sort linter errors
dave42w Nov 3, 2024
8433c59
sync project.toml and poetry.lock
dave42w Nov 4, 2024
25df6bc
Corrected import for Route
dave42w Nov 4, 2024
39dac34
Needed to run using uv
dave42w Nov 4, 2024
c54238a
Needed to use integration tests with uv
dave42w Nov 4, 2024
11f198a
remove temporary pytest mark
dave42w Nov 4, 2024
beae5ee
update pytest to avoid lots of python 3.14 depreciation warnings
dave42w Nov 4, 2024
032c247
Added abstract methodist to interface, added a docstring for set_roby…
dave42w Nov 4, 2024
e9df6ab
rename url_for to get_function_url & working HttpMethod arg & unit te…
dave42w Nov 4, 2024
620fb62
encoding wasn't needed (causing me git problems with the utf-8 specified
dave42w Nov 4, 2024
a24d8e2
Move as much uv stuff as possible into separate sections
dave42w Nov 4, 2024
bfb3ca8
Moving pyproject.toml target
dave42w Nov 4, 2024
6a5168c
Merge branch 'sparckles:main' into url_for
dave42w Nov 6, 2024
79318e9
support for kwargs for url's with tests
dave42w Nov 6, 2024
32836ff
support for kwargs for url's with tests
dave42w Nov 6, 2024
b655eba
support for kwargs for url's with tests
dave42w Nov 6, 2024
3d93d85
restore pyproject
dave42w Nov 7, 2024
c87a201
Merge branch 'sparckles:main' into url_for
dave42w Nov 11, 2024
3829ab3
Merge branch 'sparckles:main' into url_for
dave42w Nov 12, 2024
f489ee2
Merge branch 'sparckles:main' into url_for
dave42w Nov 20, 2024
f6193ea
Fix linter complaints
dave42w Nov 21, 2024
67c535b
Merge branch 'sparckles:main' into url_for
dave42w Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions integration_tests/base_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

current_file_path = pathlib.Path(__file__).parent.resolve()
jinja_template = JinjaTemplate(os.path.join(current_file_path, "templates"))
jinja_template.set_robyn(app)

# ===== Websockets =====

Expand Down
2 changes: 1 addition & 1 deletion integration_tests/templates/test.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
</head>

<body>
<h1>{{framework}} 🤝 {{templating_engine}}</h1>
<h1>{{framework}} 🤝 {{templating_engine}} <a href="{{get_function_url('sync_auth')}}">Testing get_function_url</a></h1>
</body>
</html>
1 change: 1 addition & 0 deletions integration_tests/test_get_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def check_response(r: Response):
assert r.text.startswith("\n\n<!DOCTYPE html>")
assert "Jinja2" in r.text
assert "Robyn" in r.text
assert '<a href="/sync/auth">Testing get_function_url</a>' in r.text

check_response(get(f"/{function_type}/template"))

Expand Down
67 changes: 64 additions & 3 deletions robyn/templating.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,83 @@
from abc import ABC, abstractmethod
from typing import Callable, List

from jinja2 import Environment, FileSystemLoader

from robyn import status_codes
from robyn import Robyn, status_codes
from robyn.router import Route

from .robyn import Headers, Response


def get_param_filled_url(url: str, kwdict: dict | None = None) -> str:
"""fill the :params in the url

Args:
url (str): typically comes from the route
kwdict (dict): the **kwargs as a dict

Returns:
str: _description_modified url (if there are elements in kwdict, otherwise unchanged)
"""
if kwdict is not None:
for k, v in zip(kwdict.keys(), kwdict.values()):
url = url.replace(f":{k}", f"{v}")

return url


class TemplateInterface(ABC):
def __init__(self): ...

@abstractmethod
def render_template(self, *args, **kwargs) -> Response: ...

@abstractmethod
def set_robyn(self, robyn: Robyn) -> None: ...

@abstractmethod
def get_function_url(self, function_name: str, route_type: str = "GET", **kwargs) -> str: ...
Comment on lines +38 to +39
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dave42w , should it be a part of the ABC ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put it in the ABC because I assume that if you switched from Jinja2 to a different templating engine you would still want to be able to call this function so you can have less brittle templates (ones that change if you change a subrouter prefix or the route for a function.



class JinjaTemplate(TemplateInterface):
def __init__(self, directory, encoding="utf-8", followlinks=False):
self.env = Environment(loader=FileSystemLoader(searchpath=directory, encoding=encoding, followlinks=followlinks))
def __init__(self, directory, encoding="utf-8", followlinks=False) -> None:
self.env: Environment = Environment(loader=FileSystemLoader(searchpath=directory, encoding=encoding, followlinks=followlinks))
self.add_function_to_globals("get_function_url", self.get_function_url)
self.robyn: Robyn | None = None

def add_function_to_globals(self, name: str, func: Callable):
"""
Add a global function to a Jinja environment.
"""
self.env.globals[name] = func

def set_robyn(self, robyn: Robyn) -> None:
"""
The get_function_url needs to have access to the list of routes stored in the apps Robyn object

Args:
robyn (Robyn): The top instance of the Robyn class for this app.
"""
Comment on lines +54 to +60
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sorry if I am being a bit daft but why is this function needed?

Copy link
Contributor Author

@dave42w dave42w Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we are rendering a template that includes a call into the TemplateInterface get_function_url(...) that function needs to get the live and complete list of routes. That means we can't have the TemplateInterface keep a copy of the routes that existed when it was created. Instead, when the call is made we check the live list of routes. The only place I could see that we can get that live list of routes from is the main app Robyn instance.

I couldn't see a way to get hold of the Robyn instance automagically. So it was either a set method or the constructor.

I decided to go for a set method as the constructor for JiujaTemplate already has 3 arguments plus self (I figured it will impact the existing code less and be easier to add additions templating packages)

If projects don't use get_function_url(...) then they don't need to call set_robyn.

Long term I want to find a way to get to the routes via a router singleton and to have the router do the work to return the URL. But I don't think we have that possibility yet. And it won't change the templates (only thing would be that set_robyn would not be needed but we can make it do nothing and depreciate it over a couple of releases)

self.robyn = robyn

def get_function_url(self, function_name: str, route_type: str = "GET", **kwargs) -> str:
"""Creates a link to an endpoint function name

Returns:
str: the url for the function
"""

if self.robyn is None:
return "get_function_url needs set_robyn"

routes: List[Route] = self.robyn.router.get_routes()
for r in routes:
if r.function.handler.__name__ == function_name and str(r.route_type) == f"HttpMethod.{route_type}":
if len(kwargs) > 0:
return get_param_filled_url(r.route, kwargs)
return r.route

return "route not found in Robyn router"

def render_template(self, template_name, **kwargs) -> Response:
rendered_template = self.env.get_template(template_name).render(**kwargs)
Expand Down
74 changes: 74 additions & 0 deletions unit_tests/test_get_function_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from robyn import Robyn
from robyn.templating import JinjaTemplate


def h(request):
return "Hello"


def get_hello(request):
return "get_Hello"


def post_hello(request):
return "post_Hello"


def put_hello(request):
return "put_Hello"


def delete_hello(request):
return "delete_Hello"


def patch_hello(request):
return "patch_Hello"


def options_hello(request):
return "options_Hello"


def head_hello(request):
return "head_Hello"


def test_get_function_url():
app = Robyn(__file__)
app.add_route("GET", "/", h)
app.add_route("GET", "/get_hello", get_hello)
app.add_route("POST", "/post_hello", post_hello)
app.add_route("PUT", "/put_hello", put_hello)
app.add_route("DELETE", "/delete_hello", delete_hello)
app.add_route("PATCH", "/patch_hello", patch_hello)
app.add_route("OPTIONS", "/options_hello", options_hello)
app.add_route("HEAD", "/head_hello", head_hello)

jinja_template = JinjaTemplate(".", "templates", "utf-8")
jinja_template.set_robyn(app)

assert jinja_template.get_function_url("h") == "/"
assert jinja_template.get_function_url("get_hello") == "/get_hello"
assert jinja_template.get_function_url("get_hello", "GET") == "/get_hello"
assert jinja_template.get_function_url("post_hello", "POST") == "/post_hello"
assert jinja_template.get_function_url("put_hello", "PUT") == "/put_hello"
assert jinja_template.get_function_url("delete_hello", "DELETE") == "/delete_hello"
assert jinja_template.get_function_url("patch_hello", "PATCH") == "/patch_hello"
assert jinja_template.get_function_url("options_hello", "OPTIONS") == "/options_hello"
assert jinja_template.get_function_url("head_hello", "HEAD") == "/head_hello"


def get_hello_param(request):
return "get_Hello_param"


def test_get_function_url_with_params() -> None:
app = Robyn(__file__)
app.add_route("GET", "/get_hello/:id", get_hello_param)

jinja_template = JinjaTemplate(".", "templates", "utf-8")
jinja_template.set_robyn(app)

url: str = jinja_template.get_function_url("get_hello_param", "GET", id=42)
assert url == "/get_hello/42", f"Param filled url|{url}|"
29 changes: 29 additions & 0 deletions unit_tests/test_get_param_filled_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from robyn.templating import get_param_filled_url


def test_get_param_filled_url_42() -> None:
d42: dict = {"id": 42}
assert get_param_filled_url("/get_hello/:id", d42) == "/get_hello/42"
assert get_param_filled_url("/:id", d42) == "/42"
assert get_param_filled_url("/get_hello/:idmore", d42) == "/get_hello/42more"
assert get_param_filled_url("/get_hello/:id/", d42) == "/get_hello/42/"
assert get_param_filled_url("/get_hello/:id/more", d42) == "/get_hello/42/more"


def test_get_param_filled_url_2s() -> None:
d42: dict = {"id": 42, "s": "str"}
assert get_param_filled_url("/get_hello/:id/:s", d42) == "/get_hello/42/str"
assert get_param_filled_url("/:id/:s", d42) == "/42/str"
assert get_param_filled_url("/get_hello/:id:smore", d42) == "/get_hello/42strmore"
assert get_param_filled_url("/get_hello/:id/:s/", d42) == "/get_hello/42/str/"
assert get_param_filled_url("/get_hello/:id/:s/more", d42) == "/get_hello/42/str/more"
assert get_param_filled_url("/get_hello/:s/:id/:s/more", d42) == "/get_hello/str/42/str/more"


def test_get_param_filled_url() -> None:
assert get_param_filled_url("/get_hello/:id/:s") == "/get_hello/:id/:s"
assert get_param_filled_url("/:id/:s") == "/:id/:s"
assert get_param_filled_url("/get_hello/:id:smore") == "/get_hello/:id:smore"
assert get_param_filled_url("/get_hello/:id/:s/") == "/get_hello/:id/:s/"
assert get_param_filled_url("/get_hello/:id/:s/more") == "/get_hello/:id/:s/more"
assert get_param_filled_url("/get_hello/:s/:id/:s/more") == "/get_hello/:s/:id/:s/more"
Loading