trmnl-liquid-py 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trmnl_liquid_py-0.1.0/CHANGELOG.md +34 -0
- trmnl_liquid_py-0.1.0/LICENSE +25 -0
- trmnl_liquid_py-0.1.0/PKG-INFO +190 -0
- trmnl_liquid_py-0.1.0/README.md +163 -0
- trmnl_liquid_py-0.1.0/pyproject.toml +78 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/__about__.py +3 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/__init__.py +6 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/control_flow.py +125 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/environment.py +90 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/filters.py +230 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/liquid_syntax.py +167 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/markdown.py +323 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/memory_system.py +97 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/py.typed +0 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/qr.py +346 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/ruby_coercion.py +53 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/ruby_values.py +149 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/template.py +266 -0
- trmnl_liquid_py-0.1.0/src/trmnl_liquid/template_tag.py +152 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## 0.1.0 - unreleased
|
|
6
|
+
|
|
7
|
+
Initial public release target for the Python port of `usetrmnl/trmnl-liquid` 0.8.2.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- TRMNL-compatible `Environment` and one-shot `render()` API.
|
|
12
|
+
- Inline `{% template %}` definitions backed by an in-memory template system.
|
|
13
|
+
- TRMNL filter surface for the supported non-I18n 0.8.2 scope.
|
|
14
|
+
- Ruby Liquid lax filter argument syntax used by TRMNL templates.
|
|
15
|
+
- Redcarpet-compatible Markdown rendering implemented on Mistune public APIs.
|
|
16
|
+
- RQRCode-compatible SVG output and QR mask selection using public `qrcode` APIs.
|
|
17
|
+
- Ruby-compatible scalar coercion for fallback filters.
|
|
18
|
+
- Ruby-compatible runtime error rendering and nested block continuation semantics.
|
|
19
|
+
- Typed package support through PEP 561 `py.typed` metadata.
|
|
20
|
+
|
|
21
|
+
### Compatibility
|
|
22
|
+
|
|
23
|
+
- Ruby/Python exact differential corpus: 574 cases with 0 known supported-scope mismatches.
|
|
24
|
+
- All 73 official TRMNL Liquid 0.8.2 RSpec examples classified and tracked:
|
|
25
|
+
- 46 exact differential examples,
|
|
26
|
+
- 16 property/API contract examples,
|
|
27
|
+
- 11 Rails/I18n examples explicitly deferred.
|
|
28
|
+
- CI coverage for Python 3.11, 3.12, 3.13, and 3.14.
|
|
29
|
+
|
|
30
|
+
### Deferred
|
|
31
|
+
|
|
32
|
+
- Rails/ActionView integration.
|
|
33
|
+
- Full I18n/localized `l_word` and `l_date` behavior.
|
|
34
|
+
- Features introduced after upstream `trmnl-liquid` 0.8.2.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Portions derived from usetrmnl/trmnl-liquid:
|
|
4
|
+
Copyright (c) 2025 Rockwell Schrock
|
|
5
|
+
|
|
6
|
+
Python port:
|
|
7
|
+
Copyright (c) 2026 Tomasz Jagusz
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: trmnl-liquid-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TRMNL Liquid 0.8.2 compatibility layer for Python
|
|
5
|
+
Project-URL: Homepage, https://github.com/Misiu/trmnl-liquid-py
|
|
6
|
+
Project-URL: Repository, https://github.com/Misiu/trmnl-liquid-py
|
|
7
|
+
Project-URL: Issues, https://github.com/Misiu/trmnl-liquid-py/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Misiu/trmnl-liquid-py/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Misiu
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: home-assistant,liquid,templates,trmnl
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: mistune==3.3.4
|
|
23
|
+
Requires-Dist: python-dateutil==2.9.0.post0
|
|
24
|
+
Requires-Dist: python-liquid==2.3.1
|
|
25
|
+
Requires-Dist: qrcode==8.2
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# trmnl-liquid-py
|
|
29
|
+
|
|
30
|
+
A Python compatibility layer for [`usetrmnl/trmnl-liquid`](https://github.com/usetrmnl/trmnl-liquid), targeting byte-for-byte compatible rendering for the supported TRMNL Liquid 0.8.2 surface.
|
|
31
|
+
|
|
32
|
+
> [!NOTE]
|
|
33
|
+
> The first release target is `trmnl-liquid-py` **0.1.0**, compatible with `trmnl-liquid` **0.8.2** for the supported non-I18n surface. Rails/ActionView and full I18n behavior are intentionally deferred.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
Once 0.1.0 is published to PyPI:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
python -m pip install trmnl-liquid-py
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Python **3.11–3.14** is supported.
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from trmnl_liquid import Environment
|
|
49
|
+
|
|
50
|
+
env = Environment()
|
|
51
|
+
template = env.from_string("Hello {{ name }}!")
|
|
52
|
+
print(template.render(name="TRMNL"))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For one-shot rendering:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from trmnl_liquid import render
|
|
59
|
+
|
|
60
|
+
html = render("{{ value | number_with_delimiter }}", value=1234567)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Inline templates
|
|
64
|
+
|
|
65
|
+
TRMNL adds an inline `{% template %}` definition that can later be rendered with Liquid's `{% render %}` tag:
|
|
66
|
+
|
|
67
|
+
```liquid
|
|
68
|
+
{% template card %}
|
|
69
|
+
<div>{{ title }}</div>
|
|
70
|
+
{% endtemplate %}
|
|
71
|
+
|
|
72
|
+
{% render 'card', title: 'Status' %}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`trmnl-liquid-py` reproduces TRMNL 0.8.2's raw-body and template-storage semantics, including its exact `{% endtemplate %}` terminator behavior.
|
|
76
|
+
|
|
77
|
+
## TRMNL filters
|
|
78
|
+
|
|
79
|
+
The 0.1.0 compatibility surface includes:
|
|
80
|
+
|
|
81
|
+
- `append_random`
|
|
82
|
+
- `days_ago`
|
|
83
|
+
- `group_by`
|
|
84
|
+
- `find_by`
|
|
85
|
+
- `markdown_to_html`
|
|
86
|
+
- `number_with_delimiter`
|
|
87
|
+
- `number_to_currency`
|
|
88
|
+
- `l_word` fallback behavior
|
|
89
|
+
- `l_date` fallback behavior
|
|
90
|
+
- `map_to_i`
|
|
91
|
+
- `pluralize`
|
|
92
|
+
- `json`
|
|
93
|
+
- `parse_json`
|
|
94
|
+
- `sample`
|
|
95
|
+
- `where_exp`
|
|
96
|
+
- `ordinalize`
|
|
97
|
+
- `qr_code`
|
|
98
|
+
|
|
99
|
+
Important TRMNL/Ruby syntax differences are preserved where required. For example, Ruby Liquid's lax filter syntax accepts a leading comma before the first filter argument:
|
|
100
|
+
|
|
101
|
+
```liquid
|
|
102
|
+
{{ value | qr_code, 11 }}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Markdown
|
|
106
|
+
|
|
107
|
+
`markdown_to_html` reproduces the tested output of TRMNL 0.8.2's default Redcarpet 3.6.1 configuration using Mistune on Python.
|
|
108
|
+
|
|
109
|
+
```liquid
|
|
110
|
+
{{ markdown | markdown_to_html }}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### QR codes
|
|
114
|
+
|
|
115
|
+
`qr_code` renders the SVG shape expected from TRMNL's RQRCode-based implementation, including compatible QR mask selection/scoring behavior.
|
|
116
|
+
|
|
117
|
+
```liquid
|
|
118
|
+
{{ 'https://example.com' | qr_code: 11 }}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Compatibility target
|
|
122
|
+
|
|
123
|
+
The baseline is exactly:
|
|
124
|
+
|
|
125
|
+
- `trmnl-liquid` **0.8.2**
|
|
126
|
+
- Python **3.11, 3.12, 3.13, 3.14**
|
|
127
|
+
- non-I18n behavior
|
|
128
|
+
- Rails/ActionView and full localized `l_word` / `l_date` behavior deferred
|
|
129
|
+
- features added after upstream 0.8.2 excluded until a later compatibility target is adopted
|
|
130
|
+
|
|
131
|
+
Ruby `trmnl-liquid` 0.8.2 is the reference implementation. Differences in parsing, coercion, formatting, Markdown rendering, QR generation, template behavior, error handling, or rendered output are treated as compatibility issues when they affect the supported scope.
|
|
132
|
+
|
|
133
|
+
<!-- compatibility-report:start -->
|
|
134
|
+
### Compatibility evidence
|
|
135
|
+
|
|
136
|
+
| Gate | Result |
|
|
137
|
+
| --- | ---: |
|
|
138
|
+
| Ruby 0.8.2 vs Python exact differential corpus | **574/574** |
|
|
139
|
+
| Known mismatches in the supported scope | **0** |
|
|
140
|
+
| Official TRMNL 0.8.2 RSpec examples mapped | **73/73** |
|
|
141
|
+
| Upstream examples covered by exact differential tests | **46** |
|
|
142
|
+
| Upstream examples covered by property/unit tests | **16** |
|
|
143
|
+
| Upstream examples covered in the initial non-I18n scope | **62/73** |
|
|
144
|
+
| Deferred Rails/I18n upstream examples | **11** |
|
|
145
|
+
| Python CI matrix | **3.11, 3.12, 3.13, 3.14** |
|
|
146
|
+
| Comparison | **exact rendered output** |
|
|
147
|
+
|
|
148
|
+
The `Compatibility` workflow runs the Ruby oracle first and fails on any mismatch. It then checks that this generated report still matches the checked-in corpus and upstream-spec manifest.
|
|
149
|
+
<!-- compatibility-report:end -->
|
|
150
|
+
|
|
151
|
+
See [`COMPATIBILITY.md`](COMPATIBILITY.md) for the compatibility contract, upstream spec mapping, and Ruby oracle details.
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
Install the project with development dependencies, then run the normal quality gates:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
python -m pip install -e . pytest ruff mypy build twine \
|
|
159
|
+
types-python-dateutil==2.9.0.20260807 \
|
|
160
|
+
types-qrcode==8.2.0.20260518
|
|
161
|
+
ruff check .
|
|
162
|
+
mypy src/trmnl_liquid
|
|
163
|
+
pytest
|
|
164
|
+
python -m build
|
|
165
|
+
twine check dist/*
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The Ruby/Python differential suite additionally requires Ruby and the bundle in `compatibility/ruby`:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
bundle install --gemfile compatibility/ruby/Gemfile
|
|
172
|
+
python compatibility/compare.py
|
|
173
|
+
python -m compatibility.report --check
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Release process
|
|
177
|
+
|
|
178
|
+
Releases are tag-driven. There is no manual publish workflow and no PyPI API token stored in GitHub.
|
|
179
|
+
|
|
180
|
+
1. Update `src/trmnl_liquid/__about__.py` and `CHANGELOG.md` in a normal PR.
|
|
181
|
+
2. Merge the PR to `main` and wait for both `CI` and `Compatibility` to pass on `main`.
|
|
182
|
+
3. Create and push a tag whose name is exactly the package version, for example `0.1.0`.
|
|
183
|
+
4. The `Release` workflow verifies that the tag equals `__version__` and points to a commit on `main`.
|
|
184
|
+
5. The workflow builds and validates the wheel/sdist, publishes them to PyPI through Trusted Publishing, and only after a successful PyPI publish creates the matching GitHub Release with the distributions attached.
|
|
185
|
+
|
|
186
|
+
A tag such as `v0.1.0` is intentionally invalid when `__version__ == "0.1.0"`.
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT. The repository license includes attribution for portions derived from `usetrmnl/trmnl-liquid`.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# trmnl-liquid-py
|
|
2
|
+
|
|
3
|
+
A Python compatibility layer for [`usetrmnl/trmnl-liquid`](https://github.com/usetrmnl/trmnl-liquid), targeting byte-for-byte compatible rendering for the supported TRMNL Liquid 0.8.2 surface.
|
|
4
|
+
|
|
5
|
+
> [!NOTE]
|
|
6
|
+
> The first release target is `trmnl-liquid-py` **0.1.0**, compatible with `trmnl-liquid` **0.8.2** for the supported non-I18n surface. Rails/ActionView and full I18n behavior are intentionally deferred.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
Once 0.1.0 is published to PyPI:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
python -m pip install trmnl-liquid-py
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Python **3.11–3.14** is supported.
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from trmnl_liquid import Environment
|
|
22
|
+
|
|
23
|
+
env = Environment()
|
|
24
|
+
template = env.from_string("Hello {{ name }}!")
|
|
25
|
+
print(template.render(name="TRMNL"))
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For one-shot rendering:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from trmnl_liquid import render
|
|
32
|
+
|
|
33
|
+
html = render("{{ value | number_with_delimiter }}", value=1234567)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Inline templates
|
|
37
|
+
|
|
38
|
+
TRMNL adds an inline `{% template %}` definition that can later be rendered with Liquid's `{% render %}` tag:
|
|
39
|
+
|
|
40
|
+
```liquid
|
|
41
|
+
{% template card %}
|
|
42
|
+
<div>{{ title }}</div>
|
|
43
|
+
{% endtemplate %}
|
|
44
|
+
|
|
45
|
+
{% render 'card', title: 'Status' %}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`trmnl-liquid-py` reproduces TRMNL 0.8.2's raw-body and template-storage semantics, including its exact `{% endtemplate %}` terminator behavior.
|
|
49
|
+
|
|
50
|
+
## TRMNL filters
|
|
51
|
+
|
|
52
|
+
The 0.1.0 compatibility surface includes:
|
|
53
|
+
|
|
54
|
+
- `append_random`
|
|
55
|
+
- `days_ago`
|
|
56
|
+
- `group_by`
|
|
57
|
+
- `find_by`
|
|
58
|
+
- `markdown_to_html`
|
|
59
|
+
- `number_with_delimiter`
|
|
60
|
+
- `number_to_currency`
|
|
61
|
+
- `l_word` fallback behavior
|
|
62
|
+
- `l_date` fallback behavior
|
|
63
|
+
- `map_to_i`
|
|
64
|
+
- `pluralize`
|
|
65
|
+
- `json`
|
|
66
|
+
- `parse_json`
|
|
67
|
+
- `sample`
|
|
68
|
+
- `where_exp`
|
|
69
|
+
- `ordinalize`
|
|
70
|
+
- `qr_code`
|
|
71
|
+
|
|
72
|
+
Important TRMNL/Ruby syntax differences are preserved where required. For example, Ruby Liquid's lax filter syntax accepts a leading comma before the first filter argument:
|
|
73
|
+
|
|
74
|
+
```liquid
|
|
75
|
+
{{ value | qr_code, 11 }}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Markdown
|
|
79
|
+
|
|
80
|
+
`markdown_to_html` reproduces the tested output of TRMNL 0.8.2's default Redcarpet 3.6.1 configuration using Mistune on Python.
|
|
81
|
+
|
|
82
|
+
```liquid
|
|
83
|
+
{{ markdown | markdown_to_html }}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### QR codes
|
|
87
|
+
|
|
88
|
+
`qr_code` renders the SVG shape expected from TRMNL's RQRCode-based implementation, including compatible QR mask selection/scoring behavior.
|
|
89
|
+
|
|
90
|
+
```liquid
|
|
91
|
+
{{ 'https://example.com' | qr_code: 11 }}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Compatibility target
|
|
95
|
+
|
|
96
|
+
The baseline is exactly:
|
|
97
|
+
|
|
98
|
+
- `trmnl-liquid` **0.8.2**
|
|
99
|
+
- Python **3.11, 3.12, 3.13, 3.14**
|
|
100
|
+
- non-I18n behavior
|
|
101
|
+
- Rails/ActionView and full localized `l_word` / `l_date` behavior deferred
|
|
102
|
+
- features added after upstream 0.8.2 excluded until a later compatibility target is adopted
|
|
103
|
+
|
|
104
|
+
Ruby `trmnl-liquid` 0.8.2 is the reference implementation. Differences in parsing, coercion, formatting, Markdown rendering, QR generation, template behavior, error handling, or rendered output are treated as compatibility issues when they affect the supported scope.
|
|
105
|
+
|
|
106
|
+
<!-- compatibility-report:start -->
|
|
107
|
+
### Compatibility evidence
|
|
108
|
+
|
|
109
|
+
| Gate | Result |
|
|
110
|
+
| --- | ---: |
|
|
111
|
+
| Ruby 0.8.2 vs Python exact differential corpus | **574/574** |
|
|
112
|
+
| Known mismatches in the supported scope | **0** |
|
|
113
|
+
| Official TRMNL 0.8.2 RSpec examples mapped | **73/73** |
|
|
114
|
+
| Upstream examples covered by exact differential tests | **46** |
|
|
115
|
+
| Upstream examples covered by property/unit tests | **16** |
|
|
116
|
+
| Upstream examples covered in the initial non-I18n scope | **62/73** |
|
|
117
|
+
| Deferred Rails/I18n upstream examples | **11** |
|
|
118
|
+
| Python CI matrix | **3.11, 3.12, 3.13, 3.14** |
|
|
119
|
+
| Comparison | **exact rendered output** |
|
|
120
|
+
|
|
121
|
+
The `Compatibility` workflow runs the Ruby oracle first and fails on any mismatch. It then checks that this generated report still matches the checked-in corpus and upstream-spec manifest.
|
|
122
|
+
<!-- compatibility-report:end -->
|
|
123
|
+
|
|
124
|
+
See [`COMPATIBILITY.md`](COMPATIBILITY.md) for the compatibility contract, upstream spec mapping, and Ruby oracle details.
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
Install the project with development dependencies, then run the normal quality gates:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
python -m pip install -e . pytest ruff mypy build twine \
|
|
132
|
+
types-python-dateutil==2.9.0.20260807 \
|
|
133
|
+
types-qrcode==8.2.0.20260518
|
|
134
|
+
ruff check .
|
|
135
|
+
mypy src/trmnl_liquid
|
|
136
|
+
pytest
|
|
137
|
+
python -m build
|
|
138
|
+
twine check dist/*
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The Ruby/Python differential suite additionally requires Ruby and the bundle in `compatibility/ruby`:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
bundle install --gemfile compatibility/ruby/Gemfile
|
|
145
|
+
python compatibility/compare.py
|
|
146
|
+
python -m compatibility.report --check
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Release process
|
|
150
|
+
|
|
151
|
+
Releases are tag-driven. There is no manual publish workflow and no PyPI API token stored in GitHub.
|
|
152
|
+
|
|
153
|
+
1. Update `src/trmnl_liquid/__about__.py` and `CHANGELOG.md` in a normal PR.
|
|
154
|
+
2. Merge the PR to `main` and wait for both `CI` and `Compatibility` to pass on `main`.
|
|
155
|
+
3. Create and push a tag whose name is exactly the package version, for example `0.1.0`.
|
|
156
|
+
4. The `Release` workflow verifies that the tag equals `__version__` and points to a commit on `main`.
|
|
157
|
+
5. The workflow builds and validates the wheel/sdist, publishes them to PyPI through Trusted Publishing, and only after a successful PyPI publish creates the matching GitHub Release with the distributions attached.
|
|
158
|
+
|
|
159
|
+
A tag such as `v0.1.0` is intentionally invalid when `__version__ == "0.1.0"`.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT. The repository license includes attribution for portions derived from `usetrmnl/trmnl-liquid`.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "trmnl-liquid-py"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "TRMNL Liquid 0.8.2 compatibility layer for Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Misiu" }]
|
|
13
|
+
keywords = ["trmnl", "liquid", "templates", "home-assistant"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Programming Language :: Python :: 3.14",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"mistune==3.3.4",
|
|
26
|
+
"python-dateutil==2.9.0.post0",
|
|
27
|
+
"python-liquid==2.3.1",
|
|
28
|
+
"qrcode==8.2",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/Misiu/trmnl-liquid-py"
|
|
33
|
+
Repository = "https://github.com/Misiu/trmnl-liquid-py"
|
|
34
|
+
Issues = "https://github.com/Misiu/trmnl-liquid-py/issues"
|
|
35
|
+
Changelog = "https://github.com/Misiu/trmnl-liquid-py/blob/main/CHANGELOG.md"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.version]
|
|
38
|
+
path = "src/trmnl_liquid/__about__.py"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/trmnl_liquid"]
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.sdist]
|
|
44
|
+
include = [
|
|
45
|
+
"src/trmnl_liquid",
|
|
46
|
+
"README.md",
|
|
47
|
+
"CHANGELOG.md",
|
|
48
|
+
"LICENSE",
|
|
49
|
+
"pyproject.toml",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
pythonpath = ["."]
|
|
55
|
+
addopts = "-ra --strict-config --strict-markers"
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
target-version = "py311"
|
|
59
|
+
line-length = 100
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
|
|
63
|
+
|
|
64
|
+
[tool.mypy]
|
|
65
|
+
python_version = "3.11"
|
|
66
|
+
strict = true
|
|
67
|
+
packages = ["trmnl_liquid"]
|
|
68
|
+
|
|
69
|
+
[dependency-groups]
|
|
70
|
+
dev = [
|
|
71
|
+
"build>=1.3",
|
|
72
|
+
"mypy>=1.17",
|
|
73
|
+
"pytest>=8.4",
|
|
74
|
+
"ruff>=0.12",
|
|
75
|
+
"twine>=6.1",
|
|
76
|
+
"types-python-dateutil==2.9.0.20260807",
|
|
77
|
+
"types-qrcode==8.2.0.20260518",
|
|
78
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Ruby-compatible nested control-flow nodes for python-liquid.
|
|
2
|
+
|
|
3
|
+
Ruby Liquid rescues runtime errors per child node in every ``BlockBody``. We keep
|
|
4
|
+
python-liquid's public ``IfTag`` and ``ForTag`` parsers and adapt only their parsed
|
|
5
|
+
child blocks to :class:`TRMNLBlockNode`.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
- Ruby Liquid 5.13 BlockBody rendering:
|
|
9
|
+
https://github.com/Shopify/liquid/blob/v5.13.0/lib/liquid/block_body.rb
|
|
10
|
+
- python-liquid 2.3.1 IfTag/IfNode:
|
|
11
|
+
https://github.com/jg-rp/liquid/blob/v2.3.1/liquid/builtin/tags/if_tag.py
|
|
12
|
+
- python-liquid 2.3.1 ForTag/ForNode:
|
|
13
|
+
https://github.com/jg-rp/liquid/blob/v2.3.1/liquid/builtin/tags/for_tag.py
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
from liquid.ast import BlockNode, ConditionalBlockNode, Node
|
|
21
|
+
from liquid.builtin.expressions import LoopExpression
|
|
22
|
+
from liquid.builtin.tags.for_tag import ForNode, ForTag
|
|
23
|
+
from liquid.builtin.tags.if_tag import IfNode, IfTag
|
|
24
|
+
from liquid.expression import Expression
|
|
25
|
+
from liquid.token import Token
|
|
26
|
+
|
|
27
|
+
from .template import TRMNLBlockNode
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from liquid.stream import TokenStream
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _block(block: BlockNode) -> TRMNLBlockNode:
|
|
34
|
+
return TRMNLBlockNode(block.token, block.nodes)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _optional_block(block: BlockNode | None) -> TRMNLBlockNode | None:
|
|
38
|
+
return _block(block) if block is not None else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TRMNLIfNode(IfNode):
|
|
42
|
+
"""IfNode whose branch bodies continue after individual runtime errors."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
token: Token,
|
|
47
|
+
condition: Expression,
|
|
48
|
+
consequence: BlockNode,
|
|
49
|
+
alternatives: list[ConditionalBlockNode],
|
|
50
|
+
default: BlockNode | None,
|
|
51
|
+
) -> None:
|
|
52
|
+
wrapped_alternatives = [
|
|
53
|
+
ConditionalBlockNode(
|
|
54
|
+
alternative.token,
|
|
55
|
+
expression=alternative.expression,
|
|
56
|
+
block=_block(alternative.block),
|
|
57
|
+
)
|
|
58
|
+
for alternative in alternatives
|
|
59
|
+
]
|
|
60
|
+
super().__init__(
|
|
61
|
+
token=token,
|
|
62
|
+
condition=condition,
|
|
63
|
+
consequence=_block(consequence),
|
|
64
|
+
alternatives=wrapped_alternatives,
|
|
65
|
+
default=_optional_block(default),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_upstream(cls, node: IfNode) -> TRMNLIfNode:
|
|
70
|
+
"""Wrap an upstream IfNode without changing its parsed expressions."""
|
|
71
|
+
return cls(
|
|
72
|
+
token=node.token,
|
|
73
|
+
condition=node.condition,
|
|
74
|
+
consequence=node.consequence,
|
|
75
|
+
alternatives=node.alternatives,
|
|
76
|
+
default=node.default,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class TRMNLIfTag(IfTag):
|
|
81
|
+
"""Built-in ``if`` parser with TRMNL's nested block runtime semantics."""
|
|
82
|
+
|
|
83
|
+
def parse(self, stream: TokenStream) -> Node:
|
|
84
|
+
node = super().parse(stream)
|
|
85
|
+
if not isinstance(node, IfNode):
|
|
86
|
+
return node
|
|
87
|
+
return TRMNLIfNode.from_upstream(node)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TRMNLForNode(ForNode):
|
|
91
|
+
"""ForNode whose iteration body continues after individual runtime errors."""
|
|
92
|
+
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
token: Token,
|
|
96
|
+
expression: LoopExpression,
|
|
97
|
+
block: BlockNode,
|
|
98
|
+
default: BlockNode | None = None,
|
|
99
|
+
) -> None:
|
|
100
|
+
super().__init__(
|
|
101
|
+
token=token,
|
|
102
|
+
expression=expression,
|
|
103
|
+
block=_block(block),
|
|
104
|
+
default=_optional_block(default),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def from_upstream(cls, node: ForNode) -> TRMNLForNode:
|
|
109
|
+
"""Wrap an upstream ForNode without changing its parsed expression."""
|
|
110
|
+
return cls(
|
|
111
|
+
token=node.token,
|
|
112
|
+
expression=node.expression,
|
|
113
|
+
block=node.block,
|
|
114
|
+
default=node.default,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TRMNLForTag(ForTag):
|
|
119
|
+
"""Built-in ``for`` parser with TRMNL's nested block runtime semantics."""
|
|
120
|
+
|
|
121
|
+
def parse(self, stream: TokenStream) -> Node:
|
|
122
|
+
node = super().parse(stream)
|
|
123
|
+
if not isinstance(node, ForNode):
|
|
124
|
+
return node
|
|
125
|
+
return TRMNLForNode.from_upstream(node)
|