Skip to content

Commit 1548c6d

Browse files
authored
compilation (#21)
* wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * 39 * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * fix perf
1 parent b5ea3ae commit 1548c6d

7 files changed

Lines changed: 121 additions & 21 deletions

File tree

.github/workflows/push.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ jobs:
2323
run: poetry install
2424
- name: mypy
2525
run: poetry run mypy simple_html
26+
- name: run bench (pure python)
27+
run: poetry run python -m bench.run
28+
- name: mypyc
29+
run: poetry run mypyc simple_html/utils.py
2630
- name: run tests
2731
run: poetry run pytest
28-
- name: run bench (pure python)
32+
- name: run bench (compiled)
2933
run: poetry run python -m bench.run
3034
- name: linting
3135
run: poetry run ruff check

.github/workflows/release.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: build and release
2+
3+
on: [push]
4+
5+
jobs:
6+
build_wheels:
7+
name: Build wheels on ${{ matrix.os }}
8+
runs-on: ${{ matrix.os }}
9+
strategy:
10+
matrix:
11+
# macos-13 is an intel runner, macos-14 is apple silicon
12+
os: [ ubuntu-latest, ubuntu-24.04-arm, windows-latest, macos-13, macos-14 ]
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Build wheels
17+
uses: pypa/cibuildwheel@v3.1.3
18+
19+
- uses: actions/upload-artifact@v4
20+
with:
21+
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
22+
path: ./wheelhouse/*.whl
23+
24+
build_sdist:
25+
name: Make SDist
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v4
29+
with:
30+
fetch-depth: 0 # Optional, use if you use setuptools_scm
31+
32+
- name: Build SDist
33+
run: pipx run build --sdist
34+
35+
- uses: actions/upload-artifact@v4
36+
with:
37+
name: cibw-sdist
38+
path: dist/*.tar.gz
39+
40+
upload_all:
41+
needs: [build_sdist, build_wheels]
42+
environment: pypi
43+
permissions:
44+
id-token: write
45+
runs-on: ubuntu-latest
46+
if: startsWith(github.ref, 'refs/tags/')
47+
steps:
48+
- uses: actions/download-artifact@v4
49+
with:
50+
pattern: cibw-*
51+
path: dist
52+
merge-multiple: true
53+
54+
- uses: pypa/gh-action-pypi-publish@release/v1

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Why use it?
44
- clean syntax
55
- fully-typed
6-
- speed -- faster even than jinja2
6+
- speed -- faster even than jinja
77
- zero dependencies
88
- escaped by default
99
- usually renders fewer bytes than templating
@@ -100,7 +100,7 @@ render(
100100
# <div><h1 class="neat-class"><span>cool</span><br/></h1></div>
101101
```
102102
### Strings and Things
103-
Strings, ints, floats, and Decimals are generally rendered as one would expect expect. For security, `str`s are
103+
Strings, ints, floats, and Decimals are generally rendered as one would expect expect. For safety, `str`s are
104104
escaped by default; `SafeString`s can be used to bypass escaping.
105105

106106
```python
@@ -128,7 +128,7 @@ render(node)
128128
# <div empty-str-attribute="" key-only-attr></div>
129129
```
130130

131-
Attributes are escaped by default -- both keys and values. You can use `SafeString` to bypass, if needed.
131+
String attributes are escaped by default -- both keys and values. You can use `SafeString` to bypass, if needed.
132132

133133
```python
134134
from simple_html import div, render, SafeString
@@ -144,6 +144,17 @@ render(
144144
# <div <bad>="</also bad>"></div>
145145
```
146146

147+
You can also use `int`, `float`, and `Decimal` instances for attribute values.
148+
```python
149+
from decimal import Decimal
150+
from simple_html import div, render, SafeString
151+
152+
render(
153+
div({"x": 1, "y": 2.3, "z": Decimal('3.45')})
154+
)
155+
# <div x="1" y="2.3" z="3.45"></div>
156+
```
157+
147158
### CSS
148159

149160
You can render inline CSS styles with `render_styles`:
@@ -168,7 +179,7 @@ render(node)
168179
```
169180

170181
### Collections
171-
You can pass many items as a `Tag`'s children using `*args`, lists and generators:
182+
You can pass many items as a `Tag`'s children using `*args`, lists or generators:
172183
```python
173184
from typing import Generator
174185
from simple_html import div, render, Node, br, p
@@ -178,7 +189,8 @@ div(
178189
)
179190
# renders to <div>neat<br/><p>cool</p></div>
180191

181-
# same, but no star args
192+
193+
# passing the raw list instead of *args
182194
div(
183195
["neat", br],
184196
p("cool")

pyproject.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ ruff = "0.12.8"
3737

3838

3939
[build-system]
40-
requires = ["poetry-core>=1.0.0"]
41-
build-backend = "poetry.core.masonry.api"
40+
requires = [
41+
"poetry-core>=1.0.0",
42+
"mypy[mypyc]==1.17.1",
43+
]
4244

4345
[tool.mypy]
4446
allow_redefinition = false
@@ -65,3 +67,9 @@ warn_return_any = true
6567
warn_unused_configs = true
6668
warn_unused_ignores = true
6769
warn_unreachable = true
70+
71+
[tool.cibuildwheel]
72+
skip = ["cp38-*", "cp314*"]
73+
74+
[tool.cibuildwheel.windows]
75+
archs = ["AMD64", "x86"]

setup.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from setuptools import setup
2+
from mypyc.build import mypycify
3+
4+
ext_modules = mypycify([
5+
"simple_html/utils.py",
6+
])
7+
8+
setup(
9+
name="simple_html",
10+
ext_modules=ext_modules,
11+
packages=["simple_html"],
12+
python_requires=">=3.9",
13+
)

simple_html/utils.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from decimal import Decimal
22
from types import GeneratorType
3-
from typing import Any, Union, Generator, Iterable, Callable, Final
3+
from typing import Any, Union, Generator, Iterable, Callable, Final, TYPE_CHECKING
44

55

66
class SafeString:
@@ -128,38 +128,43 @@ def __init__(self, name: str, self_closing: bool = False) -> None:
128128

129129
def __call__(
130130
self,
131-
attrs_or_first_child: Union[dict[Union[SafeString, str], Union[str, SafeString, None]], Node],
131+
attrs_or_first_child: Union[dict[Union[SafeString, str], Union[str, SafeString, int, float, Decimal, None]], Node],
132132
*children: Node,
133133
) -> Union[TagTuple, SafeString]:
134134
if isinstance(attrs_or_first_child, dict):
135135
# in this case this tends to be faster than attrs = "".join([...])
136-
tag_start_with_attrs = self.tag_start
136+
attrs: list[str] = []
137137
for key in attrs_or_first_child:
138138
# seems to be faster than using .items()
139-
val: Union[str, SafeString, None] = attrs_or_first_child[key]
139+
val: Union[str, SafeString, int, float, Decimal, None] = attrs_or_first_child[key]
140140

141141
# optimization: a large portion of attribute keys should be
142142
# covered by this check. It allows us to skip escaping
143143
# where it is not needed. Note this is for attribute names only;
144144
# attributes values are always escaped (when they are `str`s)
145+
# key_: str
145146
if key not in _common_safe_attribute_names:
146147
key = (
147148
escape_attribute_key(key)
148149
if isinstance(key, str)
149150
else key.safe_str
150151
)
152+
elif TYPE_CHECKING:
153+
assert isinstance(key, str)
151154

152155
if type(val) is str:
153-
tag_start_with_attrs += f' {key}="{faster_escape(val)}"'
156+
attrs.append(f' {key}="{faster_escape(val)}"')
154157
elif type(val) is SafeString:
155-
tag_start_with_attrs += f' {key}="{val.safe_str}"'
158+
attrs.append(f' {key}="{val.safe_str}"')
156159
elif val is None:
157-
tag_start_with_attrs += f" {key}"
160+
attrs.append(" " + key)
161+
elif isinstance(val, (int, float, Decimal)):
162+
attrs.append(f' {key}="{val}"')
158163

159164
if children:
160-
return f"{tag_start_with_attrs}>", children, self.closing_tag
165+
return self.tag_start + "".join(attrs) + ">", children, self.closing_tag
161166
else:
162-
return SafeString(f"{tag_start_with_attrs}{self.no_children_close}")
167+
return SafeString(self.tag_start + "".join(attrs) + self.no_children_close)
163168
else:
164169
return self.tag_start_no_attrs, (attrs_or_first_child,) + children, self.closing_tag
165170

@@ -407,7 +412,8 @@ def _render(nodes: Iterable[Node], append_to_list: Callable[[str], None]) -> Non
407412
def render_styles(
408413
styles: dict[Union[str, SafeString], Union[str, int, float, Decimal, SafeString]]
409414
) -> SafeString:
410-
ret = ""
415+
ret: list[str] = []
416+
app = ret.append
411417
for k, v in styles.items():
412418
if k not in _common_safe_css_props:
413419
if isinstance(k, SafeString):
@@ -421,9 +427,9 @@ def render_styles(
421427
v = faster_escape(v)
422428
# note that ints and floats pass through these condition checks
423429

424-
ret += f"{k}:{v};"
430+
app(f"{k}:{v};")
425431

426-
return SafeString(ret)
432+
return SafeString("".join(ret))
427433

428434

429435
def render(*nodes: Node) -> str:

tests/test_simple_html.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,4 +263,7 @@ def test_works_for_decimal() -> None:
263263
assert render(div({}, Decimal("-123.456"))) == "<div>-123.456</div>"
264264

265265
def test_tag_repr() -> None:
266-
assert repr(img) == "Tag(name='img', self_closing=True)"
266+
assert repr(img) == "Tag(name='img', self_closing=True)"
267+
268+
def test_render_number_attributes() -> None:
269+
assert render(div({"x": 1, "y": 2.01, "z": Decimal("3.02")})) == '<div x="1" y="2.01" z="3.02"></div>'

0 commit comments

Comments
 (0)