Skip to content

Commit

Permalink
Merge pull request #8 from angrybayblade/feat/named-arguments
Browse files Browse the repository at this point in the history
Named arguments
  • Loading branch information
angrybayblade authored Mar 12, 2024
2 parents f429242 + 3d77d55 commit 4b8833a
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 7 deletions.
69 changes: 66 additions & 3 deletions docs/html.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,13 @@ print(template.render(context={"name": "Jane Doe"}))
</html>
```
<!-- end -->

Using overridable views and reusable views you can create a modular and reusable codebase and save a lot of time an effort in the process.

## Function As View

You can define your view as a python function create views based on conditions, using for loops or any other kind of logical operation to build your view.

<!-- {"type": "html", "file": "examples/function_as_view.py"} -->
```python
import typing as t
Expand Down Expand Up @@ -290,8 +294,67 @@ print(template.render(context={}))
</html>
```
<!-- end -->

You can also use named arguments instead of context argument to make thing more simple.

<!-- {"type": "html", "file": "examples/function_as_view_with_args.py"} -->
```python
import typing as t

from ph7.html import body, div, html, node

user = div(class_name="user")
users = div(class_name="user")
nousers = div("Error, Users not found", class_name="error")


def render_users(number_of_users: t.Optional[int] = None) -> node:
"""Render users."""
if number_of_users is None:
return nousers
return users(user(f"User {i}") for i in range(number_of_users))


template = html(
body(
render_users,
)
)

print("<!-- With `number_of_users` parameter -->\n")
print(template.render(context={"number_of_users": 5}), end="\n\n")

print("<!-- Without `number_of_users` parameter -->\n")
print(template.render(context={}))
```

```html
<!-- With `number_of_users` parameter -->
<html>
<body>
<div class="user">
<div class="user">User 0</div>
<div class="user">User 1</div>
<div class="user">User 2</div>
<div class="user">User 3</div>
<div class="user">User 4</div>
</div>
</body>
</html>
<!-- Without `number_of_users` parameter -->
<html>
<body>
<div class="error">Error, Users not found</div>
</body>
</html>
```
<!-- end -->


## Caching

Since you can define your views as python functions you can also use caching utilities like `functools.lru_cache` to reduce rendering time.

<!-- {"type": "stdout", "file": "examples/cacheable_view.py"} -->
```python
import time
Expand Down Expand Up @@ -338,9 +401,9 @@ print(f"Third render: {time.perf_counter() - tick}")
```

```stdout
First render: 6.383804208
Second render: 0.35257962499999973
Third render: 0.34973895799999966
First render: 6.87079775
Second render: 0.3591619169999998
Third render: 0.3592319160000006
```
<!-- end -->

Expand Down
27 changes: 27 additions & 0 deletions examples/function_as_view_with_args.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import typing as t

from ph7.html import body, div, html, node

user = div(class_name="user")
users = div(class_name="user")
nousers = div("Error, Users not found", class_name="error")


def render_users(number_of_users: t.Optional[int] = None) -> node:
"""Render users."""
if number_of_users is None:
return nousers
return users(user(f"User {i}") for i in range(number_of_users))


template = html(
body(
render_users,
)
)

print("<!-- With `number_of_users` parameter -->\n")
print(template.render(context={"number_of_users": 5}), end="\n\n")

print("<!-- Without `number_of_users` parameter -->\n")
print(template.render(context={}))
55 changes: 52 additions & 3 deletions ph7/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

# pylint: disable=line-too-long,too-many-lines,redefined-outer-name,redefined-builtin,invalid-name,too-many-locals

import inspect
import re
import typing as t
from pathlib import Path
Expand All @@ -18,6 +19,8 @@

TEMPLATE_RE = re.compile(r"\$\{([a-z0-9_]+)(\|([a-zA-Z0-9_\. ]+))?\}")

P = t.ParamSpec("P")


def _wrap(child: "ChildType") -> t.Tuple["node", ...]:
"""Warp child as node type."""
Expand Down Expand Up @@ -263,21 +266,67 @@ def __init__(self, view: "CallableAsView") -> None:
self.templates = {}
self._is_overridable = False
self._overridable_name = None
self.call = self._generate_call_signature()

def _generate_call_signature(self) -> t.Callable[[t.Dict], node]:
"""Generate call signature for view."""
args = {}
kwargs = {}
defaults = {}
signature = inspect.getfullargspec(self.view)

if signature.defaults is not None:
for arg, value in zip(
reversed(signature.args), reversed(signature.defaults)
):
kwargs[arg] = signature.annotations.get(arg)
defaults[arg] = value

for arg in signature.args:
if arg in kwargs:
continue
args[arg] = signature.annotations.get(arg)

def call(context: t.Dict) -> node:
"""Render a callable view."""
call_args = {}
call_kwargs = {}
for arg in args:
if arg == "context":
call_args[arg] = context
continue
if arg not in context:
raise ValueError(
f"Error calling '{self.view.__module__}.{self.view.__name__}'; "
f"Value for argument '{arg}' not provided"
)
call_args[arg] = context[arg]
for kwarg in kwargs:
if kwarg == "context":
call_kwargs[kwarg] = context
continue
if kwarg not in context:
call_kwargs[kwarg] = defaults[kwarg]
continue
call_kwargs[kwarg] = context[kwarg]
return self.view(**call_args, **call_kwargs)

return call

def copy(self) -> "node":
"""Copy data node."""
return wrapper(view=self.view)

def render(self, context: t.Dict) -> str:
"""Render string."""
return self.view(context).render(context=context)
return self.call(context).render(context=context)

def stream(
self,
context: t.Dict,
) -> t.Generator[str, None, None]:
"""Stream chunks of response."""
yield from self.view(context).stream(context=context)
yield from self.call(context).stream(context=context)


class data(node):
Expand Down Expand Up @@ -365,7 +414,7 @@ def stream(
yield from self._stream(context=context)


CallableAsView = t.Callable[[t.Dict], node]
CallableAsView = t.Callable

ChildTypeMeta = t.Union[node, str, int, float, bool, CallableAsView]

Expand Down
2 changes: 1 addition & 1 deletion ph7/django/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def _dry_run(self, module: t.Any, context: t.Any) -> None:
continue
try:
obj.render(context=context)
except Exception as e:
except Exception as e: # pylint: disable=broad-exception-caught
errors.append(str(e))

if len(errors) == 0:
Expand Down

0 comments on commit 4b8833a

Please sign in to comment.