From e9a71481961607ad7d2d5eaea4462106d2856f37 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Thu, 28 Aug 2025 22:44:41 -0700 Subject: [PATCH 01/52] wip --- scripts/templatize.py | 38 +++++++++++++++ simple_html/utils.py | 108 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 scripts/templatize.py diff --git a/scripts/templatize.py b/scripts/templatize.py new file mode 100644 index 0000000..a29c50f --- /dev/null +++ b/scripts/templatize.py @@ -0,0 +1,38 @@ +from simple_html import * +import time +from simple_html.utils import templatize + +def html_func(name: str, age: int) -> Node: + return html( + head(title("hi, ", name)), + body( + div({"class": "content", + "blabla": "bla"}, + h1("hi ", name, "I'm ", age), + br) + ) + ) + +templatized = templatize(html_func) + +# Example usage +if __name__ == "__main__": + # result = + # print(f"Type: {type(result)}") + # print(f"Parts: {len(result)}") + # print(f"Content: [{','.join(str(part) for part in result)}]") + start_1 = time.time() + for _ in range(10000): + render(html_func(name="Hello' World", age=300)) + end_1 = time.time() - start_1 + print(end_1) + + print(render(html_func(name="Hello' World", age=300))) + + start_2 = time.time() + for _ in range(10000): + render(templatized(name="Hello' World", age=300)) + end_2 = time.time() - start_2 + print(end_2) + + print(render(templatized(name="Hello' World", age=300))) \ No newline at end of file diff --git a/simple_html/utils.py b/simple_html/utils.py index ed886e5..7be0100 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -1,6 +1,5 @@ from decimal import Decimal -from types import GeneratorType -from typing import Any, Union, Generator, Iterable, Callable, Final, TYPE_CHECKING +from typing import Any, Union, Generator, Iterable, Callable, Final, TYPE_CHECKING, Protocol class SafeString: @@ -437,3 +436,108 @@ def render(*nodes: Node) -> str: _render(nodes, results.append) return "".join(results) + + +from _decimal import Decimal +from types import GeneratorType +from typing import Literal, Union +import inspect +import sys + +TemplatePart = Union[ + tuple[Literal["STATIC"], str], + tuple[Literal["ARG"], str] # the str is the arg name +] + +class Templatizable(Protocol): + def __call__(self, **kwargs: Node) -> Node: + ... + +def templatize( + func: Templatizable, +) -> Templatizable: + # get args + sig = inspect.signature(func) + param_names = list(sig.parameters.keys()) + + # assert valid (only strings and SafeStrings allowed) + if not param_names: + raise ValueError("Function must have at least one parameter") + + # probe function with properly typed arguments + # Use interned sentinel objects that we can identify by id + sentinel_objects = {} + probe_args = {} + + for param_name in param_names: + # Create a unique string sentinel and intern it so we can find it by identity + sentinel = sys.intern(f"__SENTINEL_{param_name}_{id(object())}__") + sentinel_objects[id(sentinel)] = param_name + probe_args[param_name] = sentinel + + # Call function to get the Node tree + template_node = func(**probe_args) + + # traverse `Node` tree structure to find usages of arguments by id + template_parts: list[TemplatePart] = [] + + def traverse_node(node: Node) -> None: + if isinstance(node, str): + # Check if this string is one of our sentinels + node_id = id(node) + if node_id in sentinel_objects: + # This is an argument placeholder - add a marker + template_parts.append(('ARG', sentinel_objects[node_id])) + else: + # Regular string content + template_parts.append(('STATIC', faster_escape(node))) + elif type(node) is tuple: + # TagTuple + template_parts.append(('STATIC', node[0])) + for n in node[1]: + traverse_node(n) + template_parts.append(('STATIC', node[2])) + elif type(node) is SafeString: + # SafeString content - check if it's a sentinel + node_id = id(node.safe_str) + if node_id in sentinel_objects: + template_parts.append(('ARG', sentinel_objects[node_id])) + else: + template_parts.append(('STATIC', node.safe_str)) + elif type(node) is Tag: + template_parts.append(('STATIC', node.rendered)) + elif type(node) is list or type(node) is GeneratorType: + for item in node: + traverse_node(item) + elif isinstance(node, (int, float, Decimal)): + # Other types - convert to string + template_parts.append(('STATIC', str(node))) + + traverse_node(template_node) + + # convert non-argument nodes to strings and coalesce for speed + coalesced_parts: list[Union[str, SafeString]] = [] # string's are for parameter names + current_static = [] + + for part_type, content in template_parts: + if part_type == 'STATIC': + current_static.append(str(content)) + else: # ARG + # Flush accumulated static content + if current_static: + coalesced_parts.append(SafeString(''.join(current_static))) + current_static = [] + coalesced_parts.append(content) + + # Flush any remaining static content + if current_static: + coalesced_parts.append(SafeString(''.join(current_static))) + + # return new function -- should just be a list of SafeStrings + def template_function(**kwargs: Node) -> Node: + return [ + kwargs[part] if type(part) is str else part + for part in coalesced_parts + ] + + return template_function From 109f25eed8838dd5d1cc218f6850f923b37bd5f9 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Thu, 28 Aug 2025 22:48:41 -0700 Subject: [PATCH 02/52] wip --- scripts/templatize.py | 6 ++++-- simple_html/utils.py | 12 ++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/scripts/templatize.py b/scripts/templatize.py index a29c50f..122194e 100644 --- a/scripts/templatize.py +++ b/scripts/templatize.py @@ -1,6 +1,8 @@ -from simple_html import * import time -from simple_html.utils import templatize + +from simple_html import title, html, head, body, div, h1, br +from simple_html.utils import templatize, render, Node + def html_func(name: str, age: int) -> Node: return html( diff --git a/simple_html/utils.py b/simple_html/utils.py index 7be0100..064da21 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -1,5 +1,8 @@ +import inspect +import sys from decimal import Decimal -from typing import Any, Union, Generator, Iterable, Callable, Final, TYPE_CHECKING, Protocol +from types import GeneratorType +from typing import Any, Union, Generator, Iterable, Callable, Final, TYPE_CHECKING, Protocol, Literal class SafeString: @@ -437,13 +440,6 @@ def render(*nodes: Node) -> str: return "".join(results) - -from _decimal import Decimal -from types import GeneratorType -from typing import Literal, Union -import inspect -import sys - TemplatePart = Union[ tuple[Literal["STATIC"], str], tuple[Literal["ARG"], str] # the str is the arg name From f188f8e2c29c5e709eabc346586ad4c8f1f79f35 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Thu, 28 Aug 2025 22:56:27 -0700 Subject: [PATCH 03/52] wip --- bench/simple.py | 23 +++++++++----- simple_html/utils.py | 72 +++++++++++++++++++++++--------------------- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/bench/simple.py b/bench/simple.py index 6b92e29..c9eed8c 100644 --- a/bench/simple.py +++ b/bench/simple.py @@ -22,6 +22,7 @@ nav, a, main, section, article, aside, footer, span, img, time, blockquote, code, pre, form, label, input_, textarea, button, table, thead, tbody, tr, th, td ) +from simple_html.utils import templatize def hello_world_empty(objs: List[None]) -> None: @@ -118,19 +119,16 @@ def lorem_ipsum(titles: List[str]) -> None: ) -def large_page(titles: list[str]) -> None: - for t in titles: - render( - DOCTYPE_HTML5, - html({"lang": "en"}, - head( +@templatize +def _get_head(title_: str) -> None: + return head( meta({"charset": "UTF-8"}), meta({"name": "viewport", "content": "width=device-width, initial-scale=1.0"}), meta({"name": "description", "content": "Tech Insights - Your source for the latest in web development, AI, and programming trends"}), meta({"name": "keywords", "content": "web development, programming, AI, JavaScript, Python, tech news"}), - title(t), + title(title_), link({"rel": "stylesheet", "href": "/static/css/main.css"}), link({"rel": "icon", "type": "image/x-icon", "href": "/favicon.ico"}), style(""" @@ -150,7 +148,16 @@ def large_page(titles: list[str]) -> None: .stats-table th, .stats-table td { padding: 0.5rem; border: 1px solid #ddd; text-align: left; } .stats-table th { background: #f0f0f0; } """) - ), + ) + + + +def large_page(titles: list[str]) -> None: + for t in titles: + render( + DOCTYPE_HTML5, + html({"lang": "en"}, + _get_head(title_=t), body( header({"class": "site-header"}, div({"class": "container"}, diff --git a/simple_html/utils.py b/simple_html/utils.py index 064da21..2b6bc97 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -449,9 +449,45 @@ class Templatizable(Protocol): def __call__(self, **kwargs: Node) -> Node: ... +def _traverse_node(node: Node, + template_parts: list[TemplatePart], + sentinel_objects: dict[int, str]) -> None: + if isinstance(node, str): + # Check if this string is one of our sentinels + node_id = id(node) + if node_id in sentinel_objects: + # This is an argument placeholder - add a marker + template_parts.append(('ARG', sentinel_objects[node_id])) + else: + # Regular string content + template_parts.append(('STATIC', faster_escape(node))) + elif type(node) is tuple: + # TagTuple + template_parts.append(('STATIC', node[0])) + for n in node[1]: + _traverse_node(n, template_parts, sentinel_objects) + template_parts.append(('STATIC', node[2])) + elif type(node) is SafeString: + # SafeString content - check if it's a sentinel + node_id = id(node.safe_str) + if node_id in sentinel_objects: + template_parts.append(('ARG', sentinel_objects[node_id])) + else: + template_parts.append(('STATIC', node.safe_str)) + elif type(node) is Tag: + template_parts.append(('STATIC', node.rendered)) + elif type(node) is list or type(node) is GeneratorType: + for item in node: + _traverse_node(item, template_parts, sentinel_objects) + elif isinstance(node, (int, float, Decimal)): + # Other types - convert to string + template_parts.append(('STATIC', str(node))) + + def templatize( func: Templatizable, ) -> Templatizable: + # TODO: clean up allowed args # get args sig = inspect.signature(func) param_names = list(sig.parameters.keys()) @@ -462,7 +498,7 @@ def templatize( # probe function with properly typed arguments # Use interned sentinel objects that we can identify by id - sentinel_objects = {} + sentinel_objects: dict[int, str] = {} probe_args = {} for param_name in param_names: @@ -477,39 +513,7 @@ def templatize( # traverse `Node` tree structure to find usages of arguments by id template_parts: list[TemplatePart] = [] - def traverse_node(node: Node) -> None: - if isinstance(node, str): - # Check if this string is one of our sentinels - node_id = id(node) - if node_id in sentinel_objects: - # This is an argument placeholder - add a marker - template_parts.append(('ARG', sentinel_objects[node_id])) - else: - # Regular string content - template_parts.append(('STATIC', faster_escape(node))) - elif type(node) is tuple: - # TagTuple - template_parts.append(('STATIC', node[0])) - for n in node[1]: - traverse_node(n) - template_parts.append(('STATIC', node[2])) - elif type(node) is SafeString: - # SafeString content - check if it's a sentinel - node_id = id(node.safe_str) - if node_id in sentinel_objects: - template_parts.append(('ARG', sentinel_objects[node_id])) - else: - template_parts.append(('STATIC', node.safe_str)) - elif type(node) is Tag: - template_parts.append(('STATIC', node.rendered)) - elif type(node) is list or type(node) is GeneratorType: - for item in node: - traverse_node(item) - elif isinstance(node, (int, float, Decimal)): - # Other types - convert to string - template_parts.append(('STATIC', str(node))) - - traverse_node(template_node) + _traverse_node(template_node, [], sentinel_objects) # convert non-argument nodes to strings and coalesce for speed coalesced_parts: list[Union[str, SafeString]] = [] # string's are for parameter names From 11740aeaae0b9aca03a6d5f405ad0d9b01c2bd4d Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Thu, 28 Aug 2025 23:09:01 -0700 Subject: [PATCH 04/52] wip --- bench/simple.py | 4 ++-- simple_html/utils.py | 18 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/bench/simple.py b/bench/simple.py index c9eed8c..c2c52a2 100644 --- a/bench/simple.py +++ b/bench/simple.py @@ -22,7 +22,7 @@ nav, a, main, section, article, aside, footer, span, img, time, blockquote, code, pre, form, label, input_, textarea, button, table, thead, tbody, tr, th, td ) -from simple_html.utils import templatize +from simple_html.utils import templatize, Node def hello_world_empty(objs: List[None]) -> None: @@ -120,7 +120,7 @@ def lorem_ipsum(titles: List[str]) -> None: @templatize -def _get_head(title_: str) -> None: +def _get_head(title_: str) -> Node: return head( meta({"charset": "UTF-8"}), meta({"name": "viewport", "content": "width=device-width, initial-scale=1.0"}), diff --git a/simple_html/utils.py b/simple_html/utils.py index 2b6bc97..bce2329 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -452,7 +452,13 @@ def __call__(self, **kwargs: Node) -> Node: def _traverse_node(node: Node, template_parts: list[TemplatePart], sentinel_objects: dict[int, str]) -> None: - if isinstance(node, str): + if type(node) is tuple: + # TagTuple + template_parts.append(('STATIC', node[0])) + for n in node[1]: + _traverse_node(n, template_parts, sentinel_objects) + template_parts.append(('STATIC', node[2])) + elif type(node) is str: # Check if this string is one of our sentinels node_id = id(node) if node_id in sentinel_objects: @@ -461,12 +467,6 @@ def _traverse_node(node: Node, else: # Regular string content template_parts.append(('STATIC', faster_escape(node))) - elif type(node) is tuple: - # TagTuple - template_parts.append(('STATIC', node[0])) - for n in node[1]: - _traverse_node(n, template_parts, sentinel_objects) - template_parts.append(('STATIC', node[2])) elif type(node) is SafeString: # SafeString content - check if it's a sentinel node_id = id(node.safe_str) @@ -482,6 +482,8 @@ def _traverse_node(node: Node, elif isinstance(node, (int, float, Decimal)): # Other types - convert to string template_parts.append(('STATIC', str(node))) + else: + raise TypeError(f"Got unexpected type for node: {type(node)}") def templatize( @@ -513,7 +515,7 @@ def templatize( # traverse `Node` tree structure to find usages of arguments by id template_parts: list[TemplatePart] = [] - _traverse_node(template_node, [], sentinel_objects) + _traverse_node(template_node, template_parts, sentinel_objects) # convert non-argument nodes to strings and coalesce for speed coalesced_parts: list[Union[str, SafeString]] = [] # string's are for parameter names From 91bbec8793f3106ee4e999782f15e07a645c425c Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Thu, 28 Aug 2025 23:12:14 -0700 Subject: [PATCH 05/52] wip --- bench/simple.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/bench/simple.py b/bench/simple.py index c2c52a2..0209313 100644 --- a/bench/simple.py +++ b/bench/simple.py @@ -150,13 +150,9 @@ def _get_head(title_: str) -> Node: """) ) - - -def large_page(titles: list[str]) -> None: - for t in titles: - render( - DOCTYPE_HTML5, - html({"lang": "en"}, +@templatize +def _html(t: str) -> Node: + return html({"lang": "en"}, _get_head(title_=t), body( header({"class": "site-header"}, @@ -471,4 +467,12 @@ def large_page(titles: list[str]) -> None: ) ) ) - ) + + + +def large_page(titles: list[str]) -> None: + for t in titles: + render( + DOCTYPE_HTML5, + _html(t=t) + ) \ No newline at end of file From 1acceae5e6cdbb6341fa7382e82fea4977e9b1be Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 08:47:21 -0700 Subject: [PATCH 06/52] wip --- simple_html/utils.py | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/simple_html/utils.py b/simple_html/utils.py index bce2329..70272e2 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -440,7 +440,7 @@ def render(*nodes: Node) -> str: return "".join(results) -TemplatePart = Union[ +_TemplatePart = Union[ tuple[Literal["STATIC"], str], tuple[Literal["ARG"], str] # the str is the arg name ] @@ -450,8 +450,9 @@ def __call__(self, **kwargs: Node) -> Node: ... def _traverse_node(node: Node, - template_parts: list[TemplatePart], + template_parts: list[_TemplatePart], sentinel_objects: dict[int, str]) -> None: + # note that this should stay up-to-speed with the `Node` definition if type(node) is tuple: # TagTuple template_parts.append(('STATIC', node[0])) @@ -485,16 +486,11 @@ def _traverse_node(node: Node, else: raise TypeError(f"Got unexpected type for node: {type(node)}") - -def templatize( - func: Templatizable, -) -> Templatizable: - # TODO: clean up allowed args - # get args +def _probe_func(func: Templatizable) -> list[_TemplatePart]: + # TODO: try different types of arguments...? sig = inspect.signature(func) - param_names = list(sig.parameters.keys()) + param_names = sig.parameters.keys() - # assert valid (only strings and SafeStrings allowed) if not param_names: raise ValueError("Function must have at least one parameter") @@ -505,7 +501,7 @@ def templatize( for param_name in param_names: # Create a unique string sentinel and intern it so we can find it by identity - sentinel = sys.intern(f"__SENTINEL_{param_name}_{id(object())}__") + sentinel = f"__SENTINEL_{param_name}_{id(object())}__" sentinel_objects[id(sentinel)] = param_name probe_args[param_name] = sentinel @@ -513,15 +509,22 @@ def templatize( template_node = func(**probe_args) # traverse `Node` tree structure to find usages of arguments by id - template_parts: list[TemplatePart] = [] + template_parts: list[_TemplatePart] = [] _traverse_node(template_node, template_parts, sentinel_objects) + return template_parts + +_CoalescedPart = Union[str, SafeString] + +def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: + template_parts_1 = _probe_func(func) + # convert non-argument nodes to strings and coalesce for speed - coalesced_parts: list[Union[str, SafeString]] = [] # string's are for parameter names - current_static = [] + coalesced_parts: list[_CoalescedPart] = [] # string's are for parameter names + current_static: list[str] = [] - for part_type, content in template_parts: + for part_type, content in template_parts_1: if part_type == 'STATIC': current_static.append(str(content)) else: # ARG @@ -535,6 +538,11 @@ def templatize( if current_static: coalesced_parts.append(SafeString(''.join(current_static))) + return coalesced_parts + +def templatize(func: Templatizable) -> Templatizable: + coalesced_parts = _coalesce_func(func) + # return new function -- should just be a list of SafeStrings def template_function(**kwargs: Node) -> Node: return [ From 3f3cfdd9cf45705174b446274796fe6f02ae364d Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 08:55:10 -0700 Subject: [PATCH 07/52] basic test --- tests/test_simple_html.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/tests/test_simple_html.py b/tests/test_simple_html.py index bda13af..23b9ca9 100644 --- a/tests/test_simple_html.py +++ b/tests/test_simple_html.py @@ -21,9 +21,9 @@ DOCTYPE_HTML5, render, render_styles, - img, + img, title, h1, ) -from simple_html.utils import escape_attribute_key +from simple_html.utils import escape_attribute_key, templatize def test_renders_no_children() -> None: @@ -266,4 +266,30 @@ def test_tag_repr() -> None: assert repr(img) == "Tag(name='img', self_closing=True)" def test_render_number_attributes() -> None: - assert render(div({"x": 1, "y": 2.01, "z": Decimal("3.02")})) == '
' \ No newline at end of file + assert render(div({"x": 1, "y": 2.01, "z": Decimal("3.02")})) == '
' + +def test_templatize() -> None: + def greet(name: str, age: int) -> Node: + return html( + head(title("hi, ", name)), + body( + div({"class": "content", + "blabla": "bla"}, + # raw str / int + h1("hi ", name, "I'm ", age), + # tag + br, + # list + ["wow"], + # generator + (name for _ in range(3))) + ) + ) + + + expected = """hi, John Doe

hi John DoeI'm 100


wowJohn DoeJohn DoeJohn Doe
""" + assert render(greet("John Doe", 100)) == expected + + templatized = templatize(greet) + assert render(templatized(name="John Doe", age=100)) == expected + From c180e0b699d29275aee4ef4503a8092f8189e369 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 09:41:58 -0700 Subject: [PATCH 08/52] wip --- simple_html/utils.py | 35 ++++++++++++++++++++++++++++------- tests/test_simple_html.py | 13 +++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/simple_html/utils.py b/simple_html/utils.py index 70272e2..b845670 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -486,7 +486,7 @@ def _traverse_node(node: Node, else: raise TypeError(f"Got unexpected type for node: {type(node)}") -def _probe_func(func: Templatizable) -> list[_TemplatePart]: +def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_TemplatePart]: # TODO: try different types of arguments...? sig = inspect.signature(func) param_names = sig.parameters.keys() @@ -499,11 +499,24 @@ def _probe_func(func: Templatizable) -> list[_TemplatePart]: sentinel_objects: dict[int, str] = {} probe_args = {} - for param_name in param_names: - # Create a unique string sentinel and intern it so we can find it by identity - sentinel = f"__SENTINEL_{param_name}_{id(object())}__" - sentinel_objects[id(sentinel)] = param_name - probe_args[param_name] = sentinel + if variant == 1: + for param_name in param_names: + # Create a unique string sentinel and intern it so we can find it by identity + sentinel = f"__SENTINEL_{param_name}_{id(object())}__" + sentinel_objects[id(sentinel)] = param_name + probe_args[param_name] = sentinel + elif variant == 2: + for param_name in param_names: + # Create a unique string sentinel and intern it so we can find it by identity + sentinel = [id(object())] + sentinel_objects[id(sentinel)] = param_name + probe_args[param_name] = sentinel + else: + for param_name in param_names: + # Create a unique string sentinel and intern it so we can find it by identity + sentinel = 1039917274618672531762351823761235 + id(object()) + sentinel_objects[id(sentinel)] = param_name + probe_args[param_name] = sentinel # Call function to get the Node tree template_node = func(**probe_args) @@ -518,7 +531,15 @@ def _probe_func(func: Templatizable) -> list[_TemplatePart]: _CoalescedPart = Union[str, SafeString] def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: - template_parts_1 = _probe_func(func) + template_parts_1 = _probe_func(func, 1) + template_parts_2 = _probe_func(func, 2) + template_parts_3 = _probe_func(func, 3) + assert len(template_parts_1) == len(template_parts_2) == len(template_parts_3) + + for part_1, part_2, part_3 in zip(template_parts_1, template_parts_2, template_parts_3): + assert part_1[0] == part_2[0] == part_3[0] + if part_1[0] == "STATIC": + assert part_1[1] == part_2[1] == part_3[1], "Could not templatize. Templatizable functions should not perform logic." # convert non-argument nodes to strings and coalesce for speed coalesced_parts: list[_CoalescedPart] = [] # string's are for parameter names diff --git a/tests/test_simple_html.py b/tests/test_simple_html.py index 23b9ca9..1b1bb70 100644 --- a/tests/test_simple_html.py +++ b/tests/test_simple_html.py @@ -2,6 +2,8 @@ from decimal import Decimal from typing import Generator +import pytest + from simple_html import ( SafeString, a, @@ -293,3 +295,14 @@ def greet(name: str, age: int) -> Node: templatized = templatize(greet) assert render(templatized(name="John Doe", age=100)) == expected +def test_templatize_fails_for_arbitrary_logic() -> None: + def greet(name: str) -> Node: + return html("Your name is ", name, " and your name is ", len(name), " characters long") + + with pytest.raises(AssertionError): + templatize(greet) + + +# def test_templatize_fails_for_differently_sized_parts() -> None: +# def greet(name: str) -> Node: +# if name \ No newline at end of file From 76c77d632147f90d5466b44cda0933db51df5281 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 09:46:33 -0700 Subject: [PATCH 09/52] wip --- simple_html/utils.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/simple_html/utils.py b/simple_html/utils.py index b845670..1c18849 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -452,37 +452,44 @@ def __call__(self, **kwargs: Node) -> Node: def _traverse_node(node: Node, template_parts: list[_TemplatePart], sentinel_objects: dict[int, str]) -> None: + + def append_static(obj: str) -> _TemplatePart: + return template_parts.append(("STATIC", obj)) + + def append_arg(arg: str) -> _TemplatePart: + return template_parts.append(("ARG", arg)) + # note that this should stay up-to-speed with the `Node` definition if type(node) is tuple: # TagTuple - template_parts.append(('STATIC', node[0])) + append_static(node[0]) for n in node[1]: _traverse_node(n, template_parts, sentinel_objects) - template_parts.append(('STATIC', node[2])) + append_static(node[2]) elif type(node) is str: # Check if this string is one of our sentinels node_id = id(node) if node_id in sentinel_objects: # This is an argument placeholder - add a marker - template_parts.append(('ARG', sentinel_objects[node_id])) + append_arg(sentinel_objects[node_id]) else: # Regular string content - template_parts.append(('STATIC', faster_escape(node))) + append_static(faster_escape(node)) elif type(node) is SafeString: # SafeString content - check if it's a sentinel node_id = id(node.safe_str) if node_id in sentinel_objects: - template_parts.append(('ARG', sentinel_objects[node_id])) + append_arg(sentinel_objects[node_id]) else: - template_parts.append(('STATIC', node.safe_str)) + append_static(node.safe_str) elif type(node) is Tag: - template_parts.append(('STATIC', node.rendered)) + append_static(node.rendered) elif type(node) is list or type(node) is GeneratorType: for item in node: _traverse_node(item, template_parts, sentinel_objects) elif isinstance(node, (int, float, Decimal)): # Other types - convert to string - template_parts.append(('STATIC', str(node))) + append_static(str(node)) else: raise TypeError(f"Got unexpected type for node: {type(node)}") From 9245013f7eecb1ff595a2e54b321fd1ff4b1b929 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 09:48:55 -0700 Subject: [PATCH 10/52] wip --- simple_html/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/simple_html/utils.py b/simple_html/utils.py index 1c18849..05cdf8f 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -485,6 +485,14 @@ def append_arg(arg: str) -> _TemplatePart: elif type(node) is Tag: append_static(node.rendered) elif type(node) is list or type(node) is GeneratorType: + node_id = id(node) + if node_id in sentinel_objects: + # This is an argument placeholder - add a marker + append_arg(sentinel_objects[node_id]) + else: + for item in node: + _traverse_node(item, template_parts, sentinel_objects) + elif type(node) is GeneratorType: for item in node: _traverse_node(item, template_parts, sentinel_objects) elif isinstance(node, (int, float, Decimal)): From a400902bf4e5a7f5a0f1c0e8be20ccb6310cfd4b Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 14:16:11 -0700 Subject: [PATCH 11/52] wip --- simple_html/utils.py | 49 +++++++++++++++++++++++++-------------- tests/test_simple_html.py | 9 +++++-- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/simple_html/utils.py b/simple_html/utils.py index 05cdf8f..0ae5ffa 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -1,5 +1,4 @@ import inspect -import sys from decimal import Decimal from types import GeneratorType from typing import Any, Union, Generator, Iterable, Callable, Final, TYPE_CHECKING, Protocol, Literal @@ -449,6 +448,8 @@ class Templatizable(Protocol): def __call__(self, **kwargs: Node) -> Node: ... +_CANNOT_TEMPLATIZE_ERROR: Final[str] = "Could not templatize. Templatizable functions should not perform logic." + def _traverse_node(node: Node, template_parts: list[_TemplatePart], sentinel_objects: dict[int, str]) -> None: @@ -459,6 +460,8 @@ def append_static(obj: str) -> _TemplatePart: def append_arg(arg: str) -> _TemplatePart: return template_parts.append(("ARG", arg)) + node_id = id(node) + # note that this should stay up-to-speed with the `Node` definition if type(node) is tuple: # TagTuple @@ -468,7 +471,6 @@ def append_arg(arg: str) -> _TemplatePart: append_static(node[2]) elif type(node) is str: # Check if this string is one of our sentinels - node_id = id(node) if node_id in sentinel_objects: # This is an argument placeholder - add a marker append_arg(sentinel_objects[node_id]) @@ -477,15 +479,13 @@ def append_arg(arg: str) -> _TemplatePart: append_static(faster_escape(node)) elif type(node) is SafeString: # SafeString content - check if it's a sentinel - node_id = id(node.safe_str) if node_id in sentinel_objects: append_arg(sentinel_objects[node_id]) else: append_static(node.safe_str) elif type(node) is Tag: append_static(node.rendered) - elif type(node) is list or type(node) is GeneratorType: - node_id = id(node) + elif type(node) is list: if node_id in sentinel_objects: # This is an argument placeholder - add a marker append_arg(sentinel_objects[node_id]) @@ -496,8 +496,11 @@ def append_arg(arg: str) -> _TemplatePart: for item in node: _traverse_node(item, template_parts, sentinel_objects) elif isinstance(node, (int, float, Decimal)): - # Other types - convert to string - append_static(str(node)) + if node_id in sentinel_objects: + append_arg(sentinel_objects[node_id]) + else: + # Other types - convert to string + append_static(str(node)) else: raise TypeError(f"Got unexpected type for node: {type(node)}") @@ -533,8 +536,16 @@ def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_Templat sentinel_objects[id(sentinel)] = param_name probe_args[param_name] = sentinel - # Call function to get the Node tree - template_node = func(**probe_args) + try: + # Call function to get the Node tree + template_node = func(**probe_args) + except Exception as e: + raise Exception( + e, + AssertionError(_CANNOT_TEMPLATIZE_ERROR) + ) + + # traverse `Node` tree structure to find usages of arguments by id template_parts: list[_TemplatePart] = [] @@ -546,21 +557,25 @@ def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_Templat _CoalescedPart = Union[str, SafeString] def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: - template_parts_1 = _probe_func(func, 1) - template_parts_2 = _probe_func(func, 2) - template_parts_3 = _probe_func(func, 3) - assert len(template_parts_1) == len(template_parts_2) == len(template_parts_3) + template_part_lists: tuple[list[_TemplatePart], list[_TemplatePart], list[_TemplatePart]] = ( + _probe_func(func, 1), + _probe_func(func, 2), + _probe_func(func, 3) + ) + assert len(template_part_lists[0]) == len(template_part_lists[1]) == len(template_part_lists[2]), _CANNOT_TEMPLATIZE_ERROR - for part_1, part_2, part_3 in zip(template_parts_1, template_parts_2, template_parts_3): - assert part_1[0] == part_2[0] == part_3[0] + for part_1, part_2, part_3 in zip(*template_part_lists): + assert part_1[0] == part_2[0] == part_3[0], _CANNOT_TEMPLATIZE_ERROR if part_1[0] == "STATIC": - assert part_1[1] == part_2[1] == part_3[1], "Could not templatize. Templatizable functions should not perform logic." + print(part_1[1], part_2[1], part_3[1]) + print(part_1[1] == part_2[1] == part_3[1]) + assert (part_1[1] == part_2[1] == part_3[1]), _CANNOT_TEMPLATIZE_ERROR # convert non-argument nodes to strings and coalesce for speed coalesced_parts: list[_CoalescedPart] = [] # string's are for parameter names current_static: list[str] = [] - for part_type, content in template_parts_1: + for part_type, content in template_part_lists[0]: if part_type == 'STATIC': current_static.append(str(content)) else: # ARG diff --git a/tests/test_simple_html.py b/tests/test_simple_html.py index 1b1bb70..7ca05f9 100644 --- a/tests/test_simple_html.py +++ b/tests/test_simple_html.py @@ -284,7 +284,8 @@ def greet(name: str, age: int) -> Node: # list ["wow"], # generator - (name for _ in range(3))) + (name for _ in range(3)) + ) ) ) @@ -297,7 +298,11 @@ def greet(name: str, age: int) -> Node: def test_templatize_fails_for_arbitrary_logic() -> None: def greet(name: str) -> Node: - return html("Your name is ", name, " and your name is ", len(name), " characters long") + return html("Your name is ", + name, + " and this is what it looks like twice: ", + name + name + ) with pytest.raises(AssertionError): templatize(greet) From 5de9eb74065fcbe4bb98ed6bc9e636b1a4d105be Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 15:11:54 -0700 Subject: [PATCH 12/52] wip --- simple_html/utils.py | 103 +++++++++++++++++++++++++------------- tests/test_simple_html.py | 5 +- 2 files changed, 71 insertions(+), 37 deletions(-) diff --git a/simple_html/utils.py b/simple_html/utils.py index 0ae5ffa..6e70457 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -1,7 +1,7 @@ import inspect from decimal import Decimal from types import GeneratorType -from typing import Any, Union, Generator, Iterable, Callable, Final, TYPE_CHECKING, Protocol, Literal +from typing import Any, Union, Generator, Iterable, Callable, Final, TYPE_CHECKING, Protocol, Literal, Never class SafeString: @@ -439,17 +439,16 @@ def render(*nodes: Node) -> str: return "".join(results) +_ARG_LOCATION = Union[str, int, tuple[int, str]] _TemplatePart = Union[ tuple[Literal["STATIC"], str], - tuple[Literal["ARG"], str] # the str is the arg name + tuple[Literal["ARG"], _ARG_LOCATION] # the str is the arg name ] class Templatizable(Protocol): def __call__(self, **kwargs: Node) -> Node: ... -_CANNOT_TEMPLATIZE_ERROR: Final[str] = "Could not templatize. Templatizable functions should not perform logic." - def _traverse_node(node: Node, template_parts: list[_TemplatePart], sentinel_objects: dict[int, str]) -> None: @@ -457,7 +456,7 @@ def _traverse_node(node: Node, def append_static(obj: str) -> _TemplatePart: return template_parts.append(("STATIC", obj)) - def append_arg(arg: str) -> _TemplatePart: + def append_arg(arg: _ARG_LOCATION) -> _TemplatePart: return template_parts.append(("ARG", arg)) node_id = id(node) @@ -473,7 +472,9 @@ def append_arg(arg: str) -> _TemplatePart: # Check if this string is one of our sentinels if node_id in sentinel_objects: # This is an argument placeholder - add a marker - append_arg(sentinel_objects[node_id]) + append_arg( + sentinel_objects[node_id] + ) else: # Regular string content append_static(faster_escape(node)) @@ -504,49 +505,67 @@ def append_arg(arg: str) -> _TemplatePart: else: raise TypeError(f"Got unexpected type for node: {type(node)}") +def _cannot_templatize_message(func: Callable[[...], Any], + extra_message: str) -> str: + return f"Could not templatize function '{func.__name__}'. {extra_message}" + +_SHOULD_NOT_PERFORM_LOGIC = "Templatizable functions should not perform logic." +_NO_ARGS_OR_KWARGS = "Templatizable functions cannot accept *args or **kwargs." + def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_TemplatePart]: # TODO: try different types of arguments...? sig = inspect.signature(func) - param_names = sig.parameters.keys() + parameters = sig.parameters - if not param_names: + if not parameters: raise ValueError("Function must have at least one parameter") # probe function with properly typed arguments # Use interned sentinel objects that we can identify by id - sentinel_objects: dict[int, str] = {} - probe_args = {} + sentinel_objects: dict[int, _ARG_LOCATION] = {} + probe_args = [] + probe_kwargs = {} - if variant == 1: - for param_name in param_names: + for i, (param_name, param) in enumerate(parameters.items()): + if variant == 1: # Create a unique string sentinel and intern it so we can find it by identity sentinel = f"__SENTINEL_{param_name}_{id(object())}__" - sentinel_objects[id(sentinel)] = param_name - probe_args[param_name] = sentinel - elif variant == 2: - for param_name in param_names: + elif variant == 2: # Create a unique string sentinel and intern it so we can find it by identity sentinel = [id(object())] - sentinel_objects[id(sentinel)] = param_name - probe_args[param_name] = sentinel - else: - for param_name in param_names: + else: # Create a unique string sentinel and intern it so we can find it by identity sentinel = 1039917274618672531762351823761235 + id(object()) - sentinel_objects[id(sentinel)] = param_name - probe_args[param_name] = sentinel + + sentinel_id = id(sentinel) + + # Determine how to pass this parameter + if param.kind == inspect.Parameter.POSITIONAL_ONLY: + probe_args.append(sentinel) + sentinel_objects[sentinel_id] = i + elif param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: + # For mixed parameters, we could pass as positional or keyword + # Let's pass as positional if it's among the first parameters + probe_args.append(sentinel) + sentinel_objects[sentinel_id] = (i, param.name) + elif param.kind == inspect.Parameter.KEYWORD_ONLY: + probe_kwargs[param_name] = sentinel + sentinel_objects[sentinel_id] = param.name + elif param.kind == inspect.Parameter.VAR_POSITIONAL: + raise AssertionError(_cannot_templatize_message(func, _NO_ARGS_OR_KWARGS)) + + elif param.kind == inspect.Parameter.VAR_KEYWORD: + raise AssertionError(_cannot_templatize_message(func, _NO_ARGS_OR_KWARGS)) try: - # Call function to get the Node tree - template_node = func(**probe_args) + # Call function with both args and kwargs + template_node = func(*probe_args, **probe_kwargs) except Exception as e: raise Exception( e, - AssertionError(_CANNOT_TEMPLATIZE_ERROR) + AssertionError(_cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC)) ) - - # traverse `Node` tree structure to find usages of arguments by id template_parts: list[_TemplatePart] = [] @@ -554,7 +573,8 @@ def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_Templat return template_parts -_CoalescedPart = Union[str, SafeString] + +_CoalescedPart = Union[_ARG_LOCATION, SafeString] def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: template_part_lists: tuple[list[_TemplatePart], list[_TemplatePart], list[_TemplatePart]] = ( @@ -562,14 +582,12 @@ def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: _probe_func(func, 2), _probe_func(func, 3) ) - assert len(template_part_lists[0]) == len(template_part_lists[1]) == len(template_part_lists[2]), _CANNOT_TEMPLATIZE_ERROR + assert len(template_part_lists[0]) == len(template_part_lists[1]) == len(template_part_lists[2]), _cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC) for part_1, part_2, part_3 in zip(*template_part_lists): - assert part_1[0] == part_2[0] == part_3[0], _CANNOT_TEMPLATIZE_ERROR + assert part_1[0] == part_2[0] == part_3[0], _cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC) if part_1[0] == "STATIC": - print(part_1[1], part_2[1], part_3[1]) - print(part_1[1] == part_2[1] == part_3[1]) - assert (part_1[1] == part_2[1] == part_3[1]), _CANNOT_TEMPLATIZE_ERROR + assert (part_1[1] == part_2[1] == part_3[1]), _cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC) # convert non-argument nodes to strings and coalesce for speed coalesced_parts: list[_CoalescedPart] = [] # string's are for parameter names @@ -591,13 +609,28 @@ def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: return coalesced_parts +def get_arg_val(args: tuple[Node, ...], + kwargs: dict[str, Node], + location: _ARG_LOCATION) -> Node: + if type(location) is tuple: + int_loc, str_loc = location + if len(args) >= int_loc + 1: + return args[int_loc] + else: + return kwargs[str_loc] + elif type(location) is int: + return args[location] + else: + return kwargs[location] + + def templatize(func: Templatizable) -> Templatizable: coalesced_parts = _coalesce_func(func) # return new function -- should just be a list of SafeStrings - def template_function(**kwargs: Node) -> Node: + def template_function(*args: Node, **kwargs: Node) -> Node: return [ - kwargs[part] if type(part) is str else part + part if type(part) is SafeString else get_arg_val(args, kwargs, part) for part in coalesced_parts ] diff --git a/tests/test_simple_html.py b/tests/test_simple_html.py index 7ca05f9..283ea64 100644 --- a/tests/test_simple_html.py +++ b/tests/test_simple_html.py @@ -295,14 +295,15 @@ def greet(name: str, age: int) -> Node: templatized = templatize(greet) assert render(templatized(name="John Doe", age=100)) == expected + assert render(templatized("John Doe", age=100)) == expected + assert render(templatized("John Doe", 100)) == expected def test_templatize_fails_for_arbitrary_logic() -> None: def greet(name: str) -> Node: return html("Your name is ", name, " and this is what it looks like twice: ", - name + name - ) + name + name) with pytest.raises(AssertionError): templatize(greet) From 4eb5f62f6beaf8459c3002f0c68b3d4bfbfd9e3d Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 15:15:30 -0700 Subject: [PATCH 13/52] wip --- tests/test_simple_html.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/test_simple_html.py b/tests/test_simple_html.py index 283ea64..07d5091 100644 --- a/tests/test_simple_html.py +++ b/tests/test_simple_html.py @@ -1,5 +1,6 @@ import json from decimal import Decimal +from itertools import cycle from typing import Generator import pytest @@ -309,6 +310,17 @@ def greet(name: str) -> Node: templatize(greet) -# def test_templatize_fails_for_differently_sized_parts() -> None: -# def greet(name: str) -> Node: -# if name \ No newline at end of file +def test_templatize_fails_for_differently_sized_parts() -> None: + size = cycle([1, 2]) + def greet(name: str) -> Node: + if next(size) == 1: + return div(name) + else: + return div(name, "bad") + + assert render(greet("abc")) == "
abc
" + assert render(greet("abc")) == "
abcbad
" + + with pytest.raises(AssertionError): + templatize(greet) + From 92be98d2f4e1564e9e40ba4e5d84309aaad0d800 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 15:25:08 -0700 Subject: [PATCH 14/52] wip --- simple_html/utils.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/simple_html/utils.py b/simple_html/utils.py index 6e70457..3df6a86 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -451,12 +451,12 @@ def __call__(self, **kwargs: Node) -> Node: def _traverse_node(node: Node, template_parts: list[_TemplatePart], - sentinel_objects: dict[int, str]) -> None: + sentinel_objects: dict[int, _ARG_LOCATION]) -> None: - def append_static(obj: str) -> _TemplatePart: + def append_static(obj: str) -> None: return template_parts.append(("STATIC", obj)) - def append_arg(arg: _ARG_LOCATION) -> _TemplatePart: + def append_arg(arg: _ARG_LOCATION) -> None: return template_parts.append(("ARG", arg)) node_id = id(node) @@ -505,7 +505,7 @@ def append_arg(arg: _ARG_LOCATION) -> _TemplatePart: else: raise TypeError(f"Got unexpected type for node: {type(node)}") -def _cannot_templatize_message(func: Callable[[...], Any], +def _cannot_templatize_message(func: Callable[..., Any], extra_message: str) -> str: return f"Could not templatize function '{func.__name__}'. {extra_message}" @@ -526,6 +526,7 @@ def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_Templat probe_args = [] probe_kwargs = {} + sentinel: Union[str, list[int], int] for i, (param_name, param) in enumerate(parameters.items()): if variant == 1: # Create a unique string sentinel and intern it so we can find it by identity @@ -612,13 +613,13 @@ def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: def get_arg_val(args: tuple[Node, ...], kwargs: dict[str, Node], location: _ARG_LOCATION) -> Node: - if type(location) is tuple: + if isinstance(location, tuple): int_loc, str_loc = location if len(args) >= int_loc + 1: return args[int_loc] else: return kwargs[str_loc] - elif type(location) is int: + elif isinstance(location, int): return args[location] else: return kwargs[location] @@ -630,7 +631,7 @@ def templatize(func: Templatizable) -> Templatizable: # return new function -- should just be a list of SafeStrings def template_function(*args: Node, **kwargs: Node) -> Node: return [ - part if type(part) is SafeString else get_arg_val(args, kwargs, part) + part if isinstance(part, SafeString) else get_arg_val(args, kwargs, part) for part in coalesced_parts ] From 1c7fd572cf13f3075067ef87432d71dc88130a2c Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 16:02:59 -0700 Subject: [PATCH 15/52] wip --- simple_html/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/simple_html/utils.py b/simple_html/utils.py index 3df6a86..7b57b5b 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -523,10 +523,10 @@ def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_Templat # probe function with properly typed arguments # Use interned sentinel objects that we can identify by id sentinel_objects: dict[int, _ARG_LOCATION] = {} - probe_args = [] - probe_kwargs = {} + probe_args: list[Node] = [] + probe_kwargs: dict[str, Node] = {} - sentinel: Union[str, list[int], int] + sentinel: Node for i, (param_name, param) in enumerate(parameters.items()): if variant == 1: # Create a unique string sentinel and intern it so we can find it by identity From dd79e91e9c57ef52fae9e736f9f682a494e904dd Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 16:10:06 -0700 Subject: [PATCH 16/52] wip --- simple_html/utils.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/simple_html/utils.py b/simple_html/utils.py index 7b57b5b..7df16ba 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -442,7 +442,8 @@ def render(*nodes: Node) -> str: _ARG_LOCATION = Union[str, int, tuple[int, str]] _TemplatePart = Union[ tuple[Literal["STATIC"], str], - tuple[Literal["ARG"], _ARG_LOCATION] # the str is the arg name + tuple[Literal["ARG"], _ARG_LOCATION], # the str is the arg name + tuple[Literal["DYNAMIC"], Union[list[Node], Generator[Node, None, None]]] ] class Templatizable(Protocol): @@ -459,6 +460,9 @@ def append_static(obj: str) -> None: def append_arg(arg: _ARG_LOCATION) -> None: return template_parts.append(("ARG", arg)) + def append_dynamic(obj: Union[list[Node], Generator[Node, None, None]]) -> None: + return template_parts.append(("DYNAMIC", obj)) + node_id = id(node) # note that this should stay up-to-speed with the `Node` definition @@ -491,6 +495,7 @@ def append_arg(arg: _ARG_LOCATION) -> None: # This is an argument placeholder - add a marker append_arg(sentinel_objects[node_id]) else: + for item in node: _traverse_node(item, template_parts, sentinel_objects) elif type(node) is GeneratorType: @@ -575,7 +580,7 @@ def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_Templat return template_parts -_CoalescedPart = Union[_ARG_LOCATION, SafeString] +_CoalescedPart = Union[_ARG_LOCATION, SafeString, list[Node], Generator[Node, None, None]] def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: template_part_lists: tuple[list[_TemplatePart], list[_TemplatePart], list[_TemplatePart]] = ( @@ -612,8 +617,10 @@ def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: def get_arg_val(args: tuple[Node, ...], kwargs: dict[str, Node], - location: _ARG_LOCATION) -> Node: - if isinstance(location, tuple): + location: Union[_ARG_LOCATION, list[Node], Generator[Node, None, None]]) -> Node: + if isinstance(location, (list, Generator)): + return location + elif isinstance(location, tuple): int_loc, str_loc = location if len(args) >= int_loc + 1: return args[int_loc] From 7d2db3c905c3ba088d67f75cb5a5abbfccc064f2 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 16:41:43 -0700 Subject: [PATCH 17/52] wip --- bench/simple.py | 641 +++++++++++++++++++------------------- simple_html/utils.py | 66 ++-- tests/test_simple_html.py | 18 +- 3 files changed, 366 insertions(+), 359 deletions(-) diff --git a/bench/simple.py b/bench/simple.py index 0209313..ab45d4b 100644 --- a/bench/simple.py +++ b/bench/simple.py @@ -96,42 +96,45 @@ def basic_long(objs: List[Tuple[str, str, List[str]]]) -> None: ) +@templatize +def _lorem_html(title_: str) -> Node: + return html( + {"lang": "en"}, + head( + meta({"charset": "UTF-8"}), + meta( + { + "name": "viewport", + "content": "width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0", + } + ), + meta({"http-equiv": "X-UA-Compatible", "content": "ie=edge"}), + title(title_), + ), + body( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + ), + ) + + def lorem_ipsum(titles: List[str]) -> None: for t in titles: - render( - html( - {"lang": "en"}, - head( - meta({"charset": "UTF-8"}), - meta( - { - "name": "viewport", - "content": "width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0", - } - ), - meta({"http-equiv": "X-UA-Compatible", "content": "ie=edge"}), - title(t), - ), - body( - "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - ), - ) - ) + render(_lorem_html(t)) @templatize def _get_head(title_: str) -> Node: return head( - meta({"charset": "UTF-8"}), - meta({"name": "viewport", "content": "width=device-width, initial-scale=1.0"}), - meta({"name": "description", - "content": "Tech Insights - Your source for the latest in web development, AI, and programming trends"}), - meta({"name": "keywords", - "content": "web development, programming, AI, JavaScript, Python, tech news"}), - title(title_), - link({"rel": "stylesheet", "href": "/static/css/main.css"}), - link({"rel": "icon", "type": "image/x-icon", "href": "/favicon.ico"}), - style(""" + meta({"charset": "UTF-8"}), + meta({"name": "viewport", "content": "width=device-width, initial-scale=1.0"}), + meta({"name": "description", + "content": "Tech Insights - Your source for the latest in web development, AI, and programming trends"}), + meta({"name": "keywords", + "content": "web development, programming, AI, JavaScript, Python, tech news"}), + title(title_), + link({"rel": "stylesheet", "href": "/static/css/main.css"}), + link({"rel": "icon", "type": "image/x-icon", "href": "/favicon.ico"}), + style(""" body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; } .container { max-width: 1200px; margin: 0 auto; padding: 0 20px; } .site-header { background: #2c3e50; color: white; padding: 1rem 0; } @@ -148,104 +151,105 @@ def _get_head(title_: str) -> Node: .stats-table th, .stats-table td { padding: 0.5rem; border: 1px solid #ddd; text-align: left; } .stats-table th { background: #f0f0f0; } """) - ) + ) + @templatize def _html(t: str) -> Node: return html({"lang": "en"}, - _get_head(title_=t), - body( - header({"class": "site-header"}, - div({"class": "container"}, - h1({"class": "site-title"}, "Tech Insights"), - nav( - ul({"class": "nav-menu"}, - li(a({"href": "/"}, "Home")), - li(a({"href": "/tutorials"}, "Tutorials")), - li(a({"href": "/articles"}, "Articles")), - li(a({"href": "/reviews"}, "Reviews")), - li(a({"href": "/resources"}, "Resources")), - li(a({"href": "/about"}, "About")), - li(a({"href": "/contact"}, "Contact")) - ) - ) - ) - ), - main({"class": "container main-content"}, - section({"class": "content-area"}, - article({"class": "post featured-post"}, - h2("Complete Guide to Modern Web Development in 2024"), - p({"class": "post-meta"}, - "Published on ", time({"datetime": "2024-03-15"}, "March 15, 2024"), - " by ", span({"class": "author"}, "Sarah Johnson"), - " | ", span({"class": "read-time"}, "12 min read") - ), - img({"src": "/images/web-dev-2024.jpg", - "alt": "Modern web development tools and frameworks", - "style": "width: 100%; height: 300px; object-fit: cover; border-radius: 8px;"}), - p( - "Web development has evolved significantly in recent years, transforming from simple static pages ", - "to complex, interactive applications that power our digital world. The landscape continues to change ", - "rapidly, driven by new technologies, frameworks, and methodologies that promise to make development ", - "faster, more efficient, and more accessible." - ), - h3("Key Technologies Shaping the Future"), - p("The modern web development ecosystem is built around several core technologies:"), - ul( - li("**Component-based frameworks** like React, Vue, and Angular that promote reusable UI components"), - li("**Progressive Web Apps (PWAs)** that bridge the gap between web and native applications"), - li("**Serverless architectures** using AWS Lambda, Vercel Functions, and Netlify Functions"), - li("**JAMstack** (JavaScript, APIs, Markup) for better performance and security"), - li("**GraphQL** for more efficient data fetching and API design"), - li("**TypeScript** for type-safe JavaScript development"), - li("**Edge computing** for reduced latency and improved user experience") - ), - h3("Framework Comparison"), - table({"class": "stats-table"}, - thead( - tr( - th("Framework"), - th("Learning Curve"), - th("Performance"), - th("Community"), - th("Use Case") - ) - ), - tbody( - tr( - td("React"), - td("Medium"), - td("High"), - td("Very Large"), - td("Complex UIs, SPAs") - ), - tr( - td("Vue.js"), - td("Easy"), - td("High"), - td("Large"), - td("Rapid prototyping, SME apps") - ), - tr( - td("Angular"), - td("Steep"), - td("High"), - td("Large"), - td("Enterprise applications") - ), - tr( - td("Svelte"), - td("Easy"), - td("Very High"), - td("Growing"), - td("Performance-critical apps") - ) - ) - ), - h3("Code Example: Modern Component"), - p("Here's an example of a modern React component using hooks and TypeScript:"), - pre({"class": "code-block"}, - code(""" + _get_head(title_=t), + body( + header({"class": "site-header"}, + div({"class": "container"}, + h1({"class": "site-title"}, "Tech Insights"), + nav( + ul({"class": "nav-menu"}, + li(a({"href": "/"}, "Home")), + li(a({"href": "/tutorials"}, "Tutorials")), + li(a({"href": "/articles"}, "Articles")), + li(a({"href": "/reviews"}, "Reviews")), + li(a({"href": "/resources"}, "Resources")), + li(a({"href": "/about"}, "About")), + li(a({"href": "/contact"}, "Contact")) + ) + ) + ) + ), + main({"class": "container main-content"}, + section({"class": "content-area"}, + article({"class": "post featured-post"}, + h2("Complete Guide to Modern Web Development in 2024"), + p({"class": "post-meta"}, + "Published on ", time({"datetime": "2024-03-15"}, "March 15, 2024"), + " by ", span({"class": "author"}, "Sarah Johnson"), + " | ", span({"class": "read-time"}, "12 min read") + ), + img({"src": "/images/web-dev-2024.jpg", + "alt": "Modern web development tools and frameworks", + "style": "width: 100%; height: 300px; object-fit: cover; border-radius: 8px;"}), + p( + "Web development has evolved significantly in recent years, transforming from simple static pages ", + "to complex, interactive applications that power our digital world. The landscape continues to change ", + "rapidly, driven by new technologies, frameworks, and methodologies that promise to make development ", + "faster, more efficient, and more accessible." + ), + h3("Key Technologies Shaping the Future"), + p("The modern web development ecosystem is built around several core technologies:"), + ul( + li("**Component-based frameworks** like React, Vue, and Angular that promote reusable UI components"), + li("**Progressive Web Apps (PWAs)** that bridge the gap between web and native applications"), + li("**Serverless architectures** using AWS Lambda, Vercel Functions, and Netlify Functions"), + li("**JAMstack** (JavaScript, APIs, Markup) for better performance and security"), + li("**GraphQL** for more efficient data fetching and API design"), + li("**TypeScript** for type-safe JavaScript development"), + li("**Edge computing** for reduced latency and improved user experience") + ), + h3("Framework Comparison"), + table({"class": "stats-table"}, + thead( + tr( + th("Framework"), + th("Learning Curve"), + th("Performance"), + th("Community"), + th("Use Case") + ) + ), + tbody( + tr( + td("React"), + td("Medium"), + td("High"), + td("Very Large"), + td("Complex UIs, SPAs") + ), + tr( + td("Vue.js"), + td("Easy"), + td("High"), + td("Large"), + td("Rapid prototyping, SME apps") + ), + tr( + td("Angular"), + td("Steep"), + td("High"), + td("Large"), + td("Enterprise applications") + ), + tr( + td("Svelte"), + td("Easy"), + td("Very High"), + td("Growing"), + td("Performance-critical apps") + ) + ) + ), + h3("Code Example: Modern Component"), + p("Here's an example of a modern React component using hooks and TypeScript:"), + pre({"class": "code-block"}, + code(""" interface User { id: number; name: string; @@ -273,201 +277,204 @@ def _html(t: str) -> Node: ); }; """) + ), + h3("Best Practices for 2024"), + p("As we move forward in 2024, several best practices have emerged:"), + ol( + li("**Performance First**: Optimize for Core Web Vitals and user experience metrics"), + li("**Accessibility by Default**: Implement WCAG guidelines from the start of development"), + li("**Security-First Mindset**: Use CSP headers, sanitize inputs, and follow OWASP guidelines"), + li("**Mobile-First Design**: Start with mobile layouts and progressively enhance for larger screens"), + li("**Sustainable Web Development**: Optimize for energy efficiency and reduced carbon footprint") + ), + blockquote( + p("\"The best web developers are those who understand that technology should serve users, not the other way around.\""), + footer("— John Doe, Senior Frontend Architect at TechCorp") + ) + ), + + article({"class": "post"}, + h2("The Rise of AI in Development: Tools and Techniques"), + p({"class": "post-meta"}, + "Published on ", time({"datetime": "2024-03-10"}, "March 10, 2024"), + " by ", span({"class": "author"}, "Michael Chen"), + " | ", span({"class": "read-time"}, "8 min read") + ), + p( + "Artificial Intelligence is fundamentally transforming how we write, test, and deploy code. ", + "From intelligent autocomplete suggestions to automated bug detection and code generation, ", + "AI tools are becoming essential companions for modern developers." + ), + h3("Popular AI Development Tools"), + ul( + li("**GitHub Copilot**: AI-powered code completion and generation"), + li("**ChatGPT & GPT-4**: Code explanation, debugging, and architecture advice"), + li("**Amazon CodeWhisperer**: Real-time code suggestions with security scanning"), + li("**DeepCode**: AI-powered code review and vulnerability detection"), + li("**Kite**: Intelligent code completion for Python and JavaScript") + ), + p( + "These tools don't replace developers but rather augment their capabilities, ", + "allowing them to focus on higher-level problem solving and creative solutions." + ) + ), + + article({"class": "post"}, + h2("Python vs JavaScript: Which Language to Learn in 2024?"), + p({"class": "post-meta"}, + "Published on ", time({"datetime": "2024-03-05"}, "March 5, 2024"), + " by ", span({"class": "author"}, "Emily Rodriguez"), + " | ", span({"class": "read-time"}, "10 min read") + ), + p( + "The eternal debate continues: should new developers learn Python or JavaScript first? ", + "Both languages have their strengths and use cases, and the answer largely depends on ", + "your career goals and the type of projects you want to work on." + ), + h3("Python Advantages"), + ul( + li("Simple, readable syntax that's beginner-friendly"), + li("Excellent for data science, machine learning, and AI"), + li("Strong in automation, scripting, and backend development"), + li("Huge ecosystem of libraries and frameworks (Django, Flask, NumPy, pandas)") + ), + h3("JavaScript Advantages"), + ul( + li("Essential for web development (frontend and backend with Node.js)"), + li("Immediate visual feedback when learning"), + li("Huge job market and demand"), + li("Versatile: runs in browsers, servers, mobile apps, and desktop applications") + ), + p("The truth is, both languages are valuable, and learning one makes learning the other easier.") + ), + + section({"class": "comment-section"}, + h3("Join the Discussion"), + form({"class": "comment-form", "action": "/submit-comment", "method": "POST"}, + div( + label({"for": "name"}, "Name:"), + br, + input_( + {"type": "text", "id": "name", "name": "name", "required": None}) ), - h3("Best Practices for 2024"), - p("As we move forward in 2024, several best practices have emerged:"), - ol( - li("**Performance First**: Optimize for Core Web Vitals and user experience metrics"), - li("**Accessibility by Default**: Implement WCAG guidelines from the start of development"), - li("**Security-First Mindset**: Use CSP headers, sanitize inputs, and follow OWASP guidelines"), - li("**Mobile-First Design**: Start with mobile layouts and progressively enhance for larger screens"), - li("**Sustainable Web Development**: Optimize for energy efficiency and reduced carbon footprint") - ), - blockquote( - p("\"The best web developers are those who understand that technology should serve users, not the other way around.\""), - footer("— John Doe, Senior Frontend Architect at TechCorp") - ) - ), - - article({"class": "post"}, - h2("The Rise of AI in Development: Tools and Techniques"), - p({"class": "post-meta"}, - "Published on ", time({"datetime": "2024-03-10"}, "March 10, 2024"), - " by ", span({"class": "author"}, "Michael Chen"), - " | ", span({"class": "read-time"}, "8 min read") - ), - p( - "Artificial Intelligence is fundamentally transforming how we write, test, and deploy code. ", - "From intelligent autocomplete suggestions to automated bug detection and code generation, ", - "AI tools are becoming essential companions for modern developers." - ), - h3("Popular AI Development Tools"), - ul( - li("**GitHub Copilot**: AI-powered code completion and generation"), - li("**ChatGPT & GPT-4**: Code explanation, debugging, and architecture advice"), - li("**Amazon CodeWhisperer**: Real-time code suggestions with security scanning"), - li("**DeepCode**: AI-powered code review and vulnerability detection"), - li("**Kite**: Intelligent code completion for Python and JavaScript") - ), - p( - "These tools don't replace developers but rather augment their capabilities, ", - "allowing them to focus on higher-level problem solving and creative solutions." - ) - ), - - article({"class": "post"}, - h2("Python vs JavaScript: Which Language to Learn in 2024?"), - p({"class": "post-meta"}, - "Published on ", time({"datetime": "2024-03-05"}, "March 5, 2024"), - " by ", span({"class": "author"}, "Emily Rodriguez"), - " | ", span({"class": "read-time"}, "10 min read") - ), - p( - "The eternal debate continues: should new developers learn Python or JavaScript first? ", - "Both languages have their strengths and use cases, and the answer largely depends on ", - "your career goals and the type of projects you want to work on." - ), - h3("Python Advantages"), - ul( - li("Simple, readable syntax that's beginner-friendly"), - li("Excellent for data science, machine learning, and AI"), - li("Strong in automation, scripting, and backend development"), - li("Huge ecosystem of libraries and frameworks (Django, Flask, NumPy, pandas)") - ), - h3("JavaScript Advantages"), - ul( - li("Essential for web development (frontend and backend with Node.js)"), - li("Immediate visual feedback when learning"), - li("Huge job market and demand"), - li("Versatile: runs in browsers, servers, mobile apps, and desktop applications") - ), - p("The truth is, both languages are valuable, and learning one makes learning the other easier.") - ), - - section({"class": "comment-section"}, - h3("Join the Discussion"), - form({"class": "comment-form", "action": "/submit-comment", "method": "POST"}, - div( - label({"for": "name"}, "Name:"), - br, - input_({"type": "text", "id": "name", "name": "name", "required": None}) - ), - div( - label({"for": "email"}, "Email:"), - br, - input_( - {"type": "email", "id": "email", "name": "email", "required": None}) - ), - div( - label({"for": "comment"}, "Your Comment:"), - br, - textarea({"id": "comment", "name": "comment", "rows": "5", "cols": "50", - "required": None}) - ), - br, - button({"type": "submit"}, "Post Comment") - ) - ) - ), - - aside({"class": "sidebar"}, - section( - h3("Popular Tags"), - div({"class": "tag-cloud"}, - a({"href": "/tags/python", "class": "tag"}, "Python"), - a({"href": "/tags/javascript", "class": "tag"}, "JavaScript"), - a({"href": "/tags/react", "class": "tag"}, "React"), - a({"href": "/tags/ai", "class": "tag"}, "AI & ML"), - a({"href": "/tags/webdev", "class": "tag"}, "Web Dev"), - a({"href": "/tags/nodejs", "class": "tag"}, "Node.js"), - a({"href": "/tags/typescript", "class": "tag"}, "TypeScript"), - a({"href": "/tags/vue", "class": "tag"}, "Vue.js") - ) - ), - - section( - h3("Latest Tutorials"), - ul( - li(a({"href": "/tutorial/rest-api-python"}, - "Building REST APIs with Python and FastAPI")), - li(a({"href": "/tutorial/react-hooks"}, "Advanced React Hooks Patterns")), - li(a({"href": "/tutorial/docker-basics"}, "Docker for Beginners: Complete Guide")), - li(a({"href": "/tutorial/graphql-intro"}, "Introduction to GraphQL")), - li(a({"href": "/tutorial/css-grid"}, "Mastering CSS Grid Layout")) - ) - ), - - section( - h3("Recommended Books"), - ul( - li("Clean Code by Robert C. Martin"), - li("You Don't Know JS by Kyle Simpson"), - li("Python Crash Course by Eric Matthes"), - li("Designing Data-Intensive Applications by Martin Kleppmann"), - li("The Pragmatic Programmer by Andy Hunt") - ) - ), - - section( - h3("Follow Us"), - div( - p("Stay updated with the latest tech trends:"), - ul( - li(a({"href": "https://twitter.com/techinsights"}, "Twitter")), - li(a({"href": "https://linkedin.com/company/techinsights"}, "LinkedIn")), - li(a({"href": "/newsletter"}, "Newsletter")), - li(a({"href": "/rss"}, "RSS Feed")) - ) - ) - ), - - section( - h3("Site Statistics"), - table({"class": "stats-table"}, - tbody( - tr(td("Total Articles"), td("247")), - tr(td("Active Users"), td("12,394")), - tr(td("Comments"), td("3,891")), - tr(td("Code Examples"), td("1,205")) - ) - ) - ) - ) - ), - - footer({"class": "site-footer"}, - div({"class": "container"}, - div({"class": "footer-content"}, - div({"class": "footer-section"}, - h4("About Tech Insights"), - p("Your go-to resource for web development tutorials, programming guides, and the latest technology trends. We help developers stay current with industry best practices.") - ), - div({"class": "footer-section"}, - h4("Quick Links"), - ul( - li(a({"href": "/privacy"}, "Privacy Policy")), - li(a({"href": "/terms"}, "Terms of Service")), - li(a({"href": "/sitemap"}, "Sitemap")), - li(a({"href": "/advertise"}, "Advertise")) - ) - ), - div({"class": "footer-section"}, - h4("Contact Info"), - p("Email: hello@techinsights.dev"), - p("Location: San Francisco, CA"), - p("Phone: (555) 123-4567") - ) - ), - hr, - div({"class": "footer-bottom"}, - p("© 2024 Tech Insights. All rights reserved. Built with simple_html library."), - p("Made with ❤️ for the developer community") - ) - ) - ) - ) - ) + div( + label({"for": "email"}, "Email:"), + br, + input_( + {"type": "email", "id": "email", "name": "email", + "required": None}) + ), + div( + label({"for": "comment"}, "Your Comment:"), + br, + textarea( + {"id": "comment", "name": "comment", "rows": "5", "cols": "50", + "required": None}) + ), + br, + button({"type": "submit"}, "Post Comment") + ) + ) + ), + + aside({"class": "sidebar"}, + section( + h3("Popular Tags"), + div({"class": "tag-cloud"}, + a({"href": "/tags/python", "class": "tag"}, "Python"), + a({"href": "/tags/javascript", "class": "tag"}, "JavaScript"), + a({"href": "/tags/react", "class": "tag"}, "React"), + a({"href": "/tags/ai", "class": "tag"}, "AI & ML"), + a({"href": "/tags/webdev", "class": "tag"}, "Web Dev"), + a({"href": "/tags/nodejs", "class": "tag"}, "Node.js"), + a({"href": "/tags/typescript", "class": "tag"}, "TypeScript"), + a({"href": "/tags/vue", "class": "tag"}, "Vue.js") + ) + ), + + section( + h3("Latest Tutorials"), + ul( + li(a({"href": "/tutorial/rest-api-python"}, + "Building REST APIs with Python and FastAPI")), + li(a({"href": "/tutorial/react-hooks"}, "Advanced React Hooks Patterns")), + li(a({"href": "/tutorial/docker-basics"}, + "Docker for Beginners: Complete Guide")), + li(a({"href": "/tutorial/graphql-intro"}, "Introduction to GraphQL")), + li(a({"href": "/tutorial/css-grid"}, "Mastering CSS Grid Layout")) + ) + ), + section( + h3("Recommended Books"), + ul( + li("Clean Code by Robert C. Martin"), + li("You Don't Know JS by Kyle Simpson"), + li("Python Crash Course by Eric Matthes"), + li("Designing Data-Intensive Applications by Martin Kleppmann"), + li("The Pragmatic Programmer by Andy Hunt") + ) + ), + + section( + h3("Follow Us"), + div( + p("Stay updated with the latest tech trends:"), + ul( + li(a({"href": "https://twitter.com/techinsights"}, "Twitter")), + li(a({"href": "https://linkedin.com/company/techinsights"}, "LinkedIn")), + li(a({"href": "/newsletter"}, "Newsletter")), + li(a({"href": "/rss"}, "RSS Feed")) + ) + ) + ), + + section( + h3("Site Statistics"), + table({"class": "stats-table"}, + tbody( + tr(td("Total Articles"), td("247")), + tr(td("Active Users"), td("12,394")), + tr(td("Comments"), td("3,891")), + tr(td("Code Examples"), td("1,205")) + ) + ) + ) + ) + ), + + footer({"class": "site-footer"}, + div({"class": "container"}, + div({"class": "footer-content"}, + div({"class": "footer-section"}, + h4("About Tech Insights"), + p("Your go-to resource for web development tutorials, programming guides, and the latest technology trends. We help developers stay current with industry best practices.") + ), + div({"class": "footer-section"}, + h4("Quick Links"), + ul( + li(a({"href": "/privacy"}, "Privacy Policy")), + li(a({"href": "/terms"}, "Terms of Service")), + li(a({"href": "/sitemap"}, "Sitemap")), + li(a({"href": "/advertise"}, "Advertise")) + ) + ), + div({"class": "footer-section"}, + h4("Contact Info"), + p("Email: hello@techinsights.dev"), + p("Location: San Francisco, CA"), + p("Phone: (555) 123-4567") + ) + ), + hr, + div({"class": "footer-bottom"}, + p("© 2024 Tech Insights. All rights reserved. Built with simple_html library."), + p("Made with ❤️ for the developer community") + ) + ) + ) + ) + ) def large_page(titles: list[str]) -> None: @@ -475,4 +482,4 @@ def large_page(titles: list[str]) -> None: render( DOCTYPE_HTML5, _html(t=t) - ) \ No newline at end of file + ) diff --git a/simple_html/utils.py b/simple_html/utils.py index 7df16ba..da8dbfd 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -1,7 +1,8 @@ import inspect from decimal import Decimal from types import GeneratorType -from typing import Any, Union, Generator, Iterable, Callable, Final, TYPE_CHECKING, Protocol, Literal, Never +from typing import Any, Union, Generator, Iterable, Callable, Final, TYPE_CHECKING, Protocol, Literal, Never, cast +from uuid import uuid4 class SafeString: @@ -439,15 +440,26 @@ def render(*nodes: Node) -> str: return "".join(results) + _ARG_LOCATION = Union[str, int, tuple[int, str]] _TemplatePart = Union[ tuple[Literal["STATIC"], str], tuple[Literal["ARG"], _ARG_LOCATION], # the str is the arg name - tuple[Literal["DYNAMIC"], Union[list[Node], Generator[Node, None, None]]] +] + +TemplateNode = Union[ + str, + SafeString, + float, + int, + Decimal, + "Tag", + "TagTuple", + # list and generator types excluded because they are often mutated ] class Templatizable(Protocol): - def __call__(self, **kwargs: Node) -> Node: + def __call__(self, *args: TemplateNode, **kwargs: TemplateNode) -> TemplateNode: ... def _traverse_node(node: Node, @@ -460,9 +472,6 @@ def append_static(obj: str) -> None: def append_arg(arg: _ARG_LOCATION) -> None: return template_parts.append(("ARG", arg)) - def append_dynamic(obj: Union[list[Node], Generator[Node, None, None]]) -> None: - return template_parts.append(("DYNAMIC", obj)) - node_id = id(node) # note that this should stay up-to-speed with the `Node` definition @@ -476,9 +485,7 @@ def append_dynamic(obj: Union[list[Node], Generator[Node, None, None]]) -> None: # Check if this string is one of our sentinels if node_id in sentinel_objects: # This is an argument placeholder - add a marker - append_arg( - sentinel_objects[node_id] - ) + append_arg(sentinel_objects[node_id]) else: # Regular string content append_static(faster_escape(node)) @@ -490,17 +497,6 @@ def append_dynamic(obj: Union[list[Node], Generator[Node, None, None]]) -> None: append_static(node.safe_str) elif type(node) is Tag: append_static(node.rendered) - elif type(node) is list: - if node_id in sentinel_objects: - # This is an argument placeholder - add a marker - append_arg(sentinel_objects[node_id]) - else: - - for item in node: - _traverse_node(item, template_parts, sentinel_objects) - elif type(node) is GeneratorType: - for item in node: - _traverse_node(item, template_parts, sentinel_objects) elif isinstance(node, (int, float, Decimal)): if node_id in sentinel_objects: append_arg(sentinel_objects[node_id]) @@ -528,17 +524,17 @@ def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_Templat # probe function with properly typed arguments # Use interned sentinel objects that we can identify by id sentinel_objects: dict[int, _ARG_LOCATION] = {} - probe_args: list[Node] = [] - probe_kwargs: dict[str, Node] = {} + probe_args: list[TemplateNode] = [] + probe_kwargs: dict[str, TemplateNode] = {} - sentinel: Node + sentinel: TemplateNode for i, (param_name, param) in enumerate(parameters.items()): if variant == 1: # Create a unique string sentinel and intern it so we can find it by identity sentinel = f"__SENTINEL_{param_name}_{id(object())}__" elif variant == 2: # Create a unique string sentinel and intern it so we can find it by identity - sentinel = [id(object())] + sentinel = uuid4().hex else: # Create a unique string sentinel and intern it so we can find it by identity sentinel = 1039917274618672531762351823761235 + id(object()) @@ -580,7 +576,7 @@ def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_Templat return template_parts -_CoalescedPart = Union[_ARG_LOCATION, SafeString, list[Node], Generator[Node, None, None]] +_CoalescedPart = Union[_ARG_LOCATION, SafeString] def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: template_part_lists: tuple[list[_TemplatePart], list[_TemplatePart], list[_TemplatePart]] = ( @@ -615,12 +611,10 @@ def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: return coalesced_parts -def get_arg_val(args: tuple[Node, ...], - kwargs: dict[str, Node], - location: Union[_ARG_LOCATION, list[Node], Generator[Node, None, None]]) -> Node: - if isinstance(location, (list, Generator)): - return location - elif isinstance(location, tuple): +def _get_arg_val(args: tuple[TemplateNode, ...], + kwargs: dict[str, TemplateNode], + location: _ARG_LOCATION) -> Node: + if isinstance(location, tuple): int_loc, str_loc = location if len(args) >= int_loc + 1: return args[int_loc] @@ -632,14 +626,14 @@ def get_arg_val(args: tuple[Node, ...], return kwargs[location] -def templatize(func: Templatizable) -> Templatizable: +def templatize(func: Templatizable) -> Callable[..., Node]: coalesced_parts = _coalesce_func(func) # return new function -- should just be a list of SafeStrings - def template_function(*args: Node, **kwargs: Node) -> Node: - return [ - part if isinstance(part, SafeString) else get_arg_val(args, kwargs, part) + def template_function(*args: TemplateNode, **kwargs: TemplateNode) -> Node: + return cast(Node, [ + part if isinstance(part, SafeString) else _get_arg_val(args, kwargs, part) for part in coalesced_parts - ] + ]) return template_function diff --git a/tests/test_simple_html.py b/tests/test_simple_html.py index 07d5091..44020a3 100644 --- a/tests/test_simple_html.py +++ b/tests/test_simple_html.py @@ -26,7 +26,7 @@ render_styles, img, title, h1, ) -from simple_html.utils import escape_attribute_key, templatize +from simple_html.utils import escape_attribute_key, templatize, _coalesce_func def test_renders_no_children() -> None: @@ -282,16 +282,12 @@ def greet(name: str, age: int) -> Node: h1("hi ", name, "I'm ", age), # tag br, - # list - ["wow"], - # generator - (name for _ in range(3)) ) ) ) - expected = """hi, John Doe

hi John DoeI'm 100


wowJohn DoeJohn DoeJohn Doe
""" + expected = """hi, John Doe

hi John DoeI'm 100


""" assert render(greet("John Doe", 100)) == expected templatized = templatize(greet) @@ -324,3 +320,13 @@ def greet(name: str) -> Node: with pytest.raises(AssertionError): templatize(greet) + +def test_templatize_coalescing() -> None: + def greet(name: str) -> Node: + return body(div("Your name is ", name)) + + assert _coalesce_func(greet) == [ + SafeString("
Your name is "), # yay + (0, "name"), # arg location + SafeString("
"), # yay + ] \ No newline at end of file From e746226ed28b0ccff7153c867d7ec333b83d44c9 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 19:56:02 -0700 Subject: [PATCH 18/52] wip --- bench/simple.py | 3 ++- simple_html/utils.py | 28 +++++++++++----------------- tests/test_simple_html.py | 4 +++- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/bench/simple.py b/bench/simple.py index ab45d4b..d197b66 100644 --- a/bench/simple.py +++ b/bench/simple.py @@ -1,4 +1,4 @@ -from typing import List, Tuple +from typing import List, Tuple, Generic, TypeVar from simple_html import ( h1, @@ -122,6 +122,7 @@ def lorem_ipsum(titles: List[str]) -> None: render(_lorem_html(t)) + @templatize def _get_head(title_: str) -> Node: return head( diff --git a/simple_html/utils.py b/simple_html/utils.py index da8dbfd..73c15e3 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -447,19 +447,9 @@ def render(*nodes: Node) -> str: tuple[Literal["ARG"], _ARG_LOCATION], # the str is the arg name ] -TemplateNode = Union[ - str, - SafeString, - float, - int, - Decimal, - "Tag", - "TagTuple", - # list and generator types excluded because they are often mutated -] class Templatizable(Protocol): - def __call__(self, *args: TemplateNode, **kwargs: TemplateNode) -> TemplateNode: + def __call__(self, *args: Node, **kwargs: Node) -> Node: ... def _traverse_node(node: Node, @@ -497,6 +487,9 @@ def append_arg(arg: _ARG_LOCATION) -> None: append_static(node.safe_str) elif type(node) is Tag: append_static(node.rendered) + elif isinstance(node, (list, GeneratorType)): + for n in node: + _traverse_node(n, template_parts, sentinel_objects) elif isinstance(node, (int, float, Decimal)): if node_id in sentinel_objects: append_arg(sentinel_objects[node_id]) @@ -504,6 +497,7 @@ def append_arg(arg: _ARG_LOCATION) -> None: # Other types - convert to string append_static(str(node)) else: + print(node) raise TypeError(f"Got unexpected type for node: {type(node)}") def _cannot_templatize_message(func: Callable[..., Any], @@ -524,10 +518,10 @@ def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_Templat # probe function with properly typed arguments # Use interned sentinel objects that we can identify by id sentinel_objects: dict[int, _ARG_LOCATION] = {} - probe_args: list[TemplateNode] = [] - probe_kwargs: dict[str, TemplateNode] = {} + probe_args: list[Node] = [] + probe_kwargs: dict[str, Node] = {} - sentinel: TemplateNode + sentinel: Node for i, (param_name, param) in enumerate(parameters.items()): if variant == 1: # Create a unique string sentinel and intern it so we can find it by identity @@ -611,8 +605,8 @@ def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: return coalesced_parts -def _get_arg_val(args: tuple[TemplateNode, ...], - kwargs: dict[str, TemplateNode], +def _get_arg_val(args: tuple[Node, ...], + kwargs: dict[str, Node], location: _ARG_LOCATION) -> Node: if isinstance(location, tuple): int_loc, str_loc = location @@ -630,7 +624,7 @@ def templatize(func: Templatizable) -> Callable[..., Node]: coalesced_parts = _coalesce_func(func) # return new function -- should just be a list of SafeStrings - def template_function(*args: TemplateNode, **kwargs: TemplateNode) -> Node: + def template_function(*args: Node, **kwargs: Node) -> Node: return cast(Node, [ part if isinstance(part, SafeString) else _get_arg_val(args, kwargs, part) for part in coalesced_parts diff --git a/tests/test_simple_html.py b/tests/test_simple_html.py index 44020a3..7d6a6d4 100644 --- a/tests/test_simple_html.py +++ b/tests/test_simple_html.py @@ -282,12 +282,14 @@ def greet(name: str, age: int) -> Node: h1("hi ", name, "I'm ", age), # tag br, + ["ok", name, "hmm"], + (name for _ in range(3)) ) ) ) - expected = """hi, John Doe

hi John DoeI'm 100


""" + expected = """hi, John Doe

hi John DoeI'm 100


okJohn DoehmmJohn DoeJohn DoeJohn Doe
""" assert render(greet("John Doe", 100)) == expected templatized = templatize(greet) From 839646065a85291e245085ff78a73769e6c2e7bd Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 19:58:33 -0700 Subject: [PATCH 19/52] string contains --- simple_html/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/simple_html/utils.py b/simple_html/utils.py index 73c15e3..1524acb 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -26,6 +26,9 @@ def faster_escape(s: str) -> str: - we don't check if some of the replacements are desired - we don't re-assign a variable many times. """ + if "'" not in s and '"' not in s and '<' not in s and ">" not in s and '&' not in s: + return s + return s.replace( "&", "&" # Must be done first! ).replace("<", "<").replace(">", ">").replace('"', """).replace('\'', "'") From 22c4326bf98caa5710aeee14717719b6df131029 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 20:03:18 -0700 Subject: [PATCH 20/52] wip --- simple_html/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/simple_html/utils.py b/simple_html/utils.py index 1524acb..40d2cce 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -6,6 +6,8 @@ class SafeString: + __slots__ = ("safe_str",) + def __init__(self, safe_str: str) -> None: self.safe_str = safe_str @@ -634,3 +636,7 @@ def template_function(*args: Node, **kwargs: Node) -> Node: ]) return template_function + + +def prerender(*nodes: Node) -> SafeString: + return SafeString(render(*nodes)) From d9be239311511c921619d26380c153e45d1ea6bb Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 20:03:27 -0700 Subject: [PATCH 21/52] wip --- simple_html/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/simple_html/utils.py b/simple_html/utils.py index 40d2cce..9eb13a3 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -628,7 +628,6 @@ def _get_arg_val(args: tuple[Node, ...], def templatize(func: Templatizable) -> Callable[..., Node]: coalesced_parts = _coalesce_func(func) - # return new function -- should just be a list of SafeStrings def template_function(*args: Node, **kwargs: Node) -> Node: return cast(Node, [ part if isinstance(part, SafeString) else _get_arg_val(args, kwargs, part) From 492b1f2d4d9a0d7b2830a2df29d0a81bbe373576 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 20:03:44 -0700 Subject: [PATCH 22/52] wip --- simple_html/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/simple_html/utils.py b/simple_html/utils.py index 9eb13a3..472c046 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -629,10 +629,10 @@ def templatize(func: Templatizable) -> Callable[..., Node]: coalesced_parts = _coalesce_func(func) def template_function(*args: Node, **kwargs: Node) -> Node: - return cast(Node, [ + return [ part if isinstance(part, SafeString) else _get_arg_val(args, kwargs, part) for part in coalesced_parts - ]) + ] return template_function From dc2fa1984b1e5594abca82566baf71b69fea4d1e Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 20:05:40 -0700 Subject: [PATCH 23/52] wip --- simple_html/utils.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/simple_html/utils.py b/simple_html/utils.py index 472c046..12efcbb 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -529,25 +529,20 @@ def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_Templat sentinel: Node for i, (param_name, param) in enumerate(parameters.items()): if variant == 1: - # Create a unique string sentinel and intern it so we can find it by identity sentinel = f"__SENTINEL_{param_name}_{id(object())}__" elif variant == 2: - # Create a unique string sentinel and intern it so we can find it by identity sentinel = uuid4().hex else: - # Create a unique string sentinel and intern it so we can find it by identity sentinel = 1039917274618672531762351823761235 + id(object()) sentinel_id = id(sentinel) - # Determine how to pass this parameter if param.kind == inspect.Parameter.POSITIONAL_ONLY: probe_args.append(sentinel) sentinel_objects[sentinel_id] = i elif param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: - # For mixed parameters, we could pass as positional or keyword - # Let's pass as positional if it's among the first parameters probe_args.append(sentinel) + # allow either an index or key lookup sentinel_objects[sentinel_id] = (i, param.name) elif param.kind == inspect.Parameter.KEYWORD_ONLY: probe_kwargs[param_name] = sentinel @@ -559,7 +554,6 @@ def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_Templat raise AssertionError(_cannot_templatize_message(func, _NO_ARGS_OR_KWARGS)) try: - # Call function with both args and kwargs template_node = func(*probe_args, **probe_kwargs) except Exception as e: raise Exception( From 421101fd0a43a60edabb80f9e25d91212a0ed6b5 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 29 Aug 2025 23:42:50 -0700 Subject: [PATCH 24/52] wip --- scripts/templatize.py | 40 --------------------------------------- simple_html/utils.py | 12 +++++++----- tests/test_simple_html.py | 18 ++++++++++++++++-- 3 files changed, 23 insertions(+), 47 deletions(-) delete mode 100644 scripts/templatize.py diff --git a/scripts/templatize.py b/scripts/templatize.py deleted file mode 100644 index 122194e..0000000 --- a/scripts/templatize.py +++ /dev/null @@ -1,40 +0,0 @@ -import time - -from simple_html import title, html, head, body, div, h1, br -from simple_html.utils import templatize, render, Node - - -def html_func(name: str, age: int) -> Node: - return html( - head(title("hi, ", name)), - body( - div({"class": "content", - "blabla": "bla"}, - h1("hi ", name, "I'm ", age), - br) - ) - ) - -templatized = templatize(html_func) - -# Example usage -if __name__ == "__main__": - # result = - # print(f"Type: {type(result)}") - # print(f"Parts: {len(result)}") - # print(f"Content: [{','.join(str(part) for part in result)}]") - start_1 = time.time() - for _ in range(10000): - render(html_func(name="Hello' World", age=300)) - end_1 = time.time() - start_1 - print(end_1) - - print(render(html_func(name="Hello' World", age=300))) - - start_2 = time.time() - for _ in range(10000): - render(templatized(name="Hello' World", age=300)) - end_2 = time.time() - start_2 - print(end_2) - - print(render(templatized(name="Hello' World", age=300))) \ No newline at end of file diff --git a/simple_html/utils.py b/simple_html/utils.py index 12efcbb..3944c02 100644 --- a/simple_html/utils.py +++ b/simple_html/utils.py @@ -184,14 +184,16 @@ def _render(nodes: Iterable[Node], append_to_list: Callable[[str], None]) -> Non mutate a list instead of constantly rendering strings """ for node in nodes: - if type(node) is tuple: - append_to_list(node[0]) - _render(node[1], append_to_list) - append_to_list(node[2]) - elif type(node) is SafeString: + # SafeString first because they are very common in performance-sensitive contexts, + # such as `templatize` and `prerender` + if type(node) is SafeString: append_to_list(node.safe_str) elif type(node) is str: append_to_list(faster_escape(node)) + elif type(node) is tuple: + append_to_list(node[0]) + _render(node[1], append_to_list) + append_to_list(node[2]) elif type(node) is Tag: append_to_list(node.rendered) elif type(node) is list or type(node) is GeneratorType: diff --git a/tests/test_simple_html.py b/tests/test_simple_html.py index 7d6a6d4..d32a3c7 100644 --- a/tests/test_simple_html.py +++ b/tests/test_simple_html.py @@ -24,9 +24,9 @@ DOCTYPE_HTML5, render, render_styles, - img, title, h1, + img, title, h1, h2, ) -from simple_html.utils import escape_attribute_key, templatize, _coalesce_func +from simple_html.utils import escape_attribute_key, templatize, _coalesce_func, Tag def test_renders_no_children() -> None: @@ -331,4 +331,18 @@ def greet(name: str) -> Node: SafeString("
Your name is "), # yay (0, "name"), # arg location SafeString("
"), # yay + ] + +def test_template_handles_node_arg() -> None: + @templatize + def hi(node: Node) -> Node: + return div(node) + + assert hi(div) == [ + SafeString(safe_str='
'), div, SafeString(safe_str='
') + ] + assert hi([["ok"], 5, h2("h2")]) == [ + SafeString("
"), + [["ok"], 5, h2("h2")], + SafeString('
'), ] \ No newline at end of file From 2ef2273f45d5267bb00cab4086e2b5bc428157f3 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 30 Aug 2025 13:39:49 -0700 Subject: [PATCH 25/52] wip --- .github/workflows/push.yml | 2 +- bench/simple.py | 2 +- setup.py | 2 +- simple_html/__init__.py | 2 +- simple_html/{utils.py => helpers.py} | 185 -------------------- simple_html/utils/__init__.py | 0 simple_html/utils/templatize.py | 245 +++++++++++++++++++++++++++ tests/test_simple_html.py | 2 +- 8 files changed, 250 insertions(+), 190 deletions(-) rename simple_html/{utils.py => helpers.py} (64%) create mode 100644 simple_html/utils/__init__.py create mode 100644 simple_html/utils/templatize.py diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index ef168e4..f6ffcf9 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -26,7 +26,7 @@ jobs: - name: run bench (pure python) run: poetry run python -m bench.run - name: mypyc - run: poetry run mypyc simple_html/utils.py + run: poetry run mypyc simple_html/helpers.py - name: run tests run: poetry run pytest - name: run bench (compiled) diff --git a/bench/simple.py b/bench/simple.py index d197b66..06140f2 100644 --- a/bench/simple.py +++ b/bench/simple.py @@ -22,7 +22,7 @@ nav, a, main, section, article, aside, footer, span, img, time, blockquote, code, pre, form, label, input_, textarea, button, table, thead, tbody, tr, th, td ) -from simple_html.utils import templatize, Node +from simple_html.helpers import templatize, Node def hello_world_empty(objs: List[None]) -> None: diff --git a/setup.py b/setup.py index 9d74519..461e861 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name="simple-html", ext_modules=mypycify([ - "simple_html/utils.py", + "simple_html/helpers.py", ]), author=project_data["authors"][0]["name"], packages=["simple_html"], diff --git a/simple_html/__init__.py b/simple_html/__init__.py index 4f323bd..8b1e950 100644 --- a/simple_html/__init__.py +++ b/simple_html/__init__.py @@ -1,4 +1,4 @@ -from simple_html.utils import SafeString as SafeString, Tag, render as render, render_styles as render_styles, Node as Node, TagTuple as TagTuple +from simple_html.helpers import SafeString as SafeString, Tag, render as render, render_styles as render_styles, Node as Node, TagTuple as TagTuple DOCTYPE_HTML5 = SafeString("") diff --git a/simple_html/utils.py b/simple_html/helpers.py similarity index 64% rename from simple_html/utils.py rename to simple_html/helpers.py index 3944c02..8d2c878 100644 --- a/simple_html/utils.py +++ b/simple_html/helpers.py @@ -448,190 +448,5 @@ def render(*nodes: Node) -> str: return "".join(results) -_ARG_LOCATION = Union[str, int, tuple[int, str]] -_TemplatePart = Union[ - tuple[Literal["STATIC"], str], - tuple[Literal["ARG"], _ARG_LOCATION], # the str is the arg name -] - - -class Templatizable(Protocol): - def __call__(self, *args: Node, **kwargs: Node) -> Node: - ... - -def _traverse_node(node: Node, - template_parts: list[_TemplatePart], - sentinel_objects: dict[int, _ARG_LOCATION]) -> None: - - def append_static(obj: str) -> None: - return template_parts.append(("STATIC", obj)) - - def append_arg(arg: _ARG_LOCATION) -> None: - return template_parts.append(("ARG", arg)) - - node_id = id(node) - - # note that this should stay up-to-speed with the `Node` definition - if type(node) is tuple: - # TagTuple - append_static(node[0]) - for n in node[1]: - _traverse_node(n, template_parts, sentinel_objects) - append_static(node[2]) - elif type(node) is str: - # Check if this string is one of our sentinels - if node_id in sentinel_objects: - # This is an argument placeholder - add a marker - append_arg(sentinel_objects[node_id]) - else: - # Regular string content - append_static(faster_escape(node)) - elif type(node) is SafeString: - # SafeString content - check if it's a sentinel - if node_id in sentinel_objects: - append_arg(sentinel_objects[node_id]) - else: - append_static(node.safe_str) - elif type(node) is Tag: - append_static(node.rendered) - elif isinstance(node, (list, GeneratorType)): - for n in node: - _traverse_node(n, template_parts, sentinel_objects) - elif isinstance(node, (int, float, Decimal)): - if node_id in sentinel_objects: - append_arg(sentinel_objects[node_id]) - else: - # Other types - convert to string - append_static(str(node)) - else: - print(node) - raise TypeError(f"Got unexpected type for node: {type(node)}") - -def _cannot_templatize_message(func: Callable[..., Any], - extra_message: str) -> str: - return f"Could not templatize function '{func.__name__}'. {extra_message}" - -_SHOULD_NOT_PERFORM_LOGIC = "Templatizable functions should not perform logic." -_NO_ARGS_OR_KWARGS = "Templatizable functions cannot accept *args or **kwargs." - -def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_TemplatePart]: - # TODO: try different types of arguments...? - sig = inspect.signature(func) - parameters = sig.parameters - - if not parameters: - raise ValueError("Function must have at least one parameter") - - # probe function with properly typed arguments - # Use interned sentinel objects that we can identify by id - sentinel_objects: dict[int, _ARG_LOCATION] = {} - probe_args: list[Node] = [] - probe_kwargs: dict[str, Node] = {} - - sentinel: Node - for i, (param_name, param) in enumerate(parameters.items()): - if variant == 1: - sentinel = f"__SENTINEL_{param_name}_{id(object())}__" - elif variant == 2: - sentinel = uuid4().hex - else: - sentinel = 1039917274618672531762351823761235 + id(object()) - - sentinel_id = id(sentinel) - - if param.kind == inspect.Parameter.POSITIONAL_ONLY: - probe_args.append(sentinel) - sentinel_objects[sentinel_id] = i - elif param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: - probe_args.append(sentinel) - # allow either an index or key lookup - sentinel_objects[sentinel_id] = (i, param.name) - elif param.kind == inspect.Parameter.KEYWORD_ONLY: - probe_kwargs[param_name] = sentinel - sentinel_objects[sentinel_id] = param.name - elif param.kind == inspect.Parameter.VAR_POSITIONAL: - raise AssertionError(_cannot_templatize_message(func, _NO_ARGS_OR_KWARGS)) - - elif param.kind == inspect.Parameter.VAR_KEYWORD: - raise AssertionError(_cannot_templatize_message(func, _NO_ARGS_OR_KWARGS)) - - try: - template_node = func(*probe_args, **probe_kwargs) - except Exception as e: - raise Exception( - e, - AssertionError(_cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC)) - ) - - # traverse `Node` tree structure to find usages of arguments by id - template_parts: list[_TemplatePart] = [] - - _traverse_node(template_node, template_parts, sentinel_objects) - - return template_parts - - -_CoalescedPart = Union[_ARG_LOCATION, SafeString] - -def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: - template_part_lists: tuple[list[_TemplatePart], list[_TemplatePart], list[_TemplatePart]] = ( - _probe_func(func, 1), - _probe_func(func, 2), - _probe_func(func, 3) - ) - assert len(template_part_lists[0]) == len(template_part_lists[1]) == len(template_part_lists[2]), _cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC) - - for part_1, part_2, part_3 in zip(*template_part_lists): - assert part_1[0] == part_2[0] == part_3[0], _cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC) - if part_1[0] == "STATIC": - assert (part_1[1] == part_2[1] == part_3[1]), _cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC) - - # convert non-argument nodes to strings and coalesce for speed - coalesced_parts: list[_CoalescedPart] = [] # string's are for parameter names - current_static: list[str] = [] - - for part_type, content in template_part_lists[0]: - if part_type == 'STATIC': - current_static.append(str(content)) - else: # ARG - # Flush accumulated static content - if current_static: - coalesced_parts.append(SafeString(''.join(current_static))) - current_static = [] - coalesced_parts.append(content) - - # Flush any remaining static content - if current_static: - coalesced_parts.append(SafeString(''.join(current_static))) - - return coalesced_parts - -def _get_arg_val(args: tuple[Node, ...], - kwargs: dict[str, Node], - location: _ARG_LOCATION) -> Node: - if isinstance(location, tuple): - int_loc, str_loc = location - if len(args) >= int_loc + 1: - return args[int_loc] - else: - return kwargs[str_loc] - elif isinstance(location, int): - return args[location] - else: - return kwargs[location] - - -def templatize(func: Templatizable) -> Callable[..., Node]: - coalesced_parts = _coalesce_func(func) - - def template_function(*args: Node, **kwargs: Node) -> Node: - return [ - part if isinstance(part, SafeString) else _get_arg_val(args, kwargs, part) - for part in coalesced_parts - ] - - return template_function - - def prerender(*nodes: Node) -> SafeString: return SafeString(render(*nodes)) diff --git a/simple_html/utils/__init__.py b/simple_html/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/simple_html/utils/templatize.py b/simple_html/utils/templatize.py new file mode 100644 index 0000000..2e143df --- /dev/null +++ b/simple_html/utils/templatize.py @@ -0,0 +1,245 @@ + + +_ARG_LOCATION = Union[str, int, tuple[int, str]] +_TemplatePart = Union[ + tuple[Literal["STATIC"], str], + tuple[Literal["ARG"], _ARG_LOCATION], # the str is the arg name +] + + +Templatizable = Callable[..., Node] + + +def _traverse_node(node: Node, + template_parts: list[_TemplatePart], + sentinel_objects: dict[int, _ARG_LOCATION]) -> None: + + def append_static(obj: str) -> None: + return template_parts.append(("STATIC", obj)) + + def append_arg(arg: _ARG_LOCATION) -> None: + return template_parts.append(("ARG", arg)) + + node_id = id(node) + + # note that this should stay up-to-speed with the `Node` definition + if type(node) is tuple: + # TagTuple + append_static(node[0]) + for n in node[1]: + _traverse_node(n, template_parts, sentinel_objects) + append_static(node[2]) + elif type(node) is str: + # Check if this string is one of our sentinels + if node_id in sentinel_objects: + # This is an argument placeholder - add a marker + append_arg(sentinel_objects[node_id]) + else: + # Regular string content + append_static(faster_escape(node)) + elif type(node) is SafeString: + # SafeString content - check if it's a sentinel + if node_id in sentinel_objects: + append_arg(sentinel_objects[node_id]) + else: + append_static(node.safe_str) + elif type(node) is Tag: + append_static(node.rendered) + elif isinstance(node, (list, GeneratorType)): + for n in node: + _traverse_node(n, template_parts, sentinel_objects) + elif isinstance(node, (int, float, Decimal)): + if node_id in sentinel_objects: + append_arg(sentinel_objects[node_id]) + else: + # Other types - convert to string + append_static(str(node)) + else: + print(node) + raise TypeError(f"Got unexpected type for node: {type(node)}") + +def _cannot_templatize_message(func: Callable[..., Any], + extra_message: str) -> str: + return f"Could not templatize function '{func.__name__}'. {extra_message}" + +_SHOULD_NOT_PERFORM_LOGIC = "Templatizable functions should not perform logic." +_NO_ARGS_OR_KWARGS = "Templatizable functions cannot accept *args or **kwargs." + +def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_TemplatePart]: + # TODO: try different types of arguments...? + sig = inspect.signature(func) + parameters = sig.parameters + + if not parameters: + raise ValueError("Function must have at least one parameter") + + # probe function with properly typed arguments + # Use interned sentinel objects that we can identify by id + sentinel_objects: dict[int, _ARG_LOCATION] = {} + probe_args: list[Node] = [] + probe_kwargs: dict[str, Node] = {} + + sentinel: Node + for i, (param_name, param) in enumerate(parameters.items()): + if variant == 1: + sentinel = f"__SENTINEL_{param_name}_{id(object())}__" + elif variant == 2: + sentinel = uuid4().hex + else: + sentinel = 1039917274618672531762351823761235 + id(object()) + + sentinel_id = id(sentinel) + + if param.kind == inspect.Parameter.POSITIONAL_ONLY: + probe_args.append(sentinel) + sentinel_objects[sentinel_id] = i + elif param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: + probe_args.append(sentinel) + # allow either an index or key lookup + sentinel_objects[sentinel_id] = (i, param.name) + elif param.kind == inspect.Parameter.KEYWORD_ONLY: + probe_kwargs[param_name] = sentinel + sentinel_objects[sentinel_id] = param.name + elif param.kind == inspect.Parameter.VAR_POSITIONAL: + raise AssertionError(_cannot_templatize_message(func, _NO_ARGS_OR_KWARGS)) + + elif param.kind == inspect.Parameter.VAR_KEYWORD: + raise AssertionError(_cannot_templatize_message(func, _NO_ARGS_OR_KWARGS)) + + try: + template_node = func(*probe_args, **probe_kwargs) + except Exception as e: + raise Exception( + e, + AssertionError(_cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC)) + ) + + # traverse `Node` tree structure to find usages of arguments by id + template_parts: list[_TemplatePart] = [] + + _traverse_node(template_node, template_parts, sentinel_objects) + + return template_parts + + +_CoalescedPart = Union[_ARG_LOCATION, SafeString] + +def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: + template_part_lists: tuple[list[_TemplatePart], list[_TemplatePart], list[_TemplatePart]] = ( + _probe_func(func, 1), + _probe_func(func, 2), + _probe_func(func, 3) + ) + assert len(template_part_lists[0]) == len(template_part_lists[1]) == len(template_part_lists[2]), _cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC) + + for part_1, part_2, part_3 in zip(*template_part_lists): + assert part_1[0] == part_2[0] == part_3[0], _cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC) + if part_1[0] == "STATIC": + assert (part_1[1] == part_2[1] == part_3[1]), _cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC) + + # convert non-argument nodes to strings and coalesce for speed + coalesced_parts: list[_CoalescedPart] = [] # string's are for parameter names + current_static: list[str] = [] + + for part_type, content in template_part_lists[0]: + if part_type == 'STATIC': + current_static.append(str(content)) + else: # ARG + # Flush accumulated static content + if current_static: + coalesced_parts.append(SafeString(''.join(current_static))) + current_static = [] + coalesced_parts.append(content) + + # Flush any remaining static content + if current_static: + coalesced_parts.append(SafeString(''.join(current_static))) + + return coalesced_parts + +def _get_arg_val(args: tuple[Node, ...], + kwargs: dict[str, Node], + location: _ARG_LOCATION) -> Node: + if isinstance(location, tuple): + int_loc, str_loc = location + if len(args) >= int_loc + 1: + return args[int_loc] + else: + return kwargs[str_loc] + elif isinstance(location, int): + return args[location] + else: + return kwargs[location] + + +def templatize(func: Templatizable) -> Callable[..., Node]: + coalesced_parts = _coalesce_func(func) + + def template_function(*args: Node, **kwargs: Node) -> Node: + return [ + part if isinstance(part, SafeString) else _get_arg_val(args, kwargs, part) + for part in coalesced_parts + ] + + return template_function + + + +def is_valid_node(node: Any) -> bool: + """Check if the given object is a valid Node.""" + origin = get_origin(node) + args = get_args(node) + + if isinstance(node, (str, SafeString, float, int, Decimal)): + return True + elif isinstance(node, list): + return all(is_valid_node(item) for item in node) + elif isinstance(node, GeneratorType): + return all(is_valid_node(item) for item in node) + elif isinstance(node, Tag): + return True + elif origin is tuple and len(args) == 3: + # Assuming the tuple structure is (str, list[Node], str) + tag_tuple_type = args + if not isinstance(tag_tuple_type[0], str) or not isinstance(tag_tuple_type[2], str): + return False + return all(is_valid_node(item) for item in tag_tuple_type[1]) + else: + return False + + +def validate_node_annotations(func: Callable) -> Callable: + """ + Decorator to validate that all arguments annotated with Node are instances of Node. + + :param func: The function to decorate. + :return: A wrapped function that performs validation. + """ + from inspect import signature + + sig = signature(func) + annotations = sig.annotations + + def wrapper(*args, **kwargs): + bound_args = sig.bind(*args, **kwargs) + for name, value in bound_args.arguments.items(): + if name in annotations and get_origin(annotations[name]) is Union: + args = get_args(annotations[name]) + if Node in args: + if not is_valid_node(value): + raise TypeError(f"Argument '{name}' must be a valid Node, got {type(value).__name__}") + + return func(*args, **kwargs) + + return wrapper + + +# Example usage +@validate_node_annotations +def process_node(node: Node) -> None: + print(f"Processing node: {node}") + + +# Example nodes +root = Tag(rendered="root") +process_node(root) diff --git a/tests/test_simple_html.py b/tests/test_simple_html.py index d32a3c7..592e7b0 100644 --- a/tests/test_simple_html.py +++ b/tests/test_simple_html.py @@ -26,7 +26,7 @@ render_styles, img, title, h1, h2, ) -from simple_html.utils import escape_attribute_key, templatize, _coalesce_func, Tag +from simple_html.helpers import escape_attribute_key, templatize, _coalesce_func, Tag def test_renders_no_children() -> None: From 86d4efb910a34318acf2d1f962ac6b02740a6c54 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 30 Aug 2025 14:30:23 -0700 Subject: [PATCH 26/52] type check --- simple_html/utils/templatize.py | 75 ++++++++++++++++++++------------- tests/test_simple_html.py | 3 +- 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/simple_html/utils/templatize.py b/simple_html/utils/templatize.py index 2e143df..29e4c6e 100644 --- a/simple_html/utils/templatize.py +++ b/simple_html/utils/templatize.py @@ -1,4 +1,11 @@ +import inspect +from decimal import Decimal +from types import GeneratorType +from typing import Union, Literal, Callable, Any, get_args, get_origin +from uuid import uuid4 +from simple_html import Node, SafeString +from simple_html.helpers import faster_escape, Tag _ARG_LOCATION = Union[str, int, tuple[int, str]] _TemplatePart = Union[ @@ -46,8 +53,11 @@ def append_arg(arg: _ARG_LOCATION) -> None: elif type(node) is Tag: append_static(node.rendered) elif isinstance(node, (list, GeneratorType)): - for n in node: - _traverse_node(n, template_parts, sentinel_objects) + if node_id in sentinel_objects: + append_arg(sentinel_objects[node_id]) + else: + for n in node: + _traverse_node(n, template_parts, sentinel_objects) elif isinstance(node, (int, float, Decimal)): if node_id in sentinel_objects: append_arg(sentinel_objects[node_id]) @@ -86,7 +96,7 @@ def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_Templat elif variant == 2: sentinel = uuid4().hex else: - sentinel = 1039917274618672531762351823761235 + id(object()) + sentinel = [id(object())] sentinel_id = id(sentinel) @@ -184,50 +194,55 @@ def template_function(*args: Node, **kwargs: Node) -> Node: return template_function - def is_valid_node(node: Any) -> bool: """Check if the given object is a valid Node.""" - origin = get_origin(node) - args = get_args(node) - - if isinstance(node, (str, SafeString, float, int, Decimal)): + # Check basic types first + if isinstance(node, (str, SafeString, float, int, Decimal, Tag)): return True elif isinstance(node, list): return all(is_valid_node(item) for item in node) elif isinstance(node, GeneratorType): return all(is_valid_node(item) for item in node) - elif isinstance(node, Tag): - return True - elif origin is tuple and len(args) == 3: - # Assuming the tuple structure is (str, list[Node], str) - tag_tuple_type = args - if not isinstance(tag_tuple_type[0], str) or not isinstance(tag_tuple_type[2], str): + elif isinstance(node, tuple) and len(node) == 3: + # TagTuple structure: (str, tuple[Node, ...], str) + if not isinstance(node[0], str) or not isinstance(node[2], str): return False - return all(is_valid_node(item) for item in tag_tuple_type[1]) + return all(is_valid_node(item) for item in node[1]) else: return False -def validate_node_annotations(func: Callable) -> Callable: +def validate_node_annotations(func: Templatizable) -> Templatizable: """ Decorator to validate that all arguments annotated with Node are instances of Node. :param func: The function to decorate. :return: A wrapped function that performs validation. """ - from inspect import signature - - sig = signature(func) - annotations = sig.annotations + sig = inspect.signature(func) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> Node: bound_args = sig.bind(*args, **kwargs) - for name, value in bound_args.arguments.items(): - if name in annotations and get_origin(annotations[name]) is Union: - args = get_args(annotations[name]) - if Node in args: + bound_args.apply_defaults() + + for param_name, param in sig.parameters.items(): + if param_name in bound_args.arguments: + value = bound_args.arguments[param_name] + annotation = param.annotation + + # Check if the parameter is annotated with Node or Union containing Node + should_validate = False + + if annotation == Node: + should_validate = True + elif get_origin(annotation) is Union: + union_args = get_args(annotation) + if Node in union_args: + should_validate = True + + if should_validate: if not is_valid_node(value): - raise TypeError(f"Argument '{name}' must be a valid Node, got {type(value).__name__}") + raise TypeError(f"Argument '{param_name}' must be a valid Node, got {type(value).__name__}") return func(*args, **kwargs) @@ -236,10 +251,10 @@ def wrapper(*args, **kwargs): # Example usage @validate_node_annotations -def process_node(node: Node) -> None: - print(f"Processing node: {node}") +def process_node(node: Node) -> Node: + return node -# Example nodes -root = Tag(rendered="root") +# Example nodes - Fix the Tag creation +root = Tag("root") # Tag constructor takes name, not rendered parameter process_node(root) diff --git a/tests/test_simple_html.py b/tests/test_simple_html.py index 592e7b0..69d7428 100644 --- a/tests/test_simple_html.py +++ b/tests/test_simple_html.py @@ -26,7 +26,8 @@ render_styles, img, title, h1, h2, ) -from simple_html.helpers import escape_attribute_key, templatize, _coalesce_func, Tag +from simple_html.helpers import escape_attribute_key, Tag +from simple_html.utils.templatize import templatize, _coalesce_func def test_renders_no_children() -> None: From 8459c23b824c94ac61afa03d01f231e26ebf7888 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 30 Aug 2025 15:07:22 -0700 Subject: [PATCH 27/52] wip --- bench/simple.py | 3 +- simple_html/utils/templatize.py | 304 +++++++++++++++++++++++++++----- 2 files changed, 261 insertions(+), 46 deletions(-) diff --git a/bench/simple.py b/bench/simple.py index 06140f2..3f34bee 100644 --- a/bench/simple.py +++ b/bench/simple.py @@ -22,7 +22,8 @@ nav, a, main, section, article, aside, footer, span, img, time, blockquote, code, pre, form, label, input_, textarea, button, table, thead, tbody, tr, th, td ) -from simple_html.helpers import templatize, Node +from simple_html.helpers import Node +from simple_html.utils.templatize import templatize def hello_world_empty(objs: List[None]) -> None: diff --git a/simple_html/utils/templatize.py b/simple_html/utils/templatize.py index 29e4c6e..b5c9a30 100644 --- a/simple_html/utils/templatize.py +++ b/simple_html/utils/templatize.py @@ -1,10 +1,10 @@ import inspect from decimal import Decimal from types import GeneratorType -from typing import Union, Literal, Callable, Any, get_args, get_origin +from typing import Union, Literal, Callable, Any, get_args, get_origin, Generator, ForwardRef from uuid import uuid4 -from simple_html import Node, SafeString +from simple_html import Node, SafeString, h1 from simple_html.helpers import faster_escape, Tag _ARG_LOCATION = Union[str, int, tuple[int, str]] @@ -194,67 +194,281 @@ def template_function(*args: Node, **kwargs: Node) -> Node: return template_function -def is_valid_node(node: Any) -> bool: - """Check if the given object is a valid Node.""" - # Check basic types first - if isinstance(node, (str, SafeString, float, int, Decimal, Tag)): +def _is_valid_node_annotation(annotation: Any) -> bool: + """Check if an annotation represents a valid Node type (recursive).""" + # Handle ForwardRef objects + if isinstance(annotation, ForwardRef): + # Get the string argument from ForwardRef + ref_name = annotation.__forward_arg__ + # Check if it refers to a valid Node type + return ref_name in ('Node', 'Tag', 'TagTuple') + + # Handle string literals (like 'Node' in list['Node']) + if isinstance(annotation, str): + return annotation in ('Node', 'Tag', 'TagTuple') + + # Direct Node type + if annotation == Node: return True - elif isinstance(node, list): - return all(is_valid_node(item) for item in node) - elif isinstance(node, GeneratorType): - return all(is_valid_node(item) for item in node) - elif isinstance(node, tuple) and len(node) == 3: - # TagTuple structure: (str, tuple[Node, ...], str) - if not isinstance(node[0], str) or not isinstance(node[2], str): - return False - return all(is_valid_node(item) for item in node[1]) - else: + + # Basic valid Node component types + if annotation in (str, int, float, Decimal, SafeString, Tag): + return True + + # Check for Union types (like Optional[Node] or Union[Node, str]) + if get_origin(annotation) is Union: + union_args = get_args(annotation) + # All union members must be valid Node types (except None for Optional) + return all(_is_valid_node_annotation(arg) for arg in union_args if arg is not type(None)) + + # Check for tuple types - specifically TagTuple: tuple[str, tuple[Node, ...], str] + if get_origin(annotation) is tuple: + type_args = get_args(annotation) + if len(type_args) == 3: + # TagTuple structure: (str, tuple[Node, ...], str) + first_arg, second_arg, third_arg = type_args + if (first_arg == str and third_arg == str and + get_origin(second_arg) is tuple and len(get_args(second_arg)) >= 1): + # Check if the tuple contains Node types (like tuple[Node, ...]) + inner_args = get_args(second_arg) + return all(_is_valid_node_annotation(arg) for arg in inner_args if arg is not ...) + # If it's not a valid TagTuple structure, it's invalid return False + # Check for generic types like list[Node], Generator[Node, None, None], etc. + origin = get_origin(annotation) + if origin is not None: + type_args = get_args(annotation) + if type_args: + # For list[Node], Generator[Node, None, None], etc. + if origin is list: + # All list element types must be valid Node types + return all(_is_valid_node_annotation(arg) for arg in type_args) + elif origin is Generator or (hasattr(origin, '__name__') and 'Generator' in str(origin)): + # For Generator[Node, None, None], only check the first type argument + return _is_valid_node_annotation(type_args[0]) if type_args else False + else: + # For other generic types, check if any type argument is a valid Node type + return any(_is_valid_node_annotation(arg) for arg in type_args) + + return False + def validate_node_annotations(func: Templatizable) -> Templatizable: """ - Decorator to validate that all arguments annotated with Node are instances of Node. + Decorator to validate that the function signature only uses valid Node annotations. + Validates at decoration time, not runtime. :param func: The function to decorate. - :return: A wrapped function that performs validation. + :return: The original function if validation passes. + :raises: TypeError if the function has invalid annotations. """ sig = inspect.signature(func) - def wrapper(*args: Any, **kwargs: Any) -> Node: - bound_args = sig.bind(*args, **kwargs) - bound_args.apply_defaults() + # Check if function has at least one parameter + if not sig.parameters: + raise TypeError(f"Function '{func.__name__}' must have at least one parameter") + + # Check each parameter's annotation + for param_name, param in sig.parameters.items(): + annotation = param.annotation + + # Skip parameters without annotations + if annotation == inspect.Parameter.empty: + continue + + # Check if annotation is valid for Node types + if not _is_valid_node_annotation(annotation): + raise TypeError( + f"Parameter '{param_name}' in function '{func.__name__}' has invalid annotation: {annotation}. " + f"Only Node-compatible types are allowed." + ) + + # If we get here, the function signature is valid + return func + + +# Test cases - just annotations with expected results +test_cases = [ + # Basic valid Node types + (Node, True, "Direct Node type"), + (str, True, "Basic string type"), + (int, True, "Basic int type"), + (float, True, "Basic float type"), + (Decimal, True, "Decimal type"), + (SafeString, True, "SafeString type"), + (Tag, True, "Tag type"), + + # Basic invalid types + (bool, False, "bool is not a valid Node type"), + (type(None), False, "None type"), + (dict, False, "dict type"), + (set, False, "set type"), + (bytes, False, "bytes type"), + (complex, False, "complex number type"), + (Exception, False, "Exception type"), + + # List types - valid + (list[Node], True, "List of Nodes"), + (list[str], True, "List of strings"), + (list[int], True, "List of ints"), + (list[float], True, "List of floats"), + (list[Decimal], True, "List of Decimals"), + (list[SafeString], True, "List of SafeStrings"), + (list[Tag], True, "List of Tags"), + + # List types - invalid + (list[bool], False, "List of bools"), + (list[dict[str, str]], False, "List of dicts"), + (list[set[str]], False, "List of sets"), + (list[bytes], False, "List of bytes"), + + # Union types - all valid + (Union[Node, str], True, "Union with Node and str"), + (Union[str, int], True, "Union of valid types"), + (Union[Node, int, float], True, "Union with multiple valid types"), + (Union[SafeString, Tag], True, "Union of SafeString and Tag"), + (Union[list[Node], str], True, "Union of list[Node] and str"), + + # Union types - with invalid members + (Union[bool, str], False, "Union with invalid bool"), + (Union[Node, dict], False, "Union with invalid dict"), + (Union[str, int, bool], False, "Union mixing valid and invalid"), + (Union[list[bool], str], False, "Union with invalid list[bool]"), + + # Optional types (Union with None) + (Union[Node, None], True, "Optional Node"), + (Union[str, None], True, "Optional str"), + (Union[list[Node], None], True, "Optional list[Node]"), + (Union[bool, None], False, "Optional bool (still invalid)"), + + # Tuple types - TagTuple structures (valid) + (tuple[str, tuple[Node, ...], str], True, "TagTuple structure"), + (tuple[str, tuple[str, ...], str], True, "TagTuple with strings"), + (tuple[str, tuple[int, ...], str], True, "TagTuple with ints"), + (tuple[str, tuple[SafeString, ...], str], True, "TagTuple with SafeStrings"), + (tuple[str, tuple[Union[Node, str], ...], str], True, "TagTuple with Union types"), + + # Tuple types - invalid structures + (tuple[int, str], False, "Invalid tuple structure (wrong length)"), + (tuple[str, str, str], False, "Invalid tuple structure (middle not tuple)"), + (tuple[int, tuple[Node, ...], str], False, "Invalid tuple structure (first not str)"), + (tuple[str, tuple[Node, ...], int], False, "Invalid tuple structure (last not str)"), + (tuple[str, tuple[bool, ...], str], False, "TagTuple with invalid bool"), + (tuple[str, list[Node], str], False, "TagTuple with list instead of tuple"), + + # Generator types + (Generator[Node, None, None], True, "Generator of Nodes"), + (Generator[str, None, None], True, "Generator of strings"), + (Generator[int, None, None], True, "Generator of ints"), + (Generator[SafeString, None, None], True, "Generator of SafeStrings"), + (Generator[bool, None, None], False, "Generator of bools"), + (Generator[dict[str, str], None, None], False, "Generator of dicts"), + (Generator[Union[Node, str], None, None], True, "Generator of Union types"), + (Generator[list[Node], None, None], True, "Generator of list[Node]"), + + # Complex nested structures + (list[tuple[str, tuple[Node, ...], str] | str | SafeString], True, "Complex nested Union"), + (list[Union[Node, str, int]], True, "List of Union with valid types"), + (list[Union[bool, str]], False, "List of Union with invalid bool"), + (Union[list[Node], tuple[str, tuple[Node, ...], str]], True, "Union of complex types"), + (list[list[Node]], True, "Nested list of Nodes"), + (list[list[str]], True, "Nested list of strings"), + (list[list[bool]], False, "Nested list of bools"), + + # Generator with complex types + (Generator[tuple[str, tuple[Node, ...], str], None, None], True, "Generator of TagTuples"), + (Generator[Union[Node, str], None, None], True, "Generator of Union types"), + (Generator[list[Node], None, None], True, "Generator of list[Node]"), + + # Edge cases with deeply nested types + (list[Union[tuple[str, tuple[Node, ...], str], Node, str]], True, "Deeply nested valid types"), + (Union[Generator[Node, None, None], list[str]], True, "Union of Generator and list"), + (list[Generator[Node, None, None]], True, "List of Generators"), + (tuple[str, tuple[Union[Node, SafeString, str], ...], str], True, "TagTuple with complex Union"), + + # More invalid edge cases + (tuple[str, tuple[Union[bool, str], ...], str], False, "TagTuple with invalid Union"), + (list[tuple[str, str, str]], False, "List of invalid tuples"), + (Union[Generator[bool, None, None], str], False, "Union with invalid Generator"), + (list[Union[dict[int, int], str]], False, "List with invalid Union member"), + + # Types that might be confused with valid ones + (type, False, "type itself"), + (object, False, "object type"), + (any, False, "any (lowercase)") if 'any' in globals() else (str, True, "any not defined"), + + # Additional container types that should be invalid + (tuple[Node], False, "Single-element tuple (not TagTuple)"), + (tuple[Node, str], False, "Two-element tuple (not TagTuple)"), + (tuple[str, tuple[Node, ...], str, int], False, "Four-element tuple"), +] + +# Run the tests +print("Testing _is_valid_node_annotation:") +passed = 0 +failed = 0 + +for annotation, should_pass, description in test_cases: + try: + result = _is_valid_node_annotation(annotation) + if result == should_pass: + status = "✓ PASS" + passed += 1 + else: + status = f"✗ FAIL (expected {should_pass}, got {result})" + failed += 1 + print(f"{status}: {description} - {annotation}") + except Exception as e: + print(f"✗ ERROR: {description} - {annotation}: {e}") + failed += 1 + +print(f"\nResults: {passed} passed, {failed} failed, {len(test_cases)} total") + +# Note: The actual test functions need to be defined properly for this to work +def process_node(node: Node, a: None) -> Node: + return node - for param_name, param in sig.parameters.items(): - if param_name in bound_args.arguments: - value = bound_args.arguments[param_name] - annotation = param.annotation - # Check if the parameter is annotated with Node or Union containing Node - should_validate = False +def process_node_1() -> Node: + return h1 - if annotation == Node: - should_validate = True - elif get_origin(annotation) is Union: - union_args = get_args(annotation) - if Node in union_args: - should_validate = True - if should_validate: - if not is_valid_node(value): - raise TypeError(f"Argument '{param_name}' must be a valid Node, got {type(value).__name__}") +def process_node_2(a: list[str], b: str, c: list[Node]) -> Node: + return h1 - return func(*args, **kwargs) - return wrapper +def process_node_3(a: bool) -> Node: + return h1 -# Example usage -@validate_node_annotations -def process_node(node: Node) -> Node: - return node +def process_node_4(x: tuple[str, tuple[Node, ...], str]) -> Node: + return h1 + +def process_node_5(x: list[tuple[str, tuple[Node, ...], str] | str | SafeString]) -> Node: + return h1 -# Example nodes - Fix the Tag creation -root = Tag("root") # Tag constructor takes name, not rendered parameter -process_node(root) + +# Test the validation +test_functions: list[tuple[Templatizable, bool, str]] = [ + (process_node, False, "None annotation should fail"), + (process_node_1, False, "no parameters should fail"), + (process_node_2, True, "mixed valid types should pass"), + (process_node_3, False, "bool should fail"), + (process_node_4, True, "TagTuple should pass"), + (process_node_5, True, "complex nested Union should pass"), +] + +for func, should_pass, description in test_functions: + try: + validate_node_annotations(func) + if should_pass: + print(f"✓ {func.__name__}: PASSED - {description}") + else: + print(f"✗ {func.__name__}: SHOULD HAVE FAILED - {description}") + except TypeError as e: + if not should_pass: + print(f"✓ {func.__name__}: CORRECTLY FAILED - {description}") + else: + print(f"✗ {func.__name__}: UNEXPECTEDLY FAILED - {description}: {e}") From 788cf9f28c4e3750d66fd3c2c111a1ae7022933e Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 30 Aug 2025 15:08:41 -0700 Subject: [PATCH 28/52] wi --- simple_html/utils/templatize.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/simple_html/utils/templatize.py b/simple_html/utils/templatize.py index b5c9a30..a902863 100644 --- a/simple_html/utils/templatize.py +++ b/simple_html/utils/templatize.py @@ -449,6 +449,9 @@ def process_node_4(x: tuple[str, tuple[Node, ...], str]) -> Node: def process_node_5(x: list[tuple[str, tuple[Node, ...], str] | str | SafeString]) -> Node: return h1 +def process_node_6(a, b) -> Node: + return h1 + # Test the validation test_functions: list[tuple[Templatizable, bool, str]] = [ @@ -458,6 +461,7 @@ def process_node_5(x: list[tuple[str, tuple[Node, ...], str] | str | SafeString] (process_node_3, False, "bool should fail"), (process_node_4, True, "TagTuple should pass"), (process_node_5, True, "complex nested Union should pass"), + (process_node_6, True, "unannotated args should pass"), ] for func, should_pass, description in test_functions: From b24d46f7706b29d7cc2a93467dbedeec9b8439b4 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 30 Aug 2025 20:19:22 -0700 Subject: [PATCH 29/52] wip --- simple_html/utils/templatize.py | 208 +--------------------------- tests/test_utils/__init__.py | 0 tests/test_utils/test_templatize.py | 200 ++++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 201 deletions(-) create mode 100644 tests/test_utils/__init__.py create mode 100644 tests/test_utils/test_templatize.py diff --git a/simple_html/utils/templatize.py b/simple_html/utils/templatize.py index a902863..3133d78 100644 --- a/simple_html/utils/templatize.py +++ b/simple_html/utils/templatize.py @@ -1,4 +1,5 @@ import inspect +import types from decimal import Decimal from types import GeneratorType from typing import Union, Literal, Callable, Any, get_args, get_origin, Generator, ForwardRef @@ -204,25 +205,25 @@ def _is_valid_node_annotation(annotation: Any) -> bool: return ref_name in ('Node', 'Tag', 'TagTuple') # Handle string literals (like 'Node' in list['Node']) - if isinstance(annotation, str): + elif isinstance(annotation, str): return annotation in ('Node', 'Tag', 'TagTuple') # Direct Node type - if annotation == Node: + elif annotation == Node: return True # Basic valid Node component types - if annotation in (str, int, float, Decimal, SafeString, Tag): + elif annotation in (str, int, float, Decimal, SafeString, Tag): return True # Check for Union types (like Optional[Node] or Union[Node, str]) - if get_origin(annotation) is Union: + elif (origin := get_origin(annotation)) is Union or (hasattr(types, 'UnionType') and isinstance(annotation, types.UnionType)): union_args = get_args(annotation) # All union members must be valid Node types (except None for Optional) return all(_is_valid_node_annotation(arg) for arg in union_args if arg is not type(None)) # Check for tuple types - specifically TagTuple: tuple[str, tuple[Node, ...], str] - if get_origin(annotation) is tuple: + elif get_origin(annotation) is tuple: type_args = get_args(annotation) if len(type_args) == 3: # TagTuple structure: (str, tuple[Node, ...], str) @@ -236,8 +237,7 @@ def _is_valid_node_annotation(annotation: Any) -> bool: return False # Check for generic types like list[Node], Generator[Node, None, None], etc. - origin = get_origin(annotation) - if origin is not None: + elif (origin := get_origin(annotation)) is not None: type_args = get_args(annotation) if type_args: # For list[Node], Generator[Node, None, None], etc. @@ -247,10 +247,6 @@ def _is_valid_node_annotation(annotation: Any) -> bool: elif origin is Generator or (hasattr(origin, '__name__') and 'Generator' in str(origin)): # For Generator[Node, None, None], only check the first type argument return _is_valid_node_annotation(type_args[0]) if type_args else False - else: - # For other generic types, check if any type argument is a valid Node type - return any(_is_valid_node_annotation(arg) for arg in type_args) - return False @@ -286,193 +282,3 @@ def validate_node_annotations(func: Templatizable) -> Templatizable: # If we get here, the function signature is valid return func - - -# Test cases - just annotations with expected results -test_cases = [ - # Basic valid Node types - (Node, True, "Direct Node type"), - (str, True, "Basic string type"), - (int, True, "Basic int type"), - (float, True, "Basic float type"), - (Decimal, True, "Decimal type"), - (SafeString, True, "SafeString type"), - (Tag, True, "Tag type"), - - # Basic invalid types - (bool, False, "bool is not a valid Node type"), - (type(None), False, "None type"), - (dict, False, "dict type"), - (set, False, "set type"), - (bytes, False, "bytes type"), - (complex, False, "complex number type"), - (Exception, False, "Exception type"), - - # List types - valid - (list[Node], True, "List of Nodes"), - (list[str], True, "List of strings"), - (list[int], True, "List of ints"), - (list[float], True, "List of floats"), - (list[Decimal], True, "List of Decimals"), - (list[SafeString], True, "List of SafeStrings"), - (list[Tag], True, "List of Tags"), - - # List types - invalid - (list[bool], False, "List of bools"), - (list[dict[str, str]], False, "List of dicts"), - (list[set[str]], False, "List of sets"), - (list[bytes], False, "List of bytes"), - - # Union types - all valid - (Union[Node, str], True, "Union with Node and str"), - (Union[str, int], True, "Union of valid types"), - (Union[Node, int, float], True, "Union with multiple valid types"), - (Union[SafeString, Tag], True, "Union of SafeString and Tag"), - (Union[list[Node], str], True, "Union of list[Node] and str"), - - # Union types - with invalid members - (Union[bool, str], False, "Union with invalid bool"), - (Union[Node, dict], False, "Union with invalid dict"), - (Union[str, int, bool], False, "Union mixing valid and invalid"), - (Union[list[bool], str], False, "Union with invalid list[bool]"), - - # Optional types (Union with None) - (Union[Node, None], True, "Optional Node"), - (Union[str, None], True, "Optional str"), - (Union[list[Node], None], True, "Optional list[Node]"), - (Union[bool, None], False, "Optional bool (still invalid)"), - - # Tuple types - TagTuple structures (valid) - (tuple[str, tuple[Node, ...], str], True, "TagTuple structure"), - (tuple[str, tuple[str, ...], str], True, "TagTuple with strings"), - (tuple[str, tuple[int, ...], str], True, "TagTuple with ints"), - (tuple[str, tuple[SafeString, ...], str], True, "TagTuple with SafeStrings"), - (tuple[str, tuple[Union[Node, str], ...], str], True, "TagTuple with Union types"), - - # Tuple types - invalid structures - (tuple[int, str], False, "Invalid tuple structure (wrong length)"), - (tuple[str, str, str], False, "Invalid tuple structure (middle not tuple)"), - (tuple[int, tuple[Node, ...], str], False, "Invalid tuple structure (first not str)"), - (tuple[str, tuple[Node, ...], int], False, "Invalid tuple structure (last not str)"), - (tuple[str, tuple[bool, ...], str], False, "TagTuple with invalid bool"), - (tuple[str, list[Node], str], False, "TagTuple with list instead of tuple"), - - # Generator types - (Generator[Node, None, None], True, "Generator of Nodes"), - (Generator[str, None, None], True, "Generator of strings"), - (Generator[int, None, None], True, "Generator of ints"), - (Generator[SafeString, None, None], True, "Generator of SafeStrings"), - (Generator[bool, None, None], False, "Generator of bools"), - (Generator[dict[str, str], None, None], False, "Generator of dicts"), - (Generator[Union[Node, str], None, None], True, "Generator of Union types"), - (Generator[list[Node], None, None], True, "Generator of list[Node]"), - - # Complex nested structures - (list[tuple[str, tuple[Node, ...], str] | str | SafeString], True, "Complex nested Union"), - (list[Union[Node, str, int]], True, "List of Union with valid types"), - (list[Union[bool, str]], False, "List of Union with invalid bool"), - (Union[list[Node], tuple[str, tuple[Node, ...], str]], True, "Union of complex types"), - (list[list[Node]], True, "Nested list of Nodes"), - (list[list[str]], True, "Nested list of strings"), - (list[list[bool]], False, "Nested list of bools"), - - # Generator with complex types - (Generator[tuple[str, tuple[Node, ...], str], None, None], True, "Generator of TagTuples"), - (Generator[Union[Node, str], None, None], True, "Generator of Union types"), - (Generator[list[Node], None, None], True, "Generator of list[Node]"), - - # Edge cases with deeply nested types - (list[Union[tuple[str, tuple[Node, ...], str], Node, str]], True, "Deeply nested valid types"), - (Union[Generator[Node, None, None], list[str]], True, "Union of Generator and list"), - (list[Generator[Node, None, None]], True, "List of Generators"), - (tuple[str, tuple[Union[Node, SafeString, str], ...], str], True, "TagTuple with complex Union"), - - # More invalid edge cases - (tuple[str, tuple[Union[bool, str], ...], str], False, "TagTuple with invalid Union"), - (list[tuple[str, str, str]], False, "List of invalid tuples"), - (Union[Generator[bool, None, None], str], False, "Union with invalid Generator"), - (list[Union[dict[int, int], str]], False, "List with invalid Union member"), - - # Types that might be confused with valid ones - (type, False, "type itself"), - (object, False, "object type"), - (any, False, "any (lowercase)") if 'any' in globals() else (str, True, "any not defined"), - - # Additional container types that should be invalid - (tuple[Node], False, "Single-element tuple (not TagTuple)"), - (tuple[Node, str], False, "Two-element tuple (not TagTuple)"), - (tuple[str, tuple[Node, ...], str, int], False, "Four-element tuple"), -] - -# Run the tests -print("Testing _is_valid_node_annotation:") -passed = 0 -failed = 0 - -for annotation, should_pass, description in test_cases: - try: - result = _is_valid_node_annotation(annotation) - if result == should_pass: - status = "✓ PASS" - passed += 1 - else: - status = f"✗ FAIL (expected {should_pass}, got {result})" - failed += 1 - print(f"{status}: {description} - {annotation}") - except Exception as e: - print(f"✗ ERROR: {description} - {annotation}: {e}") - failed += 1 - -print(f"\nResults: {passed} passed, {failed} failed, {len(test_cases)} total") - -# Note: The actual test functions need to be defined properly for this to work -def process_node(node: Node, a: None) -> Node: - return node - - -def process_node_1() -> Node: - return h1 - - -def process_node_2(a: list[str], b: str, c: list[Node]) -> Node: - return h1 - - -def process_node_3(a: bool) -> Node: - return h1 - - -def process_node_4(x: tuple[str, tuple[Node, ...], str]) -> Node: - return h1 - - -def process_node_5(x: list[tuple[str, tuple[Node, ...], str] | str | SafeString]) -> Node: - return h1 - -def process_node_6(a, b) -> Node: - return h1 - - -# Test the validation -test_functions: list[tuple[Templatizable, bool, str]] = [ - (process_node, False, "None annotation should fail"), - (process_node_1, False, "no parameters should fail"), - (process_node_2, True, "mixed valid types should pass"), - (process_node_3, False, "bool should fail"), - (process_node_4, True, "TagTuple should pass"), - (process_node_5, True, "complex nested Union should pass"), - (process_node_6, True, "unannotated args should pass"), -] - -for func, should_pass, description in test_functions: - try: - validate_node_annotations(func) - if should_pass: - print(f"✓ {func.__name__}: PASSED - {description}") - else: - print(f"✗ {func.__name__}: SHOULD HAVE FAILED - {description}") - except TypeError as e: - if not should_pass: - print(f"✓ {func.__name__}: CORRECTLY FAILED - {description}") - else: - print(f"✗ {func.__name__}: UNEXPECTEDLY FAILED - {description}: {e}") diff --git a/tests/test_utils/__init__.py b/tests/test_utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_utils/test_templatize.py b/tests/test_utils/test_templatize.py new file mode 100644 index 0000000..6ec70e9 --- /dev/null +++ b/tests/test_utils/test_templatize.py @@ -0,0 +1,200 @@ +from decimal import Decimal +from typing import Union, Generator + +import pytest + +from simple_html import Node, SafeString, Tag +from simple_html.utils.templatize import _is_valid_node_annotation + +test_annotations = [ + # Basic valid Node types + (Node, True, "Direct Node type"), + (str, True, "Basic string type"), + (int, True, "Basic int type"), + (float, True, "Basic float type"), + (Decimal, True, "Decimal type"), + (SafeString, True, "SafeString type"), + (Tag, True, "Tag type"), + + # Basic invalid types + (bool, False, "bool is not a valid Node type"), + (type(None), False, "None type"), + (dict, False, "dict type"), + (set, False, "set type"), + (bytes, False, "bytes type"), + (complex, False, "complex number type"), + (Exception, False, "Exception type"), + + # List types - valid + (list[Node], True, "List of Nodes"), + (list[str], True, "List of strings"), + (list[int], True, "List of ints"), + (list[float], True, "List of floats"), + (list[Decimal], True, "List of Decimals"), + (list[SafeString], True, "List of SafeStrings"), + (list[Tag], True, "List of Tags"), + + # List types - invalid + (list[bool], False, "List of bools"), + (list[dict[str, str]], False, "List of dicts"), + (list[set[str]], False, "List of sets"), + (list[bytes], False, "List of bytes"), + + # Union types - all valid + (Union[Node, str], True, "Union with Node and str"), + (Union[str, int], True, "Union of valid types"), + (Union[Node, int, float], True, "Union with multiple valid types"), + (Union[SafeString, Tag], True, "Union of SafeString and Tag"), + (Union[list[Node], str], True, "Union of list[Node] and str"), + + # Union types - with invalid members + (Union[bool, str], False, "Union with invalid bool"), + (Union[Node, dict], False, "Union with invalid dict"), + (Union[str, int, bool], False, "Union mixing valid and invalid"), + (Union[list[bool], str], False, "Union with invalid list[bool]"), + + # Optional types (Union with None) + (Union[Node, None], True, "Optional Node"), + (Union[str, None], True, "Optional str"), + (Union[list[Node], None], True, "Optional list[Node]"), + (Union[bool, None], False, "Optional bool (still invalid)"), + + # Tuple types - TagTuple structures (valid) + (tuple[str, tuple[Node, ...], str], True, "TagTuple structure"), + (tuple[str, tuple[str, ...], str], True, "TagTuple with strings"), + (tuple[str, tuple[int, ...], str], True, "TagTuple with ints"), + (tuple[str, tuple[SafeString, ...], str], True, "TagTuple with SafeStrings"), + (tuple[str, tuple[Union[Node, str], ...], str], True, "TagTuple with Union types"), + + # Tuple types - invalid structures + (tuple[int, str], False, "Invalid tuple structure (wrong length)"), + (tuple[str, str, str], False, "Invalid tuple structure (middle not tuple)"), + (tuple[int, tuple[Node, ...], str], False, "Invalid tuple structure (first not str)"), + (tuple[str, tuple[Node, ...], int], False, "Invalid tuple structure (last not str)"), + (tuple[str, tuple[bool, ...], str], False, "TagTuple with invalid bool"), + (tuple[str, list[Node], str], False, "TagTuple with list instead of tuple"), + + # Generator types + (Generator[Node, None, None], True, "Generator of Nodes"), + (Generator[str, None, None], True, "Generator of strings"), + (Generator[int, None, None], True, "Generator of ints"), + (Generator[SafeString, None, None], True, "Generator of SafeStrings"), + (Generator[bool, None, None], False, "Generator of bools"), + (Generator[dict[str, str], None, None], False, "Generator of dicts"), + (Generator[Union[Node, str], None, None], True, "Generator of Union types"), + (Generator[list[Node], None, None], True, "Generator of list[Node]"), + + # Complex nested structures + (list[tuple[str, tuple[Node, ...], str] | str | SafeString], True, "Complex nested Union"), + (list[Union[Node, str, int]], True, "List of Union with valid types"), + (list[Union[bool, str]], False, "List of Union with invalid bool"), + (Union[list[Node], tuple[str, tuple[Node, ...], str]], True, "Union of complex types"), + (list[list[Node]], True, "Nested list of Nodes"), + (list[list[str]], True, "Nested list of strings"), + (list[list[bool]], False, "Nested list of bools"), + + # Generator with complex types + (Generator[tuple[str, tuple[Node, ...], str], None, None], True, "Generator of TagTuples"), + (Generator[Union[Node, str], None, None], True, "Generator of Union types"), + (Generator[list[Node], None, None], True, "Generator of list[Node]"), + + # Edge cases with deeply nested types + (list[Union[tuple[str, tuple[Node, ...], str], Node, str]], True, "Deeply nested valid types"), + (Union[Generator[Node, None, None], list[str]], True, "Union of Generator and list"), + (list[Generator[Node, None, None]], True, "List of Generators"), + (tuple[str, tuple[Union[Node, SafeString, str], ...], str], True, "TagTuple with complex Union"), + + # More invalid edge cases + (tuple[str, tuple[Union[bool, str], ...], str], False, "TagTuple with invalid Union"), + (list[tuple[str, str, str]], False, "List of invalid tuples"), + (Union[Generator[bool, None, None], str], False, "Union with invalid Generator"), + (list[Union[dict[int, int], str]], False, "List with invalid Union member"), + + # Types that might be confused with valid ones + (type, False, "type itself"), + (object, False, "object type"), + (any, False, "any (lowercase)") if 'any' in globals() else (str, True, "any not defined"), + + # Additional container types that should be invalid + (tuple[Node], False, "Single-element tuple (not TagTuple)"), + (tuple[Node, str], False, "Two-element tuple (not TagTuple)"), + (tuple[str, tuple[Node, ...], str, int], False, "Four-element tuple"), +] + +# Run the tests +print("Testing _is_valid_node_annotation:") +passed = 0 +failed = 0 + +@pytest.mark.parametrize('annotation,expected_result,description', test_annotations) +def test_annotation(annotation, expected_result, description) -> None: + assert _is_valid_node_annotation(annotation) is expected_result, description +# +# +# for annotation, should_pass, description in test_cases: +# try: +# result = _is_valid_node_annotation(annotation) +# if result == should_pass: +# status = "✓ PASS" +# passed += 1 +# else: +# status = f"✗ FAIL (expected {should_pass}, got {result})" +# failed += 1 +# print(f"{status}: {description} - {annotation}") +# except Exception as e: +# print(f"✗ ERROR: {description} - {annotation}: {e}") +# failed += 1 +# +# print(f"\nResults: {passed} passed, {failed} failed, {len(test_cases)} total") +# +# # Note: The actual test functions need to be defined properly for this to work +# def process_node(node: Node, a: None) -> Node: +# return node +# +# +# def process_node_1() -> Node: +# return h1 +# +# +# def process_node_2(a: list[str], b: str, c: list[Node]) -> Node: +# return h1 +# +# +# def process_node_3(a: bool) -> Node: +# return h1 +# +# +# def process_node_4(x: tuple[str, tuple[Node, ...], str]) -> Node: +# return h1 +# +# +# def process_node_5(x: list[tuple[str, tuple[Node, ...], str] | str | SafeString]) -> Node: +# return h1 +# +# def process_node_6(a, b) -> Node: +# return h1 +# +# +# # Test the validation +# test_functions: list[tuple[Templatizable, bool, str]] = [ +# (process_node, False, "None annotation should fail"), +# (process_node_1, False, "no parameters should fail"), +# (process_node_2, True, "mixed valid types should pass"), +# (process_node_3, False, "bool should fail"), +# (process_node_4, True, "TagTuple should pass"), +# (process_node_5, True, "complex nested Union should pass"), +# (process_node_6, True, "unannotated args should pass"), +# ] +# +# for func, should_pass, description in test_functions: +# try: +# validate_node_annotations(func) +# if should_pass: +# print(f"✓ {func.__name__}: PASSED - {description}") +# else: +# print(f"✗ {func.__name__}: SHOULD HAVE FAILED - {description}") +# except TypeError as e: +# if not should_pass: +# print(f"✓ {func.__name__}: CORRECTLY FAILED - {description}") +# else: +# print(f"✗ {func.__name__}: UNEXPECTEDLY FAILED - {description}: {e}") From 34f7bb966afe30cf16ae7f8075215392b19b0d6a Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 30 Aug 2025 21:24:00 -0700 Subject: [PATCH 30/52] wip --- bench/simple.py | 2 +- simple_html/utils/templatize.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bench/simple.py b/bench/simple.py index 3f34bee..743be56 100644 --- a/bench/simple.py +++ b/bench/simple.py @@ -157,7 +157,7 @@ def _get_head(title_: str) -> Node: @templatize -def _html(t: str) -> Node: +def _html(t: str | bool) -> Node: return html({"lang": "en"}, _get_head(title_=t), body( diff --git a/simple_html/utils/templatize.py b/simple_html/utils/templatize.py index 3133d78..cd3c0cc 100644 --- a/simple_html/utils/templatize.py +++ b/simple_html/utils/templatize.py @@ -1,5 +1,6 @@ import inspect import types +import warnings from decimal import Decimal from types import GeneratorType from typing import Union, Literal, Callable, Any, get_args, get_origin, Generator, ForwardRef @@ -77,6 +78,8 @@ def _cannot_templatize_message(func: Callable[..., Any], _NO_ARGS_OR_KWARGS = "Templatizable functions cannot accept *args or **kwargs." def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_TemplatePart]: + warn_if_invalid_annotations(func) + # TODO: try different types of arguments...? sig = inspect.signature(func) parameters = sig.parameters @@ -250,7 +253,7 @@ def _is_valid_node_annotation(annotation: Any) -> bool: return False -def validate_node_annotations(func: Templatizable) -> Templatizable: +def warn_if_invalid_annotations(func: Templatizable) -> None: """ Decorator to validate that the function signature only uses valid Node annotations. Validates at decoration time, not runtime. @@ -275,10 +278,7 @@ def validate_node_annotations(func: Templatizable) -> Templatizable: # Check if annotation is valid for Node types if not _is_valid_node_annotation(annotation): - raise TypeError( + warnings.warn( f"Parameter '{param_name}' in function '{func.__name__}' has invalid annotation: {annotation}. " - f"Only Node-compatible types are allowed." + f"Only Node-compatible types are allowed in Templatize." ) - - # If we get here, the function signature is valid - return func From a434f942cc243a4459411827f63782172406432c Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 30 Aug 2025 23:02:02 -0700 Subject: [PATCH 31/52] wip --- .github/workflows/push.yml | 2 +- bench/simple.py | 8 ++++---- setup.py | 2 +- simple_html/__init__.py | 2 +- simple_html/{helpers.py => core.py} | 0 simple_html/{utils => }/templatize.py | 2 +- simple_html/utils/__init__.py | 0 tests/{test_simple_html.py => test_core.py} | 4 ++-- tests/{test_utils => }/test_templatize.py | 2 +- tests/test_utils/__init__.py | 0 10 files changed, 11 insertions(+), 11 deletions(-) rename simple_html/{helpers.py => core.py} (100%) rename simple_html/{utils => }/templatize.py (99%) delete mode 100644 simple_html/utils/__init__.py rename tests/{test_simple_html.py => test_core.py} (98%) rename tests/{test_utils => }/test_templatize.py (99%) delete mode 100644 tests/test_utils/__init__.py diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index f6ffcf9..65bfbec 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -26,7 +26,7 @@ jobs: - name: run bench (pure python) run: poetry run python -m bench.run - name: mypyc - run: poetry run mypyc simple_html/helpers.py + run: poetry run mypyc simple_html/core.py - name: run tests run: poetry run pytest - name: run bench (compiled) diff --git a/bench/simple.py b/bench/simple.py index 743be56..8dbf548 100644 --- a/bench/simple.py +++ b/bench/simple.py @@ -1,4 +1,4 @@ -from typing import List, Tuple, Generic, TypeVar +from typing import List, Tuple from simple_html import ( h1, @@ -22,8 +22,8 @@ nav, a, main, section, article, aside, footer, span, img, time, blockquote, code, pre, form, label, input_, textarea, button, table, thead, tbody, tr, th, td ) -from simple_html.helpers import Node -from simple_html.utils.templatize import templatize +from simple_html.core import Node +from simple_html.templatize import templatize def hello_world_empty(objs: List[None]) -> None: @@ -157,7 +157,7 @@ def _get_head(title_: str) -> Node: @templatize -def _html(t: str | bool) -> Node: +def _html(t: str) -> Node: return html({"lang": "en"}, _get_head(title_=t), body( diff --git a/setup.py b/setup.py index 461e861..2536fa0 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name="simple-html", ext_modules=mypycify([ - "simple_html/helpers.py", + "simple_html/core.py", ]), author=project_data["authors"][0]["name"], packages=["simple_html"], diff --git a/simple_html/__init__.py b/simple_html/__init__.py index 8b1e950..56a25f3 100644 --- a/simple_html/__init__.py +++ b/simple_html/__init__.py @@ -1,4 +1,4 @@ -from simple_html.helpers import SafeString as SafeString, Tag, render as render, render_styles as render_styles, Node as Node, TagTuple as TagTuple +from simple_html.core import SafeString as SafeString, Tag, render as render, render_styles as render_styles, Node as Node, TagTuple as TagTuple DOCTYPE_HTML5 = SafeString("") diff --git a/simple_html/helpers.py b/simple_html/core.py similarity index 100% rename from simple_html/helpers.py rename to simple_html/core.py diff --git a/simple_html/utils/templatize.py b/simple_html/templatize.py similarity index 99% rename from simple_html/utils/templatize.py rename to simple_html/templatize.py index cd3c0cc..0c5639e 100644 --- a/simple_html/utils/templatize.py +++ b/simple_html/templatize.py @@ -7,7 +7,7 @@ from uuid import uuid4 from simple_html import Node, SafeString, h1 -from simple_html.helpers import faster_escape, Tag +from simple_html.core import faster_escape, Tag _ARG_LOCATION = Union[str, int, tuple[int, str]] _TemplatePart = Union[ diff --git a/simple_html/utils/__init__.py b/simple_html/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_simple_html.py b/tests/test_core.py similarity index 98% rename from tests/test_simple_html.py rename to tests/test_core.py index 69d7428..fcba269 100644 --- a/tests/test_simple_html.py +++ b/tests/test_core.py @@ -26,8 +26,8 @@ render_styles, img, title, h1, h2, ) -from simple_html.helpers import escape_attribute_key, Tag -from simple_html.utils.templatize import templatize, _coalesce_func +from simple_html.core import escape_attribute_key +from simple_html.templatize import templatize, _coalesce_func def test_renders_no_children() -> None: diff --git a/tests/test_utils/test_templatize.py b/tests/test_templatize.py similarity index 99% rename from tests/test_utils/test_templatize.py rename to tests/test_templatize.py index 6ec70e9..7eb98e8 100644 --- a/tests/test_utils/test_templatize.py +++ b/tests/test_templatize.py @@ -4,7 +4,7 @@ import pytest from simple_html import Node, SafeString, Tag -from simple_html.utils.templatize import _is_valid_node_annotation +from simple_html.templatize import _is_valid_node_annotation test_annotations = [ # Basic valid Node types diff --git a/tests/test_utils/__init__.py b/tests/test_utils/__init__.py deleted file mode 100644 index e69de29..0000000 From e902babf9f46a1359ae48f0a422aaee1c050d5bc Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 30 Aug 2025 23:06:15 -0700 Subject: [PATCH 32/52] wip --- bench/simple.py | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/bench/simple.py b/bench/simple.py index 8dbf548..fa6945d 100644 --- a/bench/simple.py +++ b/bench/simple.py @@ -31,22 +31,33 @@ def hello_world_empty(objs: List[None]) -> None: render(h1("Hello, World!")) +@templatize +def _basic_html(list_items: list[Node]) -> Node: + return html( + head(title("A Great Web page!")), + body( + h1({"class": "great header", + "id": "header1", + "other_attr": "5"}, + "Welcome!"), + div( + p("What a great web page!!!", br, br), + ul(list_items) + ) + ) + ) + + def basic(objs: List[Tuple[str, str, List[str]]]) -> None: for title_, content, oks in objs: - render( - DOCTYPE_HTML5, - html( - head(title("A Great Web page!")), - body( - h1({"class": "great header", - "id": "header1", - "other_attr": "5"}, - "Welcome!"), - div( - p("What a great web page!!!", br, br), - ul([ - li({"class": "item-stuff"}, SafeString(ss)) - for ss in oks]))))) + + render(DOCTYPE_HTML5, + _basic_html( + [ + li({"class": "item-stuff"}, SafeString(ss)) + for ss in oks + ] + )) def basic_long(objs: List[Tuple[str, str, List[str]]]) -> None: From 0ed243dd0083f566b7cc55cf5086727e6f2a20f9 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 30 Aug 2025 23:26:37 -0700 Subject: [PATCH 33/52] wip --- setup.py | 2 +- simple_html/__init__.py | 2 +- simple_html/templatize.py | 47 +++++++++------ tests/test_templatize.py | 124 +++++++++++++++----------------------- 4 files changed, 77 insertions(+), 98 deletions(-) diff --git a/setup.py b/setup.py index 2536fa0..198465f 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ import tomllib except ModuleNotFoundError: # python 3.10 and earlier - import tomli as tomllib + import tomli as tomllib # type: ignore[no-redef] from pathlib import Path from setuptools import setup diff --git a/simple_html/__init__.py b/simple_html/__init__.py index 56a25f3..05b1c0b 100644 --- a/simple_html/__init__.py +++ b/simple_html/__init__.py @@ -1,4 +1,4 @@ -from simple_html.core import SafeString as SafeString, Tag, render as render, render_styles as render_styles, Node as Node, TagTuple as TagTuple +from simple_html.core import SafeString as SafeString, Tag as Tag, render as render, render_styles as render_styles, Node as Node, TagTuple as TagTuple DOCTYPE_HTML5 = SafeString("") diff --git a/simple_html/templatize.py b/simple_html/templatize.py index 0c5639e..d652d21 100644 --- a/simple_html/templatize.py +++ b/simple_html/templatize.py @@ -78,9 +78,18 @@ def _cannot_templatize_message(func: Callable[..., Any], _NO_ARGS_OR_KWARGS = "Templatizable functions cannot accept *args or **kwargs." def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_TemplatePart]: - warn_if_invalid_annotations(func) + match find_invalid_annotations(func): + case None: + pass + case list() as bad_params: + for bad_param, annotation in bad_params: + warnings.warn( + f"Parameter '{bad_param}' in function '{func.__name__}' has invalid annotation: {annotation}. " + f"Only `simple_html.Node`-compatible types are allowed in Templatize." + ) + case "no_args": + raise TypeError(f"Function '{func.__name__}' must have at least one parameter") - # TODO: try different types of arguments...? sig = inspect.signature(func) parameters = sig.parameters @@ -253,7 +262,7 @@ def _is_valid_node_annotation(annotation: Any) -> bool: return False -def warn_if_invalid_annotations(func: Templatizable) -> None: +def find_invalid_annotations(func: Templatizable) -> Union[list[tuple[str, Any]], None, Literal["no_args"]]: """ Decorator to validate that the function signature only uses valid Node annotations. Validates at decoration time, not runtime. @@ -266,19 +275,19 @@ def warn_if_invalid_annotations(func: Templatizable) -> None: # Check if function has at least one parameter if not sig.parameters: - raise TypeError(f"Function '{func.__name__}' must have at least one parameter") - - # Check each parameter's annotation - for param_name, param in sig.parameters.items(): - annotation = param.annotation - - # Skip parameters without annotations - if annotation == inspect.Parameter.empty: - continue - - # Check if annotation is valid for Node types - if not _is_valid_node_annotation(annotation): - warnings.warn( - f"Parameter '{param_name}' in function '{func.__name__}' has invalid annotation: {annotation}. " - f"Only Node-compatible types are allowed in Templatize." - ) + return "no_args" + else: + bad_params = [] + # Check each parameter's annotation + for param_name, param in sig.parameters.items(): + annotation = param.annotation + + # Skip parameters without annotations + if annotation == inspect.Parameter.empty: + continue + + # Check if annotation is valid for Node types + if not _is_valid_node_annotation(annotation): + bad_params.append((param_name, annotation)) + + return bad_params or None \ No newline at end of file diff --git a/tests/test_templatize.py b/tests/test_templatize.py index 7eb98e8..76a6f02 100644 --- a/tests/test_templatize.py +++ b/tests/test_templatize.py @@ -1,10 +1,10 @@ from decimal import Decimal -from typing import Union, Generator +from typing import Union, Generator, Any, Literal import pytest -from simple_html import Node, SafeString, Tag -from simple_html.templatize import _is_valid_node_annotation +from simple_html import Node, SafeString, Tag, h1 +from simple_html.templatize import _is_valid_node_annotation, Templatizable, find_invalid_annotations test_annotations = [ # Basic valid Node types @@ -121,80 +121,50 @@ (tuple[str, tuple[Node, ...], str, int], False, "Four-element tuple"), ] -# Run the tests -print("Testing _is_valid_node_annotation:") -passed = 0 -failed = 0 @pytest.mark.parametrize('annotation,expected_result,description', test_annotations) -def test_annotation(annotation, expected_result, description) -> None: +def test_annotation(annotation: Any, expected_result: bool, description: str) -> None: assert _is_valid_node_annotation(annotation) is expected_result, description -# -# -# for annotation, should_pass, description in test_cases: -# try: -# result = _is_valid_node_annotation(annotation) -# if result == should_pass: -# status = "✓ PASS" -# passed += 1 -# else: -# status = f"✗ FAIL (expected {should_pass}, got {result})" -# failed += 1 -# print(f"{status}: {description} - {annotation}") -# except Exception as e: -# print(f"✗ ERROR: {description} - {annotation}: {e}") -# failed += 1 -# -# print(f"\nResults: {passed} passed, {failed} failed, {len(test_cases)} total") -# -# # Note: The actual test functions need to be defined properly for this to work -# def process_node(node: Node, a: None) -> Node: -# return node -# -# -# def process_node_1() -> Node: -# return h1 -# -# -# def process_node_2(a: list[str], b: str, c: list[Node]) -> Node: -# return h1 -# -# -# def process_node_3(a: bool) -> Node: -# return h1 -# -# -# def process_node_4(x: tuple[str, tuple[Node, ...], str]) -> Node: -# return h1 -# -# -# def process_node_5(x: list[tuple[str, tuple[Node, ...], str] | str | SafeString]) -> Node: -# return h1 -# -# def process_node_6(a, b) -> Node: -# return h1 -# -# -# # Test the validation -# test_functions: list[tuple[Templatizable, bool, str]] = [ -# (process_node, False, "None annotation should fail"), -# (process_node_1, False, "no parameters should fail"), -# (process_node_2, True, "mixed valid types should pass"), -# (process_node_3, False, "bool should fail"), -# (process_node_4, True, "TagTuple should pass"), -# (process_node_5, True, "complex nested Union should pass"), -# (process_node_6, True, "unannotated args should pass"), -# ] -# -# for func, should_pass, description in test_functions: -# try: -# validate_node_annotations(func) -# if should_pass: -# print(f"✓ {func.__name__}: PASSED - {description}") -# else: -# print(f"✗ {func.__name__}: SHOULD HAVE FAILED - {description}") -# except TypeError as e: -# if not should_pass: -# print(f"✓ {func.__name__}: CORRECTLY FAILED - {description}") -# else: -# print(f"✗ {func.__name__}: UNEXPECTEDLY FAILED - {description}: {e}") + + +# Note: The actual test functions need to be defined properly for this to work +def process_node(node: Node, a: None) -> Node: + return node + + +def process_node_1() -> Node: + return h1 + + +def process_node_2(a: list[str], b: str, c: list[Node]) -> Node: + return h1 + + +def process_node_3(a: bool) -> Node: + return h1 + + +def process_node_4(x: tuple[str, tuple[Node, ...], str]) -> Node: + return h1 + + +def process_node_5(x: list[tuple[str, tuple[Node, ...], str] | str | SafeString]) -> Node: + return h1 + +def process_node_6(a, b) -> Node: # type: ignore[no-untyped-def] + return h1 + + +# Test the validation +test_functions: list[tuple[Templatizable, Union[list[tuple[str, Any]], Literal["no_args"], None], str]] = [ + (process_node, [("a", None)], "None annotation should fail"), + (process_node_1, "no_args", "should warn that there are no parameters"), + (process_node_2, None, "mixed valid types should pass"), + (process_node_3, [("a", bool)], "bool should fail"), + (process_node_4, None, "TagTuple should pass"), + (process_node_5, None, "complex nested Union should pass"), + (process_node_6, None, "unannotated args should pass"), +] +@pytest.mark.parametrize('func,expected_result,description', test_functions) +def test_annotations_are_properly_checked_on_functions(func: Templatizable, expected_result: Union[list[tuple[str, Any]], Literal["no_args"], None], description: str) -> None: + assert find_invalid_annotations(func) == expected_result, description From 3dde6981133cf790049785b4b3075114cdf604bd Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 30 Aug 2025 23:28:35 -0700 Subject: [PATCH 34/52] wip --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 198465f..e7c59b5 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name="simple-html", ext_modules=mypycify([ - "simple_html/core.py", + "simple_html", ]), author=project_data["authors"][0]["name"], packages=["simple_html"], From f3672a776a523f0e60b056e5a7aa7a4092c1a572 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 30 Aug 2025 23:32:28 -0700 Subject: [PATCH 35/52] wi --- pyproject.toml | 2 +- simple_html/core.py | 4 +--- simple_html/templatize.py | 25 +++++++++++-------------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 17c857f..431f691 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "simple-html" -version = "3.0.0" +version = "3.1.0" description="Template-less HTML rendering in Python" authors = [ {name="Keith Philpott"} diff --git a/simple_html/core.py b/simple_html/core.py index 8d2c878..f5d862a 100644 --- a/simple_html/core.py +++ b/simple_html/core.py @@ -1,8 +1,6 @@ -import inspect from decimal import Decimal from types import GeneratorType -from typing import Any, Union, Generator, Iterable, Callable, Final, TYPE_CHECKING, Protocol, Literal, Never, cast -from uuid import uuid4 +from typing import Any, Union, Generator, Iterable, Callable, Final, TYPE_CHECKING class SafeString: diff --git a/simple_html/templatize.py b/simple_html/templatize.py index d652d21..540a9bd 100644 --- a/simple_html/templatize.py +++ b/simple_html/templatize.py @@ -6,7 +6,7 @@ from typing import Union, Literal, Callable, Any, get_args, get_origin, Generator, ForwardRef from uuid import uuid4 -from simple_html import Node, SafeString, h1 +from simple_html import Node, SafeString from simple_html.core import faster_escape, Tag _ARG_LOCATION = Union[str, int, tuple[int, str]] @@ -78,17 +78,15 @@ def _cannot_templatize_message(func: Callable[..., Any], _NO_ARGS_OR_KWARGS = "Templatizable functions cannot accept *args or **kwargs." def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_TemplatePart]: - match find_invalid_annotations(func): - case None: - pass - case list() as bad_params: - for bad_param, annotation in bad_params: - warnings.warn( - f"Parameter '{bad_param}' in function '{func.__name__}' has invalid annotation: {annotation}. " - f"Only `simple_html.Node`-compatible types are allowed in Templatize." - ) - case "no_args": - raise TypeError(f"Function '{func.__name__}' must have at least one parameter") + result = find_invalid_annotations(func) + if isinstance(result, list): + for bad_param, annotation in result: + warnings.warn( + f"Parameter '{bad_param}' in function '{func.__name__}' has invalid annotation: {annotation}. " + f"Only `simple_html.Node`-compatible types are allowed in Templatize." + ) + elif result == "no_args": + raise TypeError(f"Function '{func.__name__}' must have at least one parameter") sig = inspect.signature(func) parameters = sig.parameters @@ -240,8 +238,7 @@ def _is_valid_node_annotation(annotation: Any) -> bool: if len(type_args) == 3: # TagTuple structure: (str, tuple[Node, ...], str) first_arg, second_arg, third_arg = type_args - if (first_arg == str and third_arg == str and - get_origin(second_arg) is tuple and len(get_args(second_arg)) >= 1): + if (first_arg is str and third_arg is str and get_origin(second_arg) is tuple and len(get_args(second_arg)) >= 1): # Check if the tuple contains Node types (like tuple[Node, ...]) inner_args = get_args(second_arg) return all(_is_valid_node_annotation(arg) for arg in inner_args if arg is not ...) From f1f645b1f294a689c5ac8dc9b68e4603b98b6e0b Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sun, 31 Aug 2025 22:52:42 -0700 Subject: [PATCH 36/52] wip --- simple_html/templatize.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simple_html/templatize.py b/simple_html/templatize.py index 540a9bd..6ce4709 100644 --- a/simple_html/templatize.py +++ b/simple_html/templatize.py @@ -227,7 +227,7 @@ def _is_valid_node_annotation(annotation: Any) -> bool: return True # Check for Union types (like Optional[Node] or Union[Node, str]) - elif (origin := get_origin(annotation)) is Union or (hasattr(types, 'UnionType') and isinstance(annotation, types.UnionType)): + elif get_origin(annotation) is Union or (hasattr(types, 'UnionType') and isinstance(annotation, types.UnionType)): union_args = get_args(annotation) # All union members must be valid Node types (except None for Optional) return all(_is_valid_node_annotation(arg) for arg in union_args if arg is not type(None)) From 52a4c994a7c6f1f7375f603605d20f5de16db6b3 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Tue, 9 Sep 2025 09:10:36 -0700 Subject: [PATCH 37/52] wip --- bench/simple.py | 389 ++++++++++++++++++++------------------ simple_html/templatize.py | 10 +- 2 files changed, 207 insertions(+), 192 deletions(-) diff --git a/bench/simple.py b/bench/simple.py index fa6945d..2ae0bf6 100644 --- a/bench/simple.py +++ b/bench/simple.py @@ -34,23 +34,22 @@ def hello_world_empty(objs: List[None]) -> None: @templatize def _basic_html(list_items: list[Node]) -> Node: return html( - head(title("A Great Web page!")), - body( - h1({"class": "great header", - "id": "header1", - "other_attr": "5"}, - "Welcome!"), - div( - p("What a great web page!!!", br, br), - ul(list_items) - ) + head(title("A Great Web page!")), + body( + h1({"class": "great header", + "id": "header1", + "other_attr": "5"}, + "Welcome!"), + div( + p("What a great web page!!!", br, br), + ul(list_items) ) + ) ) def basic(objs: List[Tuple[str, str, List[str]]]) -> None: for title_, content, oks in objs: - render(DOCTYPE_HTML5, _basic_html( [ @@ -134,6 +133,24 @@ def lorem_ipsum(titles: List[str]) -> None: render(_lorem_html(t)) +def _article(heading: str, + published_date_iso: str, + published_data_readable, + author: str, + read_time_minutes: str, + content: list[Node], + is_featured: bool = False): + return article( + {"class": "post" + ("featured-post" if is_featured else ""), }, + h2(heading), + p({"class": "post-meta"}, + "Published on ", time({"datetime": published_date_iso}, published_data_readable), + " by ", span({"class": "author"}, author), + " | ", span({"class": "read-time"}, read_time_minutes, " min read") + ), + content + ) + @templatize def _get_head(title_: str) -> Node: @@ -168,7 +185,8 @@ def _get_head(title_: str) -> Node: @templatize -def _html(t: str) -> Node: +def _html(t: str, + articles: list[Node]) -> Node: return html({"lang": "en"}, _get_head(title_=t), body( @@ -190,177 +208,7 @@ def _html(t: str) -> Node: ), main({"class": "container main-content"}, section({"class": "content-area"}, - article({"class": "post featured-post"}, - h2("Complete Guide to Modern Web Development in 2024"), - p({"class": "post-meta"}, - "Published on ", time({"datetime": "2024-03-15"}, "March 15, 2024"), - " by ", span({"class": "author"}, "Sarah Johnson"), - " | ", span({"class": "read-time"}, "12 min read") - ), - img({"src": "/images/web-dev-2024.jpg", - "alt": "Modern web development tools and frameworks", - "style": "width: 100%; height: 300px; object-fit: cover; border-radius: 8px;"}), - p( - "Web development has evolved significantly in recent years, transforming from simple static pages ", - "to complex, interactive applications that power our digital world. The landscape continues to change ", - "rapidly, driven by new technologies, frameworks, and methodologies that promise to make development ", - "faster, more efficient, and more accessible." - ), - h3("Key Technologies Shaping the Future"), - p("The modern web development ecosystem is built around several core technologies:"), - ul( - li("**Component-based frameworks** like React, Vue, and Angular that promote reusable UI components"), - li("**Progressive Web Apps (PWAs)** that bridge the gap between web and native applications"), - li("**Serverless architectures** using AWS Lambda, Vercel Functions, and Netlify Functions"), - li("**JAMstack** (JavaScript, APIs, Markup) for better performance and security"), - li("**GraphQL** for more efficient data fetching and API design"), - li("**TypeScript** for type-safe JavaScript development"), - li("**Edge computing** for reduced latency and improved user experience") - ), - h3("Framework Comparison"), - table({"class": "stats-table"}, - thead( - tr( - th("Framework"), - th("Learning Curve"), - th("Performance"), - th("Community"), - th("Use Case") - ) - ), - tbody( - tr( - td("React"), - td("Medium"), - td("High"), - td("Very Large"), - td("Complex UIs, SPAs") - ), - tr( - td("Vue.js"), - td("Easy"), - td("High"), - td("Large"), - td("Rapid prototyping, SME apps") - ), - tr( - td("Angular"), - td("Steep"), - td("High"), - td("Large"), - td("Enterprise applications") - ), - tr( - td("Svelte"), - td("Easy"), - td("Very High"), - td("Growing"), - td("Performance-critical apps") - ) - ) - ), - h3("Code Example: Modern Component"), - p("Here's an example of a modern React component using hooks and TypeScript:"), - pre({"class": "code-block"}, - code(""" - interface User { - id: number; - name: string; - email: string; - } - - const UserProfile: React.FC<{ userId: number }> = ({ userId }) => { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - fetchUser(userId) - .then(setUser) - .finally(() => setLoading(false)); - }, [userId]); - - if (loading) return
Loading...
; - if (!user) return
User not found
; - - return ( -
-

{user.name}

-

{user.email}

-
- ); - }; - """) - ), - h3("Best Practices for 2024"), - p("As we move forward in 2024, several best practices have emerged:"), - ol( - li("**Performance First**: Optimize for Core Web Vitals and user experience metrics"), - li("**Accessibility by Default**: Implement WCAG guidelines from the start of development"), - li("**Security-First Mindset**: Use CSP headers, sanitize inputs, and follow OWASP guidelines"), - li("**Mobile-First Design**: Start with mobile layouts and progressively enhance for larger screens"), - li("**Sustainable Web Development**: Optimize for energy efficiency and reduced carbon footprint") - ), - blockquote( - p("\"The best web developers are those who understand that technology should serve users, not the other way around.\""), - footer("— John Doe, Senior Frontend Architect at TechCorp") - ) - ), - - article({"class": "post"}, - h2("The Rise of AI in Development: Tools and Techniques"), - p({"class": "post-meta"}, - "Published on ", time({"datetime": "2024-03-10"}, "March 10, 2024"), - " by ", span({"class": "author"}, "Michael Chen"), - " | ", span({"class": "read-time"}, "8 min read") - ), - p( - "Artificial Intelligence is fundamentally transforming how we write, test, and deploy code. ", - "From intelligent autocomplete suggestions to automated bug detection and code generation, ", - "AI tools are becoming essential companions for modern developers." - ), - h3("Popular AI Development Tools"), - ul( - li("**GitHub Copilot**: AI-powered code completion and generation"), - li("**ChatGPT & GPT-4**: Code explanation, debugging, and architecture advice"), - li("**Amazon CodeWhisperer**: Real-time code suggestions with security scanning"), - li("**DeepCode**: AI-powered code review and vulnerability detection"), - li("**Kite**: Intelligent code completion for Python and JavaScript") - ), - p( - "These tools don't replace developers but rather augment their capabilities, ", - "allowing them to focus on higher-level problem solving and creative solutions." - ) - ), - - article({"class": "post"}, - h2("Python vs JavaScript: Which Language to Learn in 2024?"), - p({"class": "post-meta"}, - "Published on ", time({"datetime": "2024-03-05"}, "March 5, 2024"), - " by ", span({"class": "author"}, "Emily Rodriguez"), - " | ", span({"class": "read-time"}, "10 min read") - ), - p( - "The eternal debate continues: should new developers learn Python or JavaScript first? ", - "Both languages have their strengths and use cases, and the answer largely depends on ", - "your career goals and the type of projects you want to work on." - ), - h3("Python Advantages"), - ul( - li("Simple, readable syntax that's beginner-friendly"), - li("Excellent for data science, machine learning, and AI"), - li("Strong in automation, scripting, and backend development"), - li("Huge ecosystem of libraries and frameworks (Django, Flask, NumPy, pandas)") - ), - h3("JavaScript Advantages"), - ul( - li("Essential for web development (frontend and backend with Node.js)"), - li("Immediate visual feedback when learning"), - li("Huge job market and demand"), - li("Versatile: runs in browsers, servers, mobile apps, and desktop applications") - ), - p("The truth is, both languages are valuable, and learning one makes learning the other easier.") - ), - + articles, section({"class": "comment-section"}, h3("Join the Discussion"), form({"class": "comment-form", "action": "/submit-comment", "method": "POST"}, @@ -492,7 +340,180 @@ def _html(t: str) -> Node: def large_page(titles: list[str]) -> None: for t in titles: + articles = [ + _article( + "Complete Guide to Modern Web Development in 2024", + "2024-03-15", + "March 15, 2024", + "Sarah Johnson", + read_time_minutes=12, + content=[ + img({"src": "/images/web-dev-2024.jpg", + "alt": "Modern web development tools and frameworks", + "style": "width: 100%; height: 300px; object-fit: cover; border-radius: 8px;"}), + p( + "Web development has evolved significantly in recent years, transforming from simple static pages ", + "to complex, interactive applications that power our digital world. The landscape continues to change ", + "rapidly, driven by new technologies, frameworks, and methodologies that promise to make development ", + "faster, more efficient, and more accessible." + ), + h3("Key Technologies Shaping the Future"), + p("The modern web development ecosystem is built around several core technologies:"), + ul( + li("**Component-based frameworks** like React, Vue, and Angular that promote reusable UI components"), + li("**Progressive Web Apps (PWAs)** that bridge the gap between web and native applications"), + li("**Serverless architectures** using AWS Lambda, Vercel Functions, and Netlify Functions"), + li("**JAMstack** (JavaScript, APIs, Markup) for better performance and security"), + li("**GraphQL** for more efficient data fetching and API design"), + li("**TypeScript** for type-safe JavaScript development"), + li("**Edge computing** for reduced latency and improved user experience") + ), + h3("Framework Comparison"), + table({"class": "stats-table"}, + thead( + tr( + th("Framework"), + th("Learning Curve"), + th("Performance"), + th("Community"), + th("Use Case") + ) + ), + tbody( + tr( + td("React"), + td("Medium"), + td("High"), + td("Very Large"), + td("Complex UIs, SPAs") + ), + tr( + td("Vue.js"), + td("Easy"), + td("High"), + td("Large"), + td("Rapid prototyping, SME apps") + ), + tr( + td("Angular"), + td("Steep"), + td("High"), + td("Large"), + td("Enterprise applications") + ), + tr( + td("Svelte"), + td("Easy"), + td("Very High"), + td("Growing"), + td("Performance-critical apps") + ) + ) + ), + h3("Code Example: Modern Component"), + p("Here's an example of a modern React component using hooks and TypeScript:"), + pre({"class": "code-block"}, + code(""" + interface User { + id: number; + name: string; + email: string; + } + + const UserProfile: React.FC<{ userId: number }> = ({ userId }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + fetchUser(userId) + .then(setUser) + .finally(() => setLoading(false)); + }, [userId]); + + if (loading) return
Loading...
; + if (!user) return
User not found
; + + return ( +
+

{user.name}

+

{user.email}

+
+ ); + }; + """) + ), + h3("Best Practices for 2024"), + p("As we move forward in 2024, several best practices have emerged:"), + ol( + li("**Performance First**: Optimize for Core Web Vitals and user experience metrics"), + li("**Accessibility by Default**: Implement WCAG guidelines from the start of development"), + li("**Security-First Mindset**: Use CSP headers, sanitize inputs, and follow OWASP guidelines"), + li("**Mobile-First Design**: Start with mobile layouts and progressively enhance for larger screens"), + li("**Sustainable Web Development**: Optimize for energy efficiency and reduced carbon footprint") + ), + blockquote( + p("\"The best web developers are those who understand that technology should serve users, not the other way around.\""), + footer("— John Doe, Senior Frontend Architect at TechCorp") + )] + + ), + _article( + "The Rise of AI in Development: Tools and Techniques", + "2024-03-10", + "March 10, 2024", + "Michael Chen", + 8, + [p( + "Artificial Intelligence is fundamentally transforming how we write, test, and deploy code. ", + "From intelligent autocomplete suggestions to automated bug detection and code generation, ", + "AI tools are becoming essential companions for modern developers." + ), + h3("Popular AI Development Tools"), + ul( + li("**GitHub Copilot**: AI-powered code completion and generation"), + li("**ChatGPT & GPT-4**: Code explanation, debugging, and architecture advice"), + li("**Amazon CodeWhisperer**: Real-time code suggestions with security scanning"), + li("**DeepCode**: AI-powered code review and vulnerability detection"), + li("**Kite**: Intelligent code completion for Python and JavaScript") + ), + p( + "These tools don't replace developers but rather augment their capabilities, ", + "allowing them to focus on higher-level problem solving and creative solutions." + ) + ] + ), + + _article( + "Python vs JavaScript: Which Language to Learn in 2024?", + "2024-03-05", + "March 5, 2024", + "Emily Rodriguez", + 9, + [ + p( + "The eternal debate continues: should new developers learn Python or JavaScript first? ", + "Both languages have their strengths and use cases, and the answer largely depends on ", + "your career goals and the type of projects you want to work on." + ), + h3("Python Advantages"), + ul( + li("Simple, readable syntax that's beginner-friendly"), + li("Excellent for data science, machine learning, and AI"), + li("Strong in automation, scripting, and backend development"), + li("Huge ecosystem of libraries and frameworks (Django, Flask, NumPy, pandas)") + ), + h3("JavaScript Advantages"), + ul( + li("Essential for web development (frontend and backend with Node.js)"), + li("Immediate visual feedback when learning"), + li("Huge job market and demand"), + li("Versatile: runs in browsers, servers, mobile apps, and desktop applications") + ), + p("The truth is, both languages are valuable, and learning one makes learning the other easier.") + ] + )] + render( DOCTYPE_HTML5, - _html(t=t) + _html(t, articles) ) diff --git a/simple_html/templatize.py b/simple_html/templatize.py index 6ce4709..e7d73de 100644 --- a/simple_html/templatize.py +++ b/simple_html/templatize.py @@ -207,7 +207,7 @@ def template_function(*args: Node, **kwargs: Node) -> Node: def _is_valid_node_annotation(annotation: Any) -> bool: """Check if an annotation represents a valid Node type (recursive).""" - # Handle ForwardRef objects + if isinstance(annotation, ForwardRef): # Get the string argument from ForwardRef ref_name = annotation.__forward_arg__ @@ -249,7 +249,7 @@ def _is_valid_node_annotation(annotation: Any) -> bool: elif (origin := get_origin(annotation)) is not None: type_args = get_args(annotation) if type_args: - # For list[Node], Generator[Node, None, None], etc. + # For list[Node], Generator[Node, None, None] if origin is list: # All list element types must be valid Node types return all(_is_valid_node_annotation(arg) for arg in type_args) @@ -263,19 +263,13 @@ def find_invalid_annotations(func: Templatizable) -> Union[list[tuple[str, Any]] """ Decorator to validate that the function signature only uses valid Node annotations. Validates at decoration time, not runtime. - - :param func: The function to decorate. - :return: The original function if validation passes. - :raises: TypeError if the function has invalid annotations. """ sig = inspect.signature(func) - # Check if function has at least one parameter if not sig.parameters: return "no_args" else: bad_params = [] - # Check each parameter's annotation for param_name, param in sig.parameters.items(): annotation = param.annotation From d4ed2ee44a676fe58ebfb470daa78ec28b8efa9d Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Fri, 17 Oct 2025 21:04:41 -0700 Subject: [PATCH 38/52] wip --- bench/simple.py | 5 - simple_html/__init__.py | 2 +- simple_html/core.py | 2 +- simple_html/templatize.py | 284 -------------------------------------- tests/test_core.py | 77 ----------- tests/test_templatize.py | 170 ----------------------- 6 files changed, 2 insertions(+), 538 deletions(-) delete mode 100644 simple_html/templatize.py delete mode 100644 tests/test_templatize.py diff --git a/bench/simple.py b/bench/simple.py index 2ae0bf6..9be309d 100644 --- a/bench/simple.py +++ b/bench/simple.py @@ -23,7 +23,6 @@ blockquote, code, pre, form, label, input_, textarea, button, table, thead, tbody, tr, th, td ) from simple_html.core import Node -from simple_html.templatize import templatize def hello_world_empty(objs: List[None]) -> None: @@ -31,7 +30,6 @@ def hello_world_empty(objs: List[None]) -> None: render(h1("Hello, World!")) -@templatize def _basic_html(list_items: list[Node]) -> Node: return html( head(title("A Great Web page!")), @@ -107,7 +105,6 @@ def basic_long(objs: List[Tuple[str, str, List[str]]]) -> None: ) -@templatize def _lorem_html(title_: str) -> Node: return html( {"lang": "en"}, @@ -152,7 +149,6 @@ def _article(heading: str, ) -@templatize def _get_head(title_: str) -> Node: return head( meta({"charset": "UTF-8"}), @@ -184,7 +180,6 @@ def _get_head(title_: str) -> Node: ) -@templatize def _html(t: str, articles: list[Node]) -> Node: return html({"lang": "en"}, diff --git a/simple_html/__init__.py b/simple_html/__init__.py index 05b1c0b..6a743a0 100644 --- a/simple_html/__init__.py +++ b/simple_html/__init__.py @@ -1,4 +1,4 @@ -from simple_html.core import SafeString as SafeString, Tag as Tag, render as render, render_styles as render_styles, Node as Node, TagTuple as TagTuple +from simple_html.core import SafeString as SafeString, Tag as Tag, render as render, render_styles as render_styles, Node as Node, TagTuple as TagTuple, prerender as prerender DOCTYPE_HTML5 = SafeString("") diff --git a/simple_html/core.py b/simple_html/core.py index f5d862a..183d775 100644 --- a/simple_html/core.py +++ b/simple_html/core.py @@ -183,7 +183,7 @@ def _render(nodes: Iterable[Node], append_to_list: Callable[[str], None]) -> Non """ for node in nodes: # SafeString first because they are very common in performance-sensitive contexts, - # such as `templatize` and `prerender` + # such as `prerender` if type(node) is SafeString: append_to_list(node.safe_str) elif type(node) is str: diff --git a/simple_html/templatize.py b/simple_html/templatize.py deleted file mode 100644 index e7d73de..0000000 --- a/simple_html/templatize.py +++ /dev/null @@ -1,284 +0,0 @@ -import inspect -import types -import warnings -from decimal import Decimal -from types import GeneratorType -from typing import Union, Literal, Callable, Any, get_args, get_origin, Generator, ForwardRef -from uuid import uuid4 - -from simple_html import Node, SafeString -from simple_html.core import faster_escape, Tag - -_ARG_LOCATION = Union[str, int, tuple[int, str]] -_TemplatePart = Union[ - tuple[Literal["STATIC"], str], - tuple[Literal["ARG"], _ARG_LOCATION], # the str is the arg name -] - - -Templatizable = Callable[..., Node] - - -def _traverse_node(node: Node, - template_parts: list[_TemplatePart], - sentinel_objects: dict[int, _ARG_LOCATION]) -> None: - - def append_static(obj: str) -> None: - return template_parts.append(("STATIC", obj)) - - def append_arg(arg: _ARG_LOCATION) -> None: - return template_parts.append(("ARG", arg)) - - node_id = id(node) - - # note that this should stay up-to-speed with the `Node` definition - if type(node) is tuple: - # TagTuple - append_static(node[0]) - for n in node[1]: - _traverse_node(n, template_parts, sentinel_objects) - append_static(node[2]) - elif type(node) is str: - # Check if this string is one of our sentinels - if node_id in sentinel_objects: - # This is an argument placeholder - add a marker - append_arg(sentinel_objects[node_id]) - else: - # Regular string content - append_static(faster_escape(node)) - elif type(node) is SafeString: - # SafeString content - check if it's a sentinel - if node_id in sentinel_objects: - append_arg(sentinel_objects[node_id]) - else: - append_static(node.safe_str) - elif type(node) is Tag: - append_static(node.rendered) - elif isinstance(node, (list, GeneratorType)): - if node_id in sentinel_objects: - append_arg(sentinel_objects[node_id]) - else: - for n in node: - _traverse_node(n, template_parts, sentinel_objects) - elif isinstance(node, (int, float, Decimal)): - if node_id in sentinel_objects: - append_arg(sentinel_objects[node_id]) - else: - # Other types - convert to string - append_static(str(node)) - else: - print(node) - raise TypeError(f"Got unexpected type for node: {type(node)}") - -def _cannot_templatize_message(func: Callable[..., Any], - extra_message: str) -> str: - return f"Could not templatize function '{func.__name__}'. {extra_message}" - -_SHOULD_NOT_PERFORM_LOGIC = "Templatizable functions should not perform logic." -_NO_ARGS_OR_KWARGS = "Templatizable functions cannot accept *args or **kwargs." - -def _probe_func(func: Templatizable, variant: Literal[1, 2, 3]) -> list[_TemplatePart]: - result = find_invalid_annotations(func) - if isinstance(result, list): - for bad_param, annotation in result: - warnings.warn( - f"Parameter '{bad_param}' in function '{func.__name__}' has invalid annotation: {annotation}. " - f"Only `simple_html.Node`-compatible types are allowed in Templatize." - ) - elif result == "no_args": - raise TypeError(f"Function '{func.__name__}' must have at least one parameter") - - sig = inspect.signature(func) - parameters = sig.parameters - - if not parameters: - raise ValueError("Function must have at least one parameter") - - # probe function with properly typed arguments - # Use interned sentinel objects that we can identify by id - sentinel_objects: dict[int, _ARG_LOCATION] = {} - probe_args: list[Node] = [] - probe_kwargs: dict[str, Node] = {} - - sentinel: Node - for i, (param_name, param) in enumerate(parameters.items()): - if variant == 1: - sentinel = f"__SENTINEL_{param_name}_{id(object())}__" - elif variant == 2: - sentinel = uuid4().hex - else: - sentinel = [id(object())] - - sentinel_id = id(sentinel) - - if param.kind == inspect.Parameter.POSITIONAL_ONLY: - probe_args.append(sentinel) - sentinel_objects[sentinel_id] = i - elif param.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD: - probe_args.append(sentinel) - # allow either an index or key lookup - sentinel_objects[sentinel_id] = (i, param.name) - elif param.kind == inspect.Parameter.KEYWORD_ONLY: - probe_kwargs[param_name] = sentinel - sentinel_objects[sentinel_id] = param.name - elif param.kind == inspect.Parameter.VAR_POSITIONAL: - raise AssertionError(_cannot_templatize_message(func, _NO_ARGS_OR_KWARGS)) - - elif param.kind == inspect.Parameter.VAR_KEYWORD: - raise AssertionError(_cannot_templatize_message(func, _NO_ARGS_OR_KWARGS)) - - try: - template_node = func(*probe_args, **probe_kwargs) - except Exception as e: - raise Exception( - e, - AssertionError(_cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC)) - ) - - # traverse `Node` tree structure to find usages of arguments by id - template_parts: list[_TemplatePart] = [] - - _traverse_node(template_node, template_parts, sentinel_objects) - - return template_parts - - -_CoalescedPart = Union[_ARG_LOCATION, SafeString] - -def _coalesce_func(func: Templatizable) -> list[_CoalescedPart]: - template_part_lists: tuple[list[_TemplatePart], list[_TemplatePart], list[_TemplatePart]] = ( - _probe_func(func, 1), - _probe_func(func, 2), - _probe_func(func, 3) - ) - assert len(template_part_lists[0]) == len(template_part_lists[1]) == len(template_part_lists[2]), _cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC) - - for part_1, part_2, part_3 in zip(*template_part_lists): - assert part_1[0] == part_2[0] == part_3[0], _cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC) - if part_1[0] == "STATIC": - assert (part_1[1] == part_2[1] == part_3[1]), _cannot_templatize_message(func, _SHOULD_NOT_PERFORM_LOGIC) - - # convert non-argument nodes to strings and coalesce for speed - coalesced_parts: list[_CoalescedPart] = [] # string's are for parameter names - current_static: list[str] = [] - - for part_type, content in template_part_lists[0]: - if part_type == 'STATIC': - current_static.append(str(content)) - else: # ARG - # Flush accumulated static content - if current_static: - coalesced_parts.append(SafeString(''.join(current_static))) - current_static = [] - coalesced_parts.append(content) - - # Flush any remaining static content - if current_static: - coalesced_parts.append(SafeString(''.join(current_static))) - - return coalesced_parts - -def _get_arg_val(args: tuple[Node, ...], - kwargs: dict[str, Node], - location: _ARG_LOCATION) -> Node: - if isinstance(location, tuple): - int_loc, str_loc = location - if len(args) >= int_loc + 1: - return args[int_loc] - else: - return kwargs[str_loc] - elif isinstance(location, int): - return args[location] - else: - return kwargs[location] - - -def templatize(func: Templatizable) -> Callable[..., Node]: - coalesced_parts = _coalesce_func(func) - - def template_function(*args: Node, **kwargs: Node) -> Node: - return [ - part if isinstance(part, SafeString) else _get_arg_val(args, kwargs, part) - for part in coalesced_parts - ] - - return template_function - - -def _is_valid_node_annotation(annotation: Any) -> bool: - """Check if an annotation represents a valid Node type (recursive).""" - - if isinstance(annotation, ForwardRef): - # Get the string argument from ForwardRef - ref_name = annotation.__forward_arg__ - # Check if it refers to a valid Node type - return ref_name in ('Node', 'Tag', 'TagTuple') - - # Handle string literals (like 'Node' in list['Node']) - elif isinstance(annotation, str): - return annotation in ('Node', 'Tag', 'TagTuple') - - # Direct Node type - elif annotation == Node: - return True - - # Basic valid Node component types - elif annotation in (str, int, float, Decimal, SafeString, Tag): - return True - - # Check for Union types (like Optional[Node] or Union[Node, str]) - elif get_origin(annotation) is Union or (hasattr(types, 'UnionType') and isinstance(annotation, types.UnionType)): - union_args = get_args(annotation) - # All union members must be valid Node types (except None for Optional) - return all(_is_valid_node_annotation(arg) for arg in union_args if arg is not type(None)) - - # Check for tuple types - specifically TagTuple: tuple[str, tuple[Node, ...], str] - elif get_origin(annotation) is tuple: - type_args = get_args(annotation) - if len(type_args) == 3: - # TagTuple structure: (str, tuple[Node, ...], str) - first_arg, second_arg, third_arg = type_args - if (first_arg is str and third_arg is str and get_origin(second_arg) is tuple and len(get_args(second_arg)) >= 1): - # Check if the tuple contains Node types (like tuple[Node, ...]) - inner_args = get_args(second_arg) - return all(_is_valid_node_annotation(arg) for arg in inner_args if arg is not ...) - # If it's not a valid TagTuple structure, it's invalid - return False - - # Check for generic types like list[Node], Generator[Node, None, None], etc. - elif (origin := get_origin(annotation)) is not None: - type_args = get_args(annotation) - if type_args: - # For list[Node], Generator[Node, None, None] - if origin is list: - # All list element types must be valid Node types - return all(_is_valid_node_annotation(arg) for arg in type_args) - elif origin is Generator or (hasattr(origin, '__name__') and 'Generator' in str(origin)): - # For Generator[Node, None, None], only check the first type argument - return _is_valid_node_annotation(type_args[0]) if type_args else False - return False - - -def find_invalid_annotations(func: Templatizable) -> Union[list[tuple[str, Any]], None, Literal["no_args"]]: - """ - Decorator to validate that the function signature only uses valid Node annotations. - Validates at decoration time, not runtime. - """ - sig = inspect.signature(func) - - if not sig.parameters: - return "no_args" - else: - bad_params = [] - for param_name, param in sig.parameters.items(): - annotation = param.annotation - - # Skip parameters without annotations - if annotation == inspect.Parameter.empty: - continue - - # Check if annotation is valid for Node types - if not _is_valid_node_annotation(annotation): - bad_params.append((param_name, annotation)) - - return bad_params or None \ No newline at end of file diff --git a/tests/test_core.py b/tests/test_core.py index fcba269..212f8c5 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -27,7 +27,6 @@ img, title, h1, h2, ) from simple_html.core import escape_attribute_key -from simple_html.templatize import templatize, _coalesce_func def test_renders_no_children() -> None: @@ -271,79 +270,3 @@ def test_tag_repr() -> None: def test_render_number_attributes() -> None: assert render(div({"x": 1, "y": 2.01, "z": Decimal("3.02")})) == '
' - -def test_templatize() -> None: - def greet(name: str, age: int) -> Node: - return html( - head(title("hi, ", name)), - body( - div({"class": "content", - "blabla": "bla"}, - # raw str / int - h1("hi ", name, "I'm ", age), - # tag - br, - ["ok", name, "hmm"], - (name for _ in range(3)) - ) - ) - ) - - - expected = """hi, John Doe

hi John DoeI'm 100


okJohn DoehmmJohn DoeJohn DoeJohn Doe
""" - assert render(greet("John Doe", 100)) == expected - - templatized = templatize(greet) - assert render(templatized(name="John Doe", age=100)) == expected - assert render(templatized("John Doe", age=100)) == expected - assert render(templatized("John Doe", 100)) == expected - -def test_templatize_fails_for_arbitrary_logic() -> None: - def greet(name: str) -> Node: - return html("Your name is ", - name, - " and this is what it looks like twice: ", - name + name) - - with pytest.raises(AssertionError): - templatize(greet) - - -def test_templatize_fails_for_differently_sized_parts() -> None: - size = cycle([1, 2]) - def greet(name: str) -> Node: - if next(size) == 1: - return div(name) - else: - return div(name, "bad") - - assert render(greet("abc")) == "
abc
" - assert render(greet("abc")) == "
abcbad
" - - with pytest.raises(AssertionError): - templatize(greet) - - -def test_templatize_coalescing() -> None: - def greet(name: str) -> Node: - return body(div("Your name is ", name)) - - assert _coalesce_func(greet) == [ - SafeString("
Your name is "), # yay - (0, "name"), # arg location - SafeString("
"), # yay - ] - -def test_template_handles_node_arg() -> None: - @templatize - def hi(node: Node) -> Node: - return div(node) - - assert hi(div) == [ - SafeString(safe_str='
'), div, SafeString(safe_str='
') - ] - assert hi([["ok"], 5, h2("h2")]) == [ - SafeString("
"), - [["ok"], 5, h2("h2")], - SafeString('
'), - ] \ No newline at end of file diff --git a/tests/test_templatize.py b/tests/test_templatize.py deleted file mode 100644 index 76a6f02..0000000 --- a/tests/test_templatize.py +++ /dev/null @@ -1,170 +0,0 @@ -from decimal import Decimal -from typing import Union, Generator, Any, Literal - -import pytest - -from simple_html import Node, SafeString, Tag, h1 -from simple_html.templatize import _is_valid_node_annotation, Templatizable, find_invalid_annotations - -test_annotations = [ - # Basic valid Node types - (Node, True, "Direct Node type"), - (str, True, "Basic string type"), - (int, True, "Basic int type"), - (float, True, "Basic float type"), - (Decimal, True, "Decimal type"), - (SafeString, True, "SafeString type"), - (Tag, True, "Tag type"), - - # Basic invalid types - (bool, False, "bool is not a valid Node type"), - (type(None), False, "None type"), - (dict, False, "dict type"), - (set, False, "set type"), - (bytes, False, "bytes type"), - (complex, False, "complex number type"), - (Exception, False, "Exception type"), - - # List types - valid - (list[Node], True, "List of Nodes"), - (list[str], True, "List of strings"), - (list[int], True, "List of ints"), - (list[float], True, "List of floats"), - (list[Decimal], True, "List of Decimals"), - (list[SafeString], True, "List of SafeStrings"), - (list[Tag], True, "List of Tags"), - - # List types - invalid - (list[bool], False, "List of bools"), - (list[dict[str, str]], False, "List of dicts"), - (list[set[str]], False, "List of sets"), - (list[bytes], False, "List of bytes"), - - # Union types - all valid - (Union[Node, str], True, "Union with Node and str"), - (Union[str, int], True, "Union of valid types"), - (Union[Node, int, float], True, "Union with multiple valid types"), - (Union[SafeString, Tag], True, "Union of SafeString and Tag"), - (Union[list[Node], str], True, "Union of list[Node] and str"), - - # Union types - with invalid members - (Union[bool, str], False, "Union with invalid bool"), - (Union[Node, dict], False, "Union with invalid dict"), - (Union[str, int, bool], False, "Union mixing valid and invalid"), - (Union[list[bool], str], False, "Union with invalid list[bool]"), - - # Optional types (Union with None) - (Union[Node, None], True, "Optional Node"), - (Union[str, None], True, "Optional str"), - (Union[list[Node], None], True, "Optional list[Node]"), - (Union[bool, None], False, "Optional bool (still invalid)"), - - # Tuple types - TagTuple structures (valid) - (tuple[str, tuple[Node, ...], str], True, "TagTuple structure"), - (tuple[str, tuple[str, ...], str], True, "TagTuple with strings"), - (tuple[str, tuple[int, ...], str], True, "TagTuple with ints"), - (tuple[str, tuple[SafeString, ...], str], True, "TagTuple with SafeStrings"), - (tuple[str, tuple[Union[Node, str], ...], str], True, "TagTuple with Union types"), - - # Tuple types - invalid structures - (tuple[int, str], False, "Invalid tuple structure (wrong length)"), - (tuple[str, str, str], False, "Invalid tuple structure (middle not tuple)"), - (tuple[int, tuple[Node, ...], str], False, "Invalid tuple structure (first not str)"), - (tuple[str, tuple[Node, ...], int], False, "Invalid tuple structure (last not str)"), - (tuple[str, tuple[bool, ...], str], False, "TagTuple with invalid bool"), - (tuple[str, list[Node], str], False, "TagTuple with list instead of tuple"), - - # Generator types - (Generator[Node, None, None], True, "Generator of Nodes"), - (Generator[str, None, None], True, "Generator of strings"), - (Generator[int, None, None], True, "Generator of ints"), - (Generator[SafeString, None, None], True, "Generator of SafeStrings"), - (Generator[bool, None, None], False, "Generator of bools"), - (Generator[dict[str, str], None, None], False, "Generator of dicts"), - (Generator[Union[Node, str], None, None], True, "Generator of Union types"), - (Generator[list[Node], None, None], True, "Generator of list[Node]"), - - # Complex nested structures - (list[tuple[str, tuple[Node, ...], str] | str | SafeString], True, "Complex nested Union"), - (list[Union[Node, str, int]], True, "List of Union with valid types"), - (list[Union[bool, str]], False, "List of Union with invalid bool"), - (Union[list[Node], tuple[str, tuple[Node, ...], str]], True, "Union of complex types"), - (list[list[Node]], True, "Nested list of Nodes"), - (list[list[str]], True, "Nested list of strings"), - (list[list[bool]], False, "Nested list of bools"), - - # Generator with complex types - (Generator[tuple[str, tuple[Node, ...], str], None, None], True, "Generator of TagTuples"), - (Generator[Union[Node, str], None, None], True, "Generator of Union types"), - (Generator[list[Node], None, None], True, "Generator of list[Node]"), - - # Edge cases with deeply nested types - (list[Union[tuple[str, tuple[Node, ...], str], Node, str]], True, "Deeply nested valid types"), - (Union[Generator[Node, None, None], list[str]], True, "Union of Generator and list"), - (list[Generator[Node, None, None]], True, "List of Generators"), - (tuple[str, tuple[Union[Node, SafeString, str], ...], str], True, "TagTuple with complex Union"), - - # More invalid edge cases - (tuple[str, tuple[Union[bool, str], ...], str], False, "TagTuple with invalid Union"), - (list[tuple[str, str, str]], False, "List of invalid tuples"), - (Union[Generator[bool, None, None], str], False, "Union with invalid Generator"), - (list[Union[dict[int, int], str]], False, "List with invalid Union member"), - - # Types that might be confused with valid ones - (type, False, "type itself"), - (object, False, "object type"), - (any, False, "any (lowercase)") if 'any' in globals() else (str, True, "any not defined"), - - # Additional container types that should be invalid - (tuple[Node], False, "Single-element tuple (not TagTuple)"), - (tuple[Node, str], False, "Two-element tuple (not TagTuple)"), - (tuple[str, tuple[Node, ...], str, int], False, "Four-element tuple"), -] - - -@pytest.mark.parametrize('annotation,expected_result,description', test_annotations) -def test_annotation(annotation: Any, expected_result: bool, description: str) -> None: - assert _is_valid_node_annotation(annotation) is expected_result, description - - -# Note: The actual test functions need to be defined properly for this to work -def process_node(node: Node, a: None) -> Node: - return node - - -def process_node_1() -> Node: - return h1 - - -def process_node_2(a: list[str], b: str, c: list[Node]) -> Node: - return h1 - - -def process_node_3(a: bool) -> Node: - return h1 - - -def process_node_4(x: tuple[str, tuple[Node, ...], str]) -> Node: - return h1 - - -def process_node_5(x: list[tuple[str, tuple[Node, ...], str] | str | SafeString]) -> Node: - return h1 - -def process_node_6(a, b) -> Node: # type: ignore[no-untyped-def] - return h1 - - -# Test the validation -test_functions: list[tuple[Templatizable, Union[list[tuple[str, Any]], Literal["no_args"], None], str]] = [ - (process_node, [("a", None)], "None annotation should fail"), - (process_node_1, "no_args", "should warn that there are no parameters"), - (process_node_2, None, "mixed valid types should pass"), - (process_node_3, [("a", bool)], "bool should fail"), - (process_node_4, None, "TagTuple should pass"), - (process_node_5, None, "complex nested Union should pass"), - (process_node_6, None, "unannotated args should pass"), -] -@pytest.mark.parametrize('func,expected_result,description', test_functions) -def test_annotations_are_properly_checked_on_functions(func: Templatizable, expected_result: Union[list[tuple[str, Any]], Literal["no_args"], None], description: str) -> None: - assert find_invalid_annotations(func) == expected_result, description From bd0d3d2b21db41a54f91b2f6787d1bb04454c378 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 18 Oct 2025 16:31:56 -0700 Subject: [PATCH 39/52] wip --- tests/test_core.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index 212f8c5..0e23e85 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,9 +1,7 @@ import json from decimal import Decimal -from itertools import cycle from typing import Generator -import pytest from simple_html import ( SafeString, @@ -24,7 +22,7 @@ DOCTYPE_HTML5, render, render_styles, - img, title, h1, h2, + img, ) from simple_html.core import escape_attribute_key From a9079d13b230cbedb1a57c2766e092c655f37d53 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 18 Oct 2025 16:35:16 -0700 Subject: [PATCH 40/52] wip --- .github/workflows/push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 65bfbec..ee7021d 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -6,7 +6,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] poetry-version: [2.1] os: [ubuntu-24.04, macos-latest, windows-latest] runs-on: ${{ matrix.os }} From 0287d3892bfffb8cdbfeadcec97ba30d8f2bf9e9 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 18 Oct 2025 16:37:40 -0700 Subject: [PATCH 41/52] wip --- poetry.lock | 316 +++++++++++++++++++++++++++---------------------- pyproject.toml | 4 +- 2 files changed, 179 insertions(+), 141 deletions(-) diff --git a/poetry.lock b/poetry.lock index e3cfa36..851d40b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "colorama" @@ -88,121 +88,149 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "markupsafe" -version = "3.0.2" +version = "3.0.3" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] [[package]] name = "mypy" -version = "1.17.1" +version = "1.18.2" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972"}, - {file = "mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7"}, - {file = "mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df"}, - {file = "mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390"}, - {file = "mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94"}, - {file = "mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b"}, - {file = "mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58"}, - {file = "mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5"}, - {file = "mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd"}, - {file = "mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b"}, - {file = "mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5"}, - {file = "mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b"}, - {file = "mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb"}, - {file = "mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403"}, - {file = "mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056"}, - {file = "mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341"}, - {file = "mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb"}, - {file = "mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19"}, - {file = "mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7"}, - {file = "mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81"}, - {file = "mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6"}, - {file = "mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849"}, - {file = "mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14"}, - {file = "mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a"}, - {file = "mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733"}, - {file = "mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd"}, - {file = "mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0"}, - {file = "mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a"}, - {file = "mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91"}, - {file = "mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed"}, - {file = "mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9"}, - {file = "mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99"}, - {file = "mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8"}, - {file = "mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8"}, - {file = "mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259"}, - {file = "mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d"}, - {file = "mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9"}, - {file = "mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01"}, + {file = "mypy-1.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eab0cf6294dafe397c261a75f96dc2c31bffe3b944faa24db5def4e2b0f77c"}, + {file = "mypy-1.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7a780ca61fc239e4865968ebc5240bb3bf610ef59ac398de9a7421b54e4a207e"}, + {file = "mypy-1.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448acd386266989ef11662ce3c8011fd2a7b632e0ec7d61a98edd8e27472225b"}, + {file = "mypy-1.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f9e171c465ad3901dc652643ee4bffa8e9fef4d7d0eece23b428908c77a76a66"}, + {file = "mypy-1.18.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:592ec214750bc00741af1f80cbf96b5013d81486b7bb24cb052382c19e40b428"}, + {file = "mypy-1.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:7fb95f97199ea11769ebe3638c29b550b5221e997c63b14ef93d2e971606ebed"}, + {file = "mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f"}, + {file = "mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341"}, + {file = "mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d"}, + {file = "mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86"}, + {file = "mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37"}, + {file = "mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8"}, + {file = "mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34"}, + {file = "mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764"}, + {file = "mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893"}, + {file = "mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914"}, + {file = "mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8"}, + {file = "mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074"}, + {file = "mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc"}, + {file = "mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e"}, + {file = "mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986"}, + {file = "mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d"}, + {file = "mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba"}, + {file = "mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544"}, + {file = "mypy-1.18.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:62f0e1e988ad41c2a110edde6c398383a889d95b36b3e60bcf155f5164c4fdce"}, + {file = "mypy-1.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8795a039bab805ff0c1dfdb8cd3344642c2b99b8e439d057aba30850b8d3423d"}, + {file = "mypy-1.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ca1e64b24a700ab5ce10133f7ccd956a04715463d30498e64ea8715236f9c9c"}, + {file = "mypy-1.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d924eef3795cc89fecf6bedc6ed32b33ac13e8321344f6ddbf8ee89f706c05cb"}, + {file = "mypy-1.18.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20c02215a080e3a2be3aa50506c67242df1c151eaba0dcbc1e4e557922a26075"}, + {file = "mypy-1.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:749b5f83198f1ca64345603118a6f01a4e99ad4bf9d103ddc5a3200cc4614adf"}, + {file = "mypy-1.18.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25a9c8fb67b00599f839cf472713f54249a62efd53a54b565eb61956a7e3296b"}, + {file = "mypy-1.18.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2b9c7e284ee20e7598d6f42e13ca40b4928e6957ed6813d1ab6348aa3f47133"}, + {file = "mypy-1.18.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6985ed057513e344e43a26cc1cd815c7a94602fb6a3130a34798625bc2f07b6"}, + {file = "mypy-1.18.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22f27105f1525ec024b5c630c0b9f36d5c1cc4d447d61fe51ff4bd60633f47ac"}, + {file = "mypy-1.18.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:030c52d0ea8144e721e49b1f68391e39553d7451f0c3f8a7565b59e19fcb608b"}, + {file = "mypy-1.18.2-cp39-cp39-win_amd64.whl", hash = "sha256:aa5e07ac1a60a253445797e42b8b2963c9675563a94f11291ab40718b016a7a0"}, + {file = "mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e"}, + {file = "mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b"}, ] [package.dependencies] @@ -360,60 +388,70 @@ type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.deve [[package]] name = "tomli" -version = "2.2.1" +version = "2.3.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["dev"] markers = "python_version < \"3.11\"" files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, + {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, + {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, + {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, + {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, + {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, + {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, + {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, + {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, + {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, + {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, + {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, + {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, + {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, + {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, ] [[package]] name = "typing-extensions" -version = "4.14.1" +version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, - {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "f13d075b341576d6520c0808e3bcb76e63c106297f18e79eb53c33b0b92e2cca" +content-hash = "aa1e0fc6567208e34764ca9f38470d70d3ca432428676c6f06c84024fee2c167" diff --git a/pyproject.toml b/pyproject.toml index 431f691..ba4d00a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ python = "^3.9" [tool.poetry.group.dev.dependencies] jinja2 = "3.1.6" -mypy = "1.17.1" +mypy = "1.18.2" pytest = "8.4.1" setuptools = "80.9.0" fast-html = "1.0.12" @@ -45,7 +45,7 @@ ruff = "0.12.8" [build-system] requires = [ "poetry-core>=1.0.0", - "mypy[mypyc]==1.17.1", + "mypy[mypyc]==1.18.2", "tomli==2.2.1" ] From 0e860c7a3ac4472252ed58484350af2f002ac97c Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sat, 18 Oct 2025 20:40:58 -0700 Subject: [PATCH 42/52] wip --- bench/simple.py | 65 ++++++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/bench/simple.py b/bench/simple.py index 9be309d..cda1ca8 100644 --- a/bench/simple.py +++ b/bench/simple.py @@ -22,7 +22,7 @@ nav, a, main, section, article, aside, footer, span, img, time, blockquote, code, pre, form, label, input_, textarea, button, table, thead, tbody, tr, th, td ) -from simple_html.core import Node +from simple_html.core import Node, prerender def hello_world_empty(objs: List[None]) -> None: @@ -179,6 +179,38 @@ def _get_head(title_: str) -> Node: """) ) +_footer = prerender( + footer({"class": "site-footer"}, + div({"class": "container"}, + div({"class": "footer-content"}, + div({"class": "footer-section"}, + h4("About Tech Insights"), + p("Your go-to resource for web development tutorials, programming guides, and the latest technology trends. We help developers stay current with industry best practices.") + ), + div({"class": "footer-section"}, + h4("Quick Links"), + ul( + li(a({"href": "/privacy"}, "Privacy Policy")), + li(a({"href": "/terms"}, "Terms of Service")), + li(a({"href": "/sitemap"}, "Sitemap")), + li(a({"href": "/advertise"}, "Advertise")) + ) + ), + div({"class": "footer-section"}, + h4("Contact Info"), + p("Email: hello@techinsights.dev"), + p("Location: San Francisco, CA"), + p("Phone: (555) 123-4567") + ) + ), + hr, + div({"class": "footer-bottom"}, + p("© 2024 Tech Insights. All rights reserved. Built with simple_html library."), + p("Made with ❤️ for the developer community") + ) + ) + ) +) def _html(t: str, articles: list[Node]) -> Node: @@ -299,36 +331,7 @@ def _html(t: str, ) ), - footer({"class": "site-footer"}, - div({"class": "container"}, - div({"class": "footer-content"}, - div({"class": "footer-section"}, - h4("About Tech Insights"), - p("Your go-to resource for web development tutorials, programming guides, and the latest technology trends. We help developers stay current with industry best practices.") - ), - div({"class": "footer-section"}, - h4("Quick Links"), - ul( - li(a({"href": "/privacy"}, "Privacy Policy")), - li(a({"href": "/terms"}, "Terms of Service")), - li(a({"href": "/sitemap"}, "Sitemap")), - li(a({"href": "/advertise"}, "Advertise")) - ) - ), - div({"class": "footer-section"}, - h4("Contact Info"), - p("Email: hello@techinsights.dev"), - p("Location: San Francisco, CA"), - p("Phone: (555) 123-4567") - ) - ), - hr, - div({"class": "footer-bottom"}, - p("© 2024 Tech Insights. All rights reserved. Built with simple_html library."), - p("Made with ❤️ for the developer community") - ) - ) - ) + _footer ) ) From a63bfef17effa542567176620662df226a9e8613 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Tue, 21 Oct 2025 21:19:36 -0700 Subject: [PATCH 43/52] wip --- README.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/README.md b/README.md index e0983d6..49e435c 100644 --- a/README.md +++ b/README.md @@ -225,3 +225,51 @@ node = custom_elem( render(node) # Wow ``` + +### Optimization + +#### `prerender` + +`prerender` is a very simple function. It simply `render`s a `Node` and encapsulates the resulting string inside +a `SafeString` (so its contents won't be escaped again). It's most useful at the top level, so its contents are +rendered only once. A simple use case might be website footers: + +```python +from simple_html import SafeString, prerender, footer, div, a, head, body, title, h1, html, render + +prerendered_footer: SafeString = prerender( + footer( + div(a({"href": "/about"}, "About Us")), + div(a({"href": "/blog"}, "Blog")), + div(a({"href": "/contact"}, "Contact")) + ) +) + + +def render_page(page_title: str) -> str: + return render( + html( + head(title(page_title)), + body( + h1(page_title), + prerendered_footer # this is extremely fast to render + ) + ) + ) +``` +This greatly reduces the amount of work `render` needs to do on the content when outputting HTML. + +#### Caching +You may want to cache rendered content. This is easy to do in many ways; the main thing to keep in +mind is you'll likely want to return a `SafeString`. Again, `prerender` is a natural fit: + +```python +from simple_html import prerender, SafeString, h1 +from functools import lru_cache + +@lru_cache +def greeting(name: str) -> SafeString: + return prerender( + h1(f"Hello, {name}") + ) +``` \ No newline at end of file From 09778de9d32b7c9cc9826389440e4951fc01ae78 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Tue, 21 Oct 2025 21:20:00 -0700 Subject: [PATCH 44/52] wip --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 49e435c..65d0606 100644 --- a/README.md +++ b/README.md @@ -257,7 +257,7 @@ def render_page(page_title: str) -> str: ) ) ``` -This greatly reduces the amount of work `render` needs to do on the content when outputting HTML. +This greatly reduces the amount of work `render` needs to do on the prerendered content when outputting HTML. #### Caching You may want to cache rendered content. This is easy to do in many ways; the main thing to keep in From 09b7bccf24bf71aeaaffe620cb74bf327a8c1f8f Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sun, 26 Oct 2025 13:38:23 -0700 Subject: [PATCH 45/52] wip --- README.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 65d0606..c2367f9 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ This greatly reduces the amount of work `render` needs to do on the prerendered #### Caching You may want to cache rendered content. This is easy to do in many ways; the main thing to keep in -mind is you'll likely want to return a `SafeString`. Again, `prerender` is a natural fit: +mind is you'll likely want to return a `SafeString`. For example, here's how you might cache locally with `lru_cache`: ```python from simple_html import prerender, SafeString, h1 @@ -272,4 +272,36 @@ def greeting(name: str) -> SafeString: return prerender( h1(f"Hello, {name}") ) -``` \ No newline at end of file +``` + +One thing to keep in mind is that not all variants of `Node` will work as _arguments_ to a function like the +one above -- i.e. `list[Node]` is not cacheable. Another way to use `prerender` in combination with a caching function +is to prerender arguments: + +```python +from simple_html import prerender, SafeString, h1, div, html, body, head, ul, li +from functools import lru_cache + +@lru_cache +def cached_content(children: SafeString) -> SafeString: + return prerender( + div( + h1("This content is cached according to the content of the children"), + children, + # presumably this function would have a lot more elements for it to be worth + # the caching overhead + ) + ) + +def page(): + return html( + head, + body( + cached_content( + prerender(ul([ + li(letter) for letter in "abcdefg" + ])) + ) + ) + ) +``` From 378f88a69d0934603b2af1c7a88165e4f006ce42 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sun, 26 Oct 2025 13:41:08 -0700 Subject: [PATCH 46/52] wip --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c2367f9..98b6c5f 100644 --- a/README.md +++ b/README.md @@ -230,9 +230,9 @@ render(node) #### `prerender` -`prerender` is a very simple function. It simply `render`s a `Node` and encapsulates the resulting string inside -a `SafeString` (so its contents won't be escaped again). It's most useful at the top level, so its contents are -rendered only once. A simple use case might be website footers: +`prerender` is a very simple function. It just `render`s a `Node` and puts the resulting string inside +a `SafeString` (so its contents won't be escaped again). It's most useful for prerendering at the module level, +which ensures the render operation happens only once. A simple use case might be a website's footer: ```python from simple_html import SafeString, prerender, footer, div, a, head, body, title, h1, html, render From 4804bdca87ce59a44737cb633611ac8306632047 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sun, 26 Oct 2025 13:42:36 -0700 Subject: [PATCH 47/52] spacing --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 98b6c5f..c4ca0b4 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ ```python from simple_html import h1, render + node = h1("Hello World!") render(node) @@ -36,6 +37,7 @@ Here's a fuller-featured example: ```python from simple_html import render, DOCTYPE_HTML5, html, head, title, body, h1, div, p, br, ul, li + render( DOCTYPE_HTML5, html( @@ -79,6 +81,7 @@ As you might have noticed, there are several ways to use `Tag`s: ```python from simple_html import br, div, h1, img, span, render + # raw node renders to empty tag render(br) #
@@ -106,6 +109,7 @@ escaped by default; `SafeString`s can be used to bypass escaping. ```python from simple_html import br, p, SafeString, render + node = p("Escaped & stuff", br, SafeString("Not escaped & stuff")) @@ -121,6 +125,7 @@ that Tag attributes with `None` as the value will only render the attribute name ```python from simple_html import div, render + node = div({"empty-str-attribute": "", "key-only-attr": None}) @@ -133,6 +138,7 @@ String attributes are escaped by default -- both keys and values. You can use `S ```python from simple_html import div, render, SafeString + render( div({"":""}) ) @@ -149,6 +155,7 @@ You can also use `int`, `float`, and `Decimal` instances for attribute values. from decimal import Decimal from simple_html import div, render, SafeString + render( div({"x": 1, "y": 2.3, "z": Decimal('3.45')}) ) @@ -161,6 +168,7 @@ You can render inline CSS styles with `render_styles`: ```python from simple_html import div, render, render_styles + styles = render_styles({"min-width": "25px"}) node = div({"style": styles}, "cool") @@ -184,6 +192,7 @@ You can pass many items as a `Tag`'s children using `*args`, lists or generators from typing import Generator from simple_html import div, render, Node, br, p + div( *["neat", br], p("cool") ) @@ -214,6 +223,7 @@ For convenience, most common tags are provided, but you can also create your own ```python from simple_html import Tag, render + custom_elem = Tag("custom-elem") # works the same as any other tag @@ -237,6 +247,7 @@ which ensures the render operation happens only once. A simple use case might be ```python from simple_html import SafeString, prerender, footer, div, a, head, body, title, h1, html, render + prerendered_footer: SafeString = prerender( footer( div(a({"href": "/about"}, "About Us")), @@ -267,6 +278,7 @@ mind is you'll likely want to return a `SafeString`. For example, here's how you from simple_html import prerender, SafeString, h1 from functools import lru_cache + @lru_cache def greeting(name: str) -> SafeString: return prerender( @@ -282,6 +294,7 @@ is to prerender arguments: from simple_html import prerender, SafeString, h1, div, html, body, head, ul, li from functools import lru_cache + @lru_cache def cached_content(children: SafeString) -> SafeString: return prerender( From 0fc32867914a84f904de27576e6589f88eb8252a Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sun, 26 Oct 2025 13:45:50 -0700 Subject: [PATCH 48/52] punct --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4ca0b4..a4474af 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,7 @@ def render_page(page_title: str) -> str: This greatly reduces the amount of work `render` needs to do on the prerendered content when outputting HTML. #### Caching -You may want to cache rendered content. This is easy to do in many ways; the main thing to keep in +You may want to cache rendered content. This is easy to do; the main thing to keep in mind is you'll likely want to return a `SafeString`. For example, here's how you might cache locally with `lru_cache`: ```python From 8368d88d1541ebaf1042f1a277c39f4bc0114c47 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sun, 26 Oct 2025 13:53:57 -0700 Subject: [PATCH 49/52] wip --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a4474af..c83ee44 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ This greatly reduces the amount of work `render` needs to do on the prerendered #### Caching You may want to cache rendered content. This is easy to do; the main thing to keep in -mind is you'll likely want to return a `SafeString`. For example, here's how you might cache locally with `lru_cache`: +mind is you'll likely want to return a `SafeString`. For example, here's how you might cache with `lru_cache`: ```python from simple_html import prerender, SafeString, h1 @@ -286,9 +286,9 @@ def greeting(name: str) -> SafeString: ) ``` -One thing to keep in mind is that not all variants of `Node` will work as _arguments_ to a function like the -one above -- i.e. `list[Node]` is not cacheable. Another way to use `prerender` in combination with a caching function -is to prerender arguments: +One thing to remember is that not all variants of `Node` are hashable, and thus will work as _arguments_ to a function +where the arguments constitute the cache key -- i.e. `list[Node]` is not hashable. Another way to use `prerender` +in combination with a caching function is to prerender arguments: ```python from simple_html import prerender, SafeString, h1, div, html, body, head, ul, li @@ -306,15 +306,18 @@ def cached_content(children: SafeString) -> SafeString: ) ) -def page(): +def page(words_to_render: list[str]): return html( head, body( cached_content( prerender(ul([ - li(letter) for letter in "abcdefg" + li(word) for word in words_to_render ])) ) ) ) ``` +Keep in mind that using `prerender` on dynamic content -- not at the module level -- still incurs all the overhead +of `render` each time that content is rendered, so, for this approach to make sense, the prerendered content should +be a small portion of the full content of the `cached_content` function. \ No newline at end of file From e42db30f58a0864f25176c100f8daee7cec24e4e Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sun, 26 Oct 2025 13:56:37 -0700 Subject: [PATCH 50/52] wip --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c83ee44..d630071 100644 --- a/README.md +++ b/README.md @@ -286,9 +286,9 @@ def greeting(name: str) -> SafeString: ) ``` -One thing to remember is that not all variants of `Node` are hashable, and thus will work as _arguments_ to a function -where the arguments constitute the cache key -- i.e. `list[Node]` is not hashable. Another way to use `prerender` -in combination with a caching function is to prerender arguments: +One thing to remember is that not all variants of `Node` are hashable, and thus cannot be passed directly to a function +where the arguments constitute the cache key -- e.g. lists and generators are not hashable, but they can be +valid `Node`s. Another way to use `prerender` in combination with a caching function is to prerender arguments: ```python from simple_html import prerender, SafeString, h1, div, html, body, head, ul, li From fa5815e63e294b80ba31c934f620d28d0d5ce861 Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sun, 26 Oct 2025 15:00:52 -0700 Subject: [PATCH 51/52] wip --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d630071..aafda70 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Why use it? - clean syntax - fully-typed -- speed -- faster even than jinja +- speed -- often faster than jinja - zero dependencies - escaped by default - usually renders fewer bytes than templating From 686fab8513ede2fcddc3259d722e88733d533cda Mon Sep 17 00:00:00 2001 From: Keith Philpott Date: Sun, 26 Oct 2025 15:07:56 -0700 Subject: [PATCH 52/52] wip --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ba4d00a..9858ff3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,7 +76,7 @@ warn_unused_ignores = true warn_unreachable = true [tool.cibuildwheel] -skip = ["cp38-*", "cp314*"] +skip = ["cp38-*", "cp315*"] [tool.cibuildwheel.windows] archs = ["AMD64", "x86"]