chameleon_partials 0.2.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.
- chameleon_partials-0.2.0/LICENSE +21 -0
- chameleon_partials-0.2.0/PKG-INFO +196 -0
- chameleon_partials-0.2.0/README.md +160 -0
- chameleon_partials-0.2.0/chameleon_partials/__init__.py +201 -0
- chameleon_partials-0.2.0/chameleon_partials/py.typed +0 -0
- chameleon_partials-0.2.0/chameleon_partials.egg-info/PKG-INFO +196 -0
- chameleon_partials-0.2.0/chameleon_partials.egg-info/SOURCES.txt +13 -0
- chameleon_partials-0.2.0/chameleon_partials.egg-info/dependency_links.txt +1 -0
- chameleon_partials-0.2.0/chameleon_partials.egg-info/requires.txt +10 -0
- chameleon_partials-0.2.0/chameleon_partials.egg-info/top_level.txt +1 -0
- chameleon_partials-0.2.0/pyproject.toml +82 -0
- chameleon_partials-0.2.0/requirements-dev.txt +9 -0
- chameleon_partials-0.2.0/requirements.txt +1 -0
- chameleon_partials-0.2.0/setup.cfg +4 -0
- chameleon_partials-0.2.0/tests/test_rendering.py +65 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Michael Kennedy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chameleon_partials
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Simple reuse of partial HTML page templates in the Chameleon template language for Python web frameworks.
|
|
5
|
+
Author-email: Michael Kennedy <michael@talkpython.fm>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mikeckennedy/chameleon_partials
|
|
8
|
+
Project-URL: Repository, https://github.com/mikeckennedy/chameleon_partials
|
|
9
|
+
Project-URL: Documentation, https://mkennedy.codes/docs/chameleon-partials/
|
|
10
|
+
Project-URL: Issues, https://github.com/mikeckennedy/chameleon_partials/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/mikeckennedy/chameleon_partials/blob/main/CHANGELOG.md
|
|
12
|
+
Project-URL: Funding, https://github.com/sponsors/mikeckennedy
|
|
13
|
+
Keywords: chameleon,partials,templates,templating,template-engine,html,rendering,render-partial,components,fragments,reusable,web,web-development,web-framework,pyramid,fastapi,htmx,frontend,jinja-partials,zpt,tal,metal
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: chameleon
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-clarity; extra == "dev"
|
|
32
|
+
Requires-Dist: ty; extra == "dev"
|
|
33
|
+
Requires-Dist: pyrefly; extra == "dev"
|
|
34
|
+
Requires-Dist: great-docs; python_version >= "3.11" and extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# Chameleon Partials
|
|
38
|
+
|
|
39
|
+
Simple reuse of partial HTML page templates in the Chameleon template language for Python web frameworks.
|
|
40
|
+
(There is also a [Jinja2/Flask version here](https://github.com/mikeckennedy/jinja_partials).)
|
|
41
|
+
|
|
42
|
+
## Overview
|
|
43
|
+
|
|
44
|
+
When building real-world web apps with Chameleon, it's easy to end up with repeated HTML fragments.
|
|
45
|
+
Just like organizing code for reuse, it would be ideal to reuse smaller sections of HTML template code.
|
|
46
|
+
That's what this library is all about.
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
Full documentation lives at
|
|
51
|
+
[mkennedy.codes/docs/chameleon-partials](https://mkennedy.codes/docs/chameleon-partials/),
|
|
52
|
+
including the complete [API reference](https://mkennedy.codes/docs/chameleon-partials/reference/)
|
|
53
|
+
generated from the library's docstrings.
|
|
54
|
+
|
|
55
|
+
## Example
|
|
56
|
+
|
|
57
|
+
This project comes with a sample Pyramid application (see the `example` folder). This app displays videos
|
|
58
|
+
that can be played on YouTube. The image, author subtitle, and view count are reused throughout the
|
|
59
|
+
app. Here's a visual:
|
|
60
|
+
|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
Check out the [**demo / example application**](https://github.com/mikeckennedy/chameleon_partials/tree/main/example)
|
|
64
|
+
to see it in action.
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
It's just `pip install chameleon-partials` and you're all set with this pure Python package.
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
Using the library is incredibly easy. The first step is to register the partial method with Chameleon.
|
|
73
|
+
Do this once at app startup:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from pathlib import Path
|
|
77
|
+
|
|
78
|
+
from pyramid.config import Configurator
|
|
79
|
+
|
|
80
|
+
import chameleon_partials
|
|
81
|
+
|
|
82
|
+
def main(_, **settings):
|
|
83
|
+
""" This function returns a Pyramid WSGI application.
|
|
84
|
+
"""
|
|
85
|
+
with Configurator(settings=settings) as config:
|
|
86
|
+
config.include('pyramid_chameleon')
|
|
87
|
+
config.include('.routes')
|
|
88
|
+
config.scan()
|
|
89
|
+
|
|
90
|
+
# Register the extension for working with Chameleon.
|
|
91
|
+
folder = (Path(__file__).parent / "templates").as_posix()
|
|
92
|
+
chameleon_partials.register_extensions(folder, auto_reload=True, cache_init=True)
|
|
93
|
+
|
|
94
|
+
return config.make_wsgi_app()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Next, you define your main HTML (Chameleon) templates as usual. Then
|
|
98
|
+
define your partial templates. I recommend locating and naming them accordingly:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
├── templates
|
|
102
|
+
│ ├── errors
|
|
103
|
+
│ │ └── 404.pt
|
|
104
|
+
│ ├── home
|
|
105
|
+
│ │ ├── index.pt
|
|
106
|
+
│ │ └── listing.pt
|
|
107
|
+
│ └── shared
|
|
108
|
+
│ ├── _layout.pt
|
|
109
|
+
│ └── partials
|
|
110
|
+
│ ├── video_image.pt
|
|
111
|
+
│ └── video_square.pt
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Notice the `partials` subfolder in the `templates/shared` folder.
|
|
115
|
+
|
|
116
|
+
The templates are just HTML fragments. Here is a stand-alone one for the YouTube thumbnail from
|
|
117
|
+
the example app:
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<img src="https://img.youtube.com/vi/${ video.id }/maxresdefault.jpg"
|
|
121
|
+
class="img img-responsive ${ ' '.join(classes or []) }"
|
|
122
|
+
alt="${ video.title }"
|
|
123
|
+
title="${ video.title }">
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Notice that an object called `video` and a list of classes are passed in as the model.
|
|
127
|
+
|
|
128
|
+
Templates can also be nested. Here is the whole single video fragment with the image as well as other info
|
|
129
|
+
linking out to YouTube:
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
<div>
|
|
133
|
+
<a href="https://www.youtube.com/watch?v=${ video.id }" target="_blank">
|
|
134
|
+
${ render_partial('shared/partials/video_image.pt', video=video, classes=[]) }
|
|
135
|
+
</a>
|
|
136
|
+
<a href="https://www.youtube.com/watch?v=${ video.id }" target="_blank"
|
|
137
|
+
class="author">${ video.author }</a>
|
|
138
|
+
<div class="views">${ "{:,}".format(video.views) } views</div>
|
|
139
|
+
</div>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Now you see the `render_partial()` method. It takes the subpath into the templates folder and
|
|
143
|
+
any model data passed in as keyword arguments.
|
|
144
|
+
|
|
145
|
+
We can finally generate the list of video blocks as follows:
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<span class="video" tal:repeat="v videos">
|
|
149
|
+
${ render_partial('shared/partials/video_square.pt', video=v) }
|
|
150
|
+
</span>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
This time, we reframe each item in the list from the outer template (called `v`) as the `video` model
|
|
154
|
+
in the inner HTML section.
|
|
155
|
+
|
|
156
|
+
## The View Methods
|
|
157
|
+
|
|
158
|
+
In order to share the `render_partial()` function with your template, you'll need to pass it along to the
|
|
159
|
+
template with your model (dictionary).
|
|
160
|
+
|
|
161
|
+
If you are using the **Pyramid web framework**, you can add this file as middleware. Just drop it into
|
|
162
|
+
your `views` folder:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# views/partials_middleware.py
|
|
166
|
+
from pyramid.events import subscriber, BeforeRender
|
|
167
|
+
|
|
168
|
+
import chameleon_partials
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@subscriber(BeforeRender)
|
|
172
|
+
def add_global(event):
|
|
173
|
+
event['render_partial'] = chameleon_partials.render_partial
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
For other frameworks using Chameleon (e.g. FastAPI), you can add `render_partial` to the
|
|
177
|
+
resulting dictionary. We've built a simple function to keep this fool-proof:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
chameleon_partials.extend_model(model)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Here's a typical view method that uses `render_partial`, notice the use of extending the
|
|
184
|
+
model before passing it to the template (the example app's views skip this because the
|
|
185
|
+
middleware above handles it):
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
@view_config(route_name='listing', renderer='demo_chameleon_partials:templates/home/listing.pt')
|
|
189
|
+
def listing(_):
|
|
190
|
+
videos = video_service.all_videos()
|
|
191
|
+
model = dict(videos=videos)
|
|
192
|
+
return chameleon_partials.extend_model(model)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Again: If you are using Pyramid, use the middleware. Otherwise, use the `extend_model()` method or something
|
|
196
|
+
similar to the middleware in your framework.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Chameleon Partials
|
|
2
|
+
|
|
3
|
+
Simple reuse of partial HTML page templates in the Chameleon template language for Python web frameworks.
|
|
4
|
+
(There is also a [Jinja2/Flask version here](https://github.com/mikeckennedy/jinja_partials).)
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
When building real-world web apps with Chameleon, it's easy to end up with repeated HTML fragments.
|
|
9
|
+
Just like organizing code for reuse, it would be ideal to reuse smaller sections of HTML template code.
|
|
10
|
+
That's what this library is all about.
|
|
11
|
+
|
|
12
|
+
## Documentation
|
|
13
|
+
|
|
14
|
+
Full documentation lives at
|
|
15
|
+
[mkennedy.codes/docs/chameleon-partials](https://mkennedy.codes/docs/chameleon-partials/),
|
|
16
|
+
including the complete [API reference](https://mkennedy.codes/docs/chameleon-partials/reference/)
|
|
17
|
+
generated from the library's docstrings.
|
|
18
|
+
|
|
19
|
+
## Example
|
|
20
|
+
|
|
21
|
+
This project comes with a sample Pyramid application (see the `example` folder). This app displays videos
|
|
22
|
+
that can be played on YouTube. The image, author subtitle, and view count are reused throughout the
|
|
23
|
+
app. Here's a visual:
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
Check out the [**demo / example application**](https://github.com/mikeckennedy/chameleon_partials/tree/main/example)
|
|
28
|
+
to see it in action.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
It's just `pip install chameleon-partials` and you're all set with this pure Python package.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
Using the library is incredibly easy. The first step is to register the partial method with Chameleon.
|
|
37
|
+
Do this once at app startup:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
|
|
42
|
+
from pyramid.config import Configurator
|
|
43
|
+
|
|
44
|
+
import chameleon_partials
|
|
45
|
+
|
|
46
|
+
def main(_, **settings):
|
|
47
|
+
""" This function returns a Pyramid WSGI application.
|
|
48
|
+
"""
|
|
49
|
+
with Configurator(settings=settings) as config:
|
|
50
|
+
config.include('pyramid_chameleon')
|
|
51
|
+
config.include('.routes')
|
|
52
|
+
config.scan()
|
|
53
|
+
|
|
54
|
+
# Register the extension for working with Chameleon.
|
|
55
|
+
folder = (Path(__file__).parent / "templates").as_posix()
|
|
56
|
+
chameleon_partials.register_extensions(folder, auto_reload=True, cache_init=True)
|
|
57
|
+
|
|
58
|
+
return config.make_wsgi_app()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Next, you define your main HTML (Chameleon) templates as usual. Then
|
|
62
|
+
define your partial templates. I recommend locating and naming them accordingly:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
├── templates
|
|
66
|
+
│ ├── errors
|
|
67
|
+
│ │ └── 404.pt
|
|
68
|
+
│ ├── home
|
|
69
|
+
│ │ ├── index.pt
|
|
70
|
+
│ │ └── listing.pt
|
|
71
|
+
│ └── shared
|
|
72
|
+
│ ├── _layout.pt
|
|
73
|
+
│ └── partials
|
|
74
|
+
│ ├── video_image.pt
|
|
75
|
+
│ └── video_square.pt
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Notice the `partials` subfolder in the `templates/shared` folder.
|
|
79
|
+
|
|
80
|
+
The templates are just HTML fragments. Here is a stand-alone one for the YouTube thumbnail from
|
|
81
|
+
the example app:
|
|
82
|
+
|
|
83
|
+
```html
|
|
84
|
+
<img src="https://img.youtube.com/vi/${ video.id }/maxresdefault.jpg"
|
|
85
|
+
class="img img-responsive ${ ' '.join(classes or []) }"
|
|
86
|
+
alt="${ video.title }"
|
|
87
|
+
title="${ video.title }">
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Notice that an object called `video` and a list of classes are passed in as the model.
|
|
91
|
+
|
|
92
|
+
Templates can also be nested. Here is the whole single video fragment with the image as well as other info
|
|
93
|
+
linking out to YouTube:
|
|
94
|
+
|
|
95
|
+
```html
|
|
96
|
+
<div>
|
|
97
|
+
<a href="https://www.youtube.com/watch?v=${ video.id }" target="_blank">
|
|
98
|
+
${ render_partial('shared/partials/video_image.pt', video=video, classes=[]) }
|
|
99
|
+
</a>
|
|
100
|
+
<a href="https://www.youtube.com/watch?v=${ video.id }" target="_blank"
|
|
101
|
+
class="author">${ video.author }</a>
|
|
102
|
+
<div class="views">${ "{:,}".format(video.views) } views</div>
|
|
103
|
+
</div>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Now you see the `render_partial()` method. It takes the subpath into the templates folder and
|
|
107
|
+
any model data passed in as keyword arguments.
|
|
108
|
+
|
|
109
|
+
We can finally generate the list of video blocks as follows:
|
|
110
|
+
|
|
111
|
+
```html
|
|
112
|
+
<span class="video" tal:repeat="v videos">
|
|
113
|
+
${ render_partial('shared/partials/video_square.pt', video=v) }
|
|
114
|
+
</span>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This time, we reframe each item in the list from the outer template (called `v`) as the `video` model
|
|
118
|
+
in the inner HTML section.
|
|
119
|
+
|
|
120
|
+
## The View Methods
|
|
121
|
+
|
|
122
|
+
In order to share the `render_partial()` function with your template, you'll need to pass it along to the
|
|
123
|
+
template with your model (dictionary).
|
|
124
|
+
|
|
125
|
+
If you are using the **Pyramid web framework**, you can add this file as middleware. Just drop it into
|
|
126
|
+
your `views` folder:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# views/partials_middleware.py
|
|
130
|
+
from pyramid.events import subscriber, BeforeRender
|
|
131
|
+
|
|
132
|
+
import chameleon_partials
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@subscriber(BeforeRender)
|
|
136
|
+
def add_global(event):
|
|
137
|
+
event['render_partial'] = chameleon_partials.render_partial
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
For other frameworks using Chameleon (e.g. FastAPI), you can add `render_partial` to the
|
|
141
|
+
resulting dictionary. We've built a simple function to keep this fool-proof:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
chameleon_partials.extend_model(model)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Here's a typical view method that uses `render_partial`, notice the use of extending the
|
|
148
|
+
model before passing it to the template (the example app's views skip this because the
|
|
149
|
+
middleware above handles it):
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
@view_config(route_name='listing', renderer='demo_chameleon_partials:templates/home/listing.pt')
|
|
153
|
+
def listing(_):
|
|
154
|
+
videos = video_service.all_videos()
|
|
155
|
+
model = dict(videos=videos)
|
|
156
|
+
return chameleon_partials.extend_model(model)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Again: If you are using Pyramid, use the middleware. Otherwise, use the `extend_model()` method or something
|
|
160
|
+
similar to the middleware in your framework.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
chameleon_partials - Simple reuse of partial HTML page templates in the
|
|
3
|
+
Chameleon template language for Python web frameworks.
|
|
4
|
+
|
|
5
|
+
Register your template folder once at application startup, then call `render_partial`
|
|
6
|
+
from any Chameleon template to insert a reusable HTML fragment, passing keyword
|
|
7
|
+
arguments as the fragment's model. Partials can nest: every partial automatically
|
|
8
|
+
receives `render_partial`, so fragments can compose further fragments. Works with any
|
|
9
|
+
framework that renders Chameleon templates (Pyramid, FastAPI, and friends).
|
|
10
|
+
|
|
11
|
+
A minimal quickstart:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import chameleon_partials
|
|
15
|
+
|
|
16
|
+
chameleon_partials.register_extensions('path/to/templates')
|
|
17
|
+
html = chameleon_partials.render_partial('shared/partials/video_image.pt', video=video)
|
|
18
|
+
```
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__version__ = '0.2.0'
|
|
22
|
+
__author__ = 'Michael Kennedy <michael@talkpython.fm>'
|
|
23
|
+
__all__ = [
|
|
24
|
+
'register_extensions',
|
|
25
|
+
'render_partial',
|
|
26
|
+
'PartialsException',
|
|
27
|
+
'extend_model',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
import os
|
|
31
|
+
from typing import Any, Dict, Optional
|
|
32
|
+
|
|
33
|
+
from chameleon import PageTemplate, PageTemplateLoader
|
|
34
|
+
|
|
35
|
+
has_registered_extensions: bool = False
|
|
36
|
+
|
|
37
|
+
__templates: Optional[PageTemplateLoader] = None
|
|
38
|
+
template_path: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PartialsException(Exception):
|
|
42
|
+
"""Raised when chameleon_partials is configured or used incorrectly.
|
|
43
|
+
|
|
44
|
+
Examples include registering with a missing template folder, rendering a partial
|
|
45
|
+
before calling `register_extensions`, or passing a non-dictionary model to
|
|
46
|
+
`extend_model`. Errors raised by Chameleon itself, such as the `ValueError` for a
|
|
47
|
+
missing template file, are propagated unchanged rather than wrapped in this type.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def register_extensions(template_folder: str, auto_reload: bool = False, cache_init: bool = True) -> None:
|
|
52
|
+
"""Register chameleon_partials with Chameleon so partials can be rendered.
|
|
53
|
+
|
|
54
|
+
Call this once during application startup, before any template is rendered. It builds a
|
|
55
|
+
Chameleon `PageTemplateLoader` rooted at `template_folder` and stores it in module-level
|
|
56
|
+
state that `render_partial` uses to locate templates. Registration is process-wide and is
|
|
57
|
+
not guarded by a lock, so call it from normal single-threaded startup code rather than
|
|
58
|
+
from concurrent request handlers.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
template_folder: Path to the root folder containing your Chameleon templates (with a
|
|
62
|
+
`partials` subfolder by convention). Must be an existing directory. Partials are
|
|
63
|
+
later looked up by path relative to this folder.
|
|
64
|
+
auto_reload: When `True`, Chameleon reloads a template from disk whenever the file
|
|
65
|
+
changes. Useful during development; leave `False` in production.
|
|
66
|
+
cache_init: When `True` (the default), calling this again after a previous
|
|
67
|
+
successful registration is a no-op, which makes repeated startup calls safe.
|
|
68
|
+
Pass `False` to force re-registration, for example to point at a different
|
|
69
|
+
template folder in tests.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
PartialsException: If `template_folder` is empty or is not an existing directory.
|
|
73
|
+
|
|
74
|
+
Examples:
|
|
75
|
+
```python
|
|
76
|
+
import chameleon_partials
|
|
77
|
+
|
|
78
|
+
# At startup; use auto_reload=True while developing.
|
|
79
|
+
chameleon_partials.register_extensions('path/to/templates', auto_reload=True)
|
|
80
|
+
```
|
|
81
|
+
"""
|
|
82
|
+
global has_registered_extensions
|
|
83
|
+
global __templates, template_path
|
|
84
|
+
|
|
85
|
+
if has_registered_extensions and cache_init:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
if not template_folder:
|
|
89
|
+
msg = 'The template_folder must be specified.'
|
|
90
|
+
raise PartialsException(msg)
|
|
91
|
+
|
|
92
|
+
if not os.path.isdir(template_folder):
|
|
93
|
+
msg = f"The specified template folder must be a folder, it's not: {template_folder}"
|
|
94
|
+
raise PartialsException(msg)
|
|
95
|
+
|
|
96
|
+
template_path = template_folder
|
|
97
|
+
__templates = PageTemplateLoader(template_folder, auto_reload=auto_reload)
|
|
98
|
+
|
|
99
|
+
# Mark as registered only after the loader was built successfully, so a call that
|
|
100
|
+
# raised above does not leave the extension flagged as registered (which would turn a
|
|
101
|
+
# later default cache_init=True retry into a silent no-op).
|
|
102
|
+
has_registered_extensions = True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class HTML:
|
|
106
|
+
"""A thin wrapper that marks a string as already-rendered, safe HTML.
|
|
107
|
+
|
|
108
|
+
`render_partial` returns one of these. Chameleon (and other template engines) call an
|
|
109
|
+
object's `__html__` method when present, so the wrapped markup is inserted verbatim
|
|
110
|
+
instead of being escaped. This type is internal and is not part of the public `__all__`
|
|
111
|
+
API; read the `html_text` attribute when you need the raw markup, for example in tests.
|
|
112
|
+
|
|
113
|
+
Attributes:
|
|
114
|
+
html_text: The rendered markup as a `str`.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(self, html_text: str) -> None:
|
|
118
|
+
self.html_text: str = html_text
|
|
119
|
+
|
|
120
|
+
def __html__(self) -> str:
|
|
121
|
+
return self.html_text
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def render_partial(template_file: str, **template_data: Any) -> HTML:
|
|
125
|
+
"""Render a partial template to an HTML fragment.
|
|
126
|
+
|
|
127
|
+
Looks up `template_file` in the folder registered with `register_extensions` and renders
|
|
128
|
+
it with the supplied keyword arguments as its model. `render_partial` is injected into the
|
|
129
|
+
model automatically, so a partial can render further nested partials. The fragment is
|
|
130
|
+
rendered as text (`str`); any `bytes` values in the model are decoded as UTF-8.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
template_file: Path to the partial, relative to the registered templates folder, for
|
|
134
|
+
example `shared/partials/video_image.pt`.
|
|
135
|
+
**template_data: Keyword arguments passed to the template as its model (the variables
|
|
136
|
+
the template can reference). Values may be any Python objects your template
|
|
137
|
+
expressions use. The name `encoding` is reserved and cannot be used; `translate`,
|
|
138
|
+
`target_language`, and `repeat` have special meaning to Chameleon.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
An `HTML` wrapper whose `__html__` method yields the rendered markup as safe, pre-escaped HTML.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
PartialsException: If `register_extensions` has not been called yet.
|
|
145
|
+
ValueError: Propagated from Chameleon if `template_file` does not exist under the registered folder.
|
|
146
|
+
|
|
147
|
+
Examples:
|
|
148
|
+
```python
|
|
149
|
+
import chameleon_partials
|
|
150
|
+
|
|
151
|
+
chameleon_partials.register_extensions('path/to/templates')
|
|
152
|
+
html = chameleon_partials.render_partial('shared/partials/user_card.pt', name='Sarah', age=32)
|
|
153
|
+
print(html.html_text)
|
|
154
|
+
```
|
|
155
|
+
"""
|
|
156
|
+
if not has_registered_extensions:
|
|
157
|
+
raise PartialsException('You must call register_extensions() before this function can be used.')
|
|
158
|
+
|
|
159
|
+
if 'render_partial' not in template_data:
|
|
160
|
+
template_data['render_partial'] = render_partial
|
|
161
|
+
|
|
162
|
+
assert __templates is not None # Guaranteed by the has_registered_extensions guard above.
|
|
163
|
+
page: PageTemplate = __templates[template_file]
|
|
164
|
+
html_source = page.render(encoding='utf-8', **template_data)
|
|
165
|
+
return HTML(html_source)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def extend_model(model: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
|
169
|
+
"""Add `render_partial` to a view model so templates can call it.
|
|
170
|
+
|
|
171
|
+
Use this in frameworks where a view returns a model dictionary (for example FastAPI) and
|
|
172
|
+
you need `render_partial` available inside the template. For Pyramid, prefer the
|
|
173
|
+
`BeforeRender` middleware shown in the README instead. Any existing `render_partial` key
|
|
174
|
+
in the model is replaced.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
model: The view model dictionary to extend. `None` is treated as an empty model.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
The same dictionary with a `render_partial` key added, or a new dictionary when `model` is `None`.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
PartialsException: If `model` is not a dictionary (and not `None`).
|
|
184
|
+
|
|
185
|
+
Examples:
|
|
186
|
+
```python
|
|
187
|
+
import chameleon_partials
|
|
188
|
+
|
|
189
|
+
model = {'name': 'Sarah'}
|
|
190
|
+
model = chameleon_partials.extend_model(model)
|
|
191
|
+
# model['render_partial'] is now callable from the template.
|
|
192
|
+
```
|
|
193
|
+
"""
|
|
194
|
+
if model is None:
|
|
195
|
+
model = {}
|
|
196
|
+
|
|
197
|
+
if not isinstance(model, dict):
|
|
198
|
+
raise PartialsException('The model must be a dictionary.')
|
|
199
|
+
|
|
200
|
+
model['render_partial'] = render_partial
|
|
201
|
+
return model
|
|
File without changes
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chameleon_partials
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Simple reuse of partial HTML page templates in the Chameleon template language for Python web frameworks.
|
|
5
|
+
Author-email: Michael Kennedy <michael@talkpython.fm>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mikeckennedy/chameleon_partials
|
|
8
|
+
Project-URL: Repository, https://github.com/mikeckennedy/chameleon_partials
|
|
9
|
+
Project-URL: Documentation, https://mkennedy.codes/docs/chameleon-partials/
|
|
10
|
+
Project-URL: Issues, https://github.com/mikeckennedy/chameleon_partials/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/mikeckennedy/chameleon_partials/blob/main/CHANGELOG.md
|
|
12
|
+
Project-URL: Funding, https://github.com/sponsors/mikeckennedy
|
|
13
|
+
Keywords: chameleon,partials,templates,templating,template-engine,html,rendering,render-partial,components,fragments,reusable,web,web-development,web-framework,pyramid,fastapi,htmx,frontend,jinja-partials,zpt,tal,metal
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: chameleon
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-clarity; extra == "dev"
|
|
32
|
+
Requires-Dist: ty; extra == "dev"
|
|
33
|
+
Requires-Dist: pyrefly; extra == "dev"
|
|
34
|
+
Requires-Dist: great-docs; python_version >= "3.11" and extra == "dev"
|
|
35
|
+
Dynamic: license-file
|
|
36
|
+
|
|
37
|
+
# Chameleon Partials
|
|
38
|
+
|
|
39
|
+
Simple reuse of partial HTML page templates in the Chameleon template language for Python web frameworks.
|
|
40
|
+
(There is also a [Jinja2/Flask version here](https://github.com/mikeckennedy/jinja_partials).)
|
|
41
|
+
|
|
42
|
+
## Overview
|
|
43
|
+
|
|
44
|
+
When building real-world web apps with Chameleon, it's easy to end up with repeated HTML fragments.
|
|
45
|
+
Just like organizing code for reuse, it would be ideal to reuse smaller sections of HTML template code.
|
|
46
|
+
That's what this library is all about.
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
Full documentation lives at
|
|
51
|
+
[mkennedy.codes/docs/chameleon-partials](https://mkennedy.codes/docs/chameleon-partials/),
|
|
52
|
+
including the complete [API reference](https://mkennedy.codes/docs/chameleon-partials/reference/)
|
|
53
|
+
generated from the library's docstrings.
|
|
54
|
+
|
|
55
|
+
## Example
|
|
56
|
+
|
|
57
|
+
This project comes with a sample Pyramid application (see the `example` folder). This app displays videos
|
|
58
|
+
that can be played on YouTube. The image, author subtitle, and view count are reused throughout the
|
|
59
|
+
app. Here's a visual:
|
|
60
|
+
|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
Check out the [**demo / example application**](https://github.com/mikeckennedy/chameleon_partials/tree/main/example)
|
|
64
|
+
to see it in action.
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
It's just `pip install chameleon-partials` and you're all set with this pure Python package.
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
Using the library is incredibly easy. The first step is to register the partial method with Chameleon.
|
|
73
|
+
Do this once at app startup:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from pathlib import Path
|
|
77
|
+
|
|
78
|
+
from pyramid.config import Configurator
|
|
79
|
+
|
|
80
|
+
import chameleon_partials
|
|
81
|
+
|
|
82
|
+
def main(_, **settings):
|
|
83
|
+
""" This function returns a Pyramid WSGI application.
|
|
84
|
+
"""
|
|
85
|
+
with Configurator(settings=settings) as config:
|
|
86
|
+
config.include('pyramid_chameleon')
|
|
87
|
+
config.include('.routes')
|
|
88
|
+
config.scan()
|
|
89
|
+
|
|
90
|
+
# Register the extension for working with Chameleon.
|
|
91
|
+
folder = (Path(__file__).parent / "templates").as_posix()
|
|
92
|
+
chameleon_partials.register_extensions(folder, auto_reload=True, cache_init=True)
|
|
93
|
+
|
|
94
|
+
return config.make_wsgi_app()
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Next, you define your main HTML (Chameleon) templates as usual. Then
|
|
98
|
+
define your partial templates. I recommend locating and naming them accordingly:
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
├── templates
|
|
102
|
+
│ ├── errors
|
|
103
|
+
│ │ └── 404.pt
|
|
104
|
+
│ ├── home
|
|
105
|
+
│ │ ├── index.pt
|
|
106
|
+
│ │ └── listing.pt
|
|
107
|
+
│ └── shared
|
|
108
|
+
│ ├── _layout.pt
|
|
109
|
+
│ └── partials
|
|
110
|
+
│ ├── video_image.pt
|
|
111
|
+
│ └── video_square.pt
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Notice the `partials` subfolder in the `templates/shared` folder.
|
|
115
|
+
|
|
116
|
+
The templates are just HTML fragments. Here is a stand-alone one for the YouTube thumbnail from
|
|
117
|
+
the example app:
|
|
118
|
+
|
|
119
|
+
```html
|
|
120
|
+
<img src="https://img.youtube.com/vi/${ video.id }/maxresdefault.jpg"
|
|
121
|
+
class="img img-responsive ${ ' '.join(classes or []) }"
|
|
122
|
+
alt="${ video.title }"
|
|
123
|
+
title="${ video.title }">
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Notice that an object called `video` and a list of classes are passed in as the model.
|
|
127
|
+
|
|
128
|
+
Templates can also be nested. Here is the whole single video fragment with the image as well as other info
|
|
129
|
+
linking out to YouTube:
|
|
130
|
+
|
|
131
|
+
```html
|
|
132
|
+
<div>
|
|
133
|
+
<a href="https://www.youtube.com/watch?v=${ video.id }" target="_blank">
|
|
134
|
+
${ render_partial('shared/partials/video_image.pt', video=video, classes=[]) }
|
|
135
|
+
</a>
|
|
136
|
+
<a href="https://www.youtube.com/watch?v=${ video.id }" target="_blank"
|
|
137
|
+
class="author">${ video.author }</a>
|
|
138
|
+
<div class="views">${ "{:,}".format(video.views) } views</div>
|
|
139
|
+
</div>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Now you see the `render_partial()` method. It takes the subpath into the templates folder and
|
|
143
|
+
any model data passed in as keyword arguments.
|
|
144
|
+
|
|
145
|
+
We can finally generate the list of video blocks as follows:
|
|
146
|
+
|
|
147
|
+
```html
|
|
148
|
+
<span class="video" tal:repeat="v videos">
|
|
149
|
+
${ render_partial('shared/partials/video_square.pt', video=v) }
|
|
150
|
+
</span>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
This time, we reframe each item in the list from the outer template (called `v`) as the `video` model
|
|
154
|
+
in the inner HTML section.
|
|
155
|
+
|
|
156
|
+
## The View Methods
|
|
157
|
+
|
|
158
|
+
In order to share the `render_partial()` function with your template, you'll need to pass it along to the
|
|
159
|
+
template with your model (dictionary).
|
|
160
|
+
|
|
161
|
+
If you are using the **Pyramid web framework**, you can add this file as middleware. Just drop it into
|
|
162
|
+
your `views` folder:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# views/partials_middleware.py
|
|
166
|
+
from pyramid.events import subscriber, BeforeRender
|
|
167
|
+
|
|
168
|
+
import chameleon_partials
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@subscriber(BeforeRender)
|
|
172
|
+
def add_global(event):
|
|
173
|
+
event['render_partial'] = chameleon_partials.render_partial
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
For other frameworks using Chameleon (e.g. FastAPI), you can add `render_partial` to the
|
|
177
|
+
resulting dictionary. We've built a simple function to keep this fool-proof:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
chameleon_partials.extend_model(model)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Here's a typical view method that uses `render_partial`, notice the use of extending the
|
|
184
|
+
model before passing it to the template (the example app's views skip this because the
|
|
185
|
+
middleware above handles it):
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
@view_config(route_name='listing', renderer='demo_chameleon_partials:templates/home/listing.pt')
|
|
189
|
+
def listing(_):
|
|
190
|
+
videos = video_service.all_videos()
|
|
191
|
+
model = dict(videos=videos)
|
|
192
|
+
return chameleon_partials.extend_model(model)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Again: If you are using Pyramid, use the middleware. Otherwise, use the `extend_model()` method or something
|
|
196
|
+
similar to the middleware in your framework.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
requirements-dev.txt
|
|
5
|
+
requirements.txt
|
|
6
|
+
chameleon_partials/__init__.py
|
|
7
|
+
chameleon_partials/py.typed
|
|
8
|
+
chameleon_partials.egg-info/PKG-INFO
|
|
9
|
+
chameleon_partials.egg-info/SOURCES.txt
|
|
10
|
+
chameleon_partials.egg-info/dependency_links.txt
|
|
11
|
+
chameleon_partials.egg-info/requires.txt
|
|
12
|
+
chameleon_partials.egg-info/top_level.txt
|
|
13
|
+
tests/test_rendering.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chameleon_partials
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chameleon_partials"
|
|
7
|
+
description = "Simple reuse of partial HTML page templates in the Chameleon template language for Python web frameworks."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = { text = "MIT" }
|
|
10
|
+
authors = [{ name = "Michael Kennedy", email = "michael@talkpython.fm" }]
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
keywords = [
|
|
13
|
+
"chameleon",
|
|
14
|
+
"partials",
|
|
15
|
+
"templates",
|
|
16
|
+
"templating",
|
|
17
|
+
"template-engine",
|
|
18
|
+
"html",
|
|
19
|
+
"rendering",
|
|
20
|
+
"render-partial",
|
|
21
|
+
"components",
|
|
22
|
+
"fragments",
|
|
23
|
+
"reusable",
|
|
24
|
+
"web",
|
|
25
|
+
"web-development",
|
|
26
|
+
"web-framework",
|
|
27
|
+
"pyramid",
|
|
28
|
+
"fastapi",
|
|
29
|
+
"htmx",
|
|
30
|
+
"frontend",
|
|
31
|
+
"jinja-partials",
|
|
32
|
+
"zpt",
|
|
33
|
+
"tal",
|
|
34
|
+
"metal",
|
|
35
|
+
]
|
|
36
|
+
# version, dependencies, and optional-dependencies are read dynamically below so the
|
|
37
|
+
# package version stays single-sourced in chameleon_partials/__init__.py and the
|
|
38
|
+
# requirements*.txt files remain the source of truth for dependencies.
|
|
39
|
+
dynamic = ["version", "dependencies", "optional-dependencies"]
|
|
40
|
+
classifiers = [
|
|
41
|
+
"Development Status :: 5 - Production/Stable",
|
|
42
|
+
"License :: OSI Approved :: MIT License",
|
|
43
|
+
"Programming Language :: Python",
|
|
44
|
+
"Programming Language :: Python :: 3",
|
|
45
|
+
"Programming Language :: Python :: 3.9",
|
|
46
|
+
"Programming Language :: Python :: 3.10",
|
|
47
|
+
"Programming Language :: Python :: 3.11",
|
|
48
|
+
"Programming Language :: Python :: 3.12",
|
|
49
|
+
"Programming Language :: Python :: 3.13",
|
|
50
|
+
"Programming Language :: Python :: 3.14",
|
|
51
|
+
"Programming Language :: Python :: 3.15",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[project.urls]
|
|
55
|
+
Homepage = "https://github.com/mikeckennedy/chameleon_partials"
|
|
56
|
+
Repository = "https://github.com/mikeckennedy/chameleon_partials"
|
|
57
|
+
Documentation = "https://mkennedy.codes/docs/chameleon-partials/"
|
|
58
|
+
Issues = "https://github.com/mikeckennedy/chameleon_partials/issues"
|
|
59
|
+
Changelog = "https://github.com/mikeckennedy/chameleon_partials/blob/main/CHANGELOG.md"
|
|
60
|
+
Funding = "https://github.com/sponsors/mikeckennedy"
|
|
61
|
+
|
|
62
|
+
[tool.setuptools]
|
|
63
|
+
packages = ["chameleon_partials"]
|
|
64
|
+
|
|
65
|
+
# Ship the PEP 561 marker so type checkers consume the package's inline annotations.
|
|
66
|
+
# Explicit because the build floor (setuptools>=64) predates automatic py.typed inclusion.
|
|
67
|
+
[tool.setuptools.package-data]
|
|
68
|
+
chameleon_partials = ["py.typed"]
|
|
69
|
+
|
|
70
|
+
[tool.setuptools.dynamic]
|
|
71
|
+
version = { attr = "chameleon_partials.__version__" }
|
|
72
|
+
dependencies = { file = ["requirements.txt"] }
|
|
73
|
+
|
|
74
|
+
[tool.setuptools.dynamic.optional-dependencies]
|
|
75
|
+
dev = { file = ["requirements-dev.txt"] }
|
|
76
|
+
|
|
77
|
+
# Only collect this package's own tests by default. The `example/` demo is a
|
|
78
|
+
# standalone Pyramid project with its own test suite and dependencies (Pyramid,
|
|
79
|
+
# WebTest — see example/setup.py's `testing` extra), so a bare `pytest` from the
|
|
80
|
+
# repo root must not try to import them.
|
|
81
|
+
[tool.pytest.ini_options]
|
|
82
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pytest
|
|
2
|
+
pytest-clarity
|
|
3
|
+
# Type checkers; the package ships a py.typed marker, so keep its annotations honest.
|
|
4
|
+
ty
|
|
5
|
+
pyrefly
|
|
6
|
+
# great-docs builds the documentation site (Phase 1 of the docs playbook). The
|
|
7
|
+
# marker keeps `pip install .[dev]` working on Python 3.10 and below, where
|
|
8
|
+
# great-docs (which requires >=3.11) is simply skipped.
|
|
9
|
+
great-docs; python_version >= '3.11'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
chameleon
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Rendering tests for chameleon_partials: bare partials, partials with model data,
|
|
2
|
+
layouts, nested (recursive) partials, and error conditions (missing template, and
|
|
3
|
+
rendering before register_extensions)."""
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
# noinspection PyPackageRequirements
|
|
8
|
+
import pytest as pytest
|
|
9
|
+
|
|
10
|
+
import chameleon_partials
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def registered_extension():
|
|
15
|
+
folder = (Path(__file__).parent / 'test_templates').as_posix()
|
|
16
|
+
chameleon_partials.register_extensions(folder, auto_reload=True, cache_init=True)
|
|
17
|
+
|
|
18
|
+
# Allow test to work with extensions as registered
|
|
19
|
+
print('********************************************')
|
|
20
|
+
print(folder)
|
|
21
|
+
print('********************************************')
|
|
22
|
+
yield
|
|
23
|
+
|
|
24
|
+
# Roll back the fact that we registered the extensions for future tests.
|
|
25
|
+
chameleon_partials.has_registered_extensions = False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_render_empty(registered_extension):
|
|
29
|
+
html: chameleon_partials.HTML = chameleon_partials.render_partial('render/bare.pt')
|
|
30
|
+
assert '<h1>This is bare HTML fragment</h1>' in html.html_text
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_render_with_data(registered_extension):
|
|
34
|
+
name = 'Sarah'
|
|
35
|
+
age = 32
|
|
36
|
+
html: chameleon_partials.HTML = chameleon_partials.render_partial('render/with_data.pt', name=name, age=age)
|
|
37
|
+
assert f'<span>Your name is {name} and age is {age}</span>' in html.html_text
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_render_with_layout(registered_extension):
|
|
41
|
+
value_text = 'The message is clear'
|
|
42
|
+
html: chameleon_partials.HTML = chameleon_partials.render_partial('render/with_layout.pt', message=value_text)
|
|
43
|
+
assert '<title>Chameleon Partials Test Template</title>' in html.html_text
|
|
44
|
+
assert value_text in html.html_text
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_render_recursive(registered_extension):
|
|
48
|
+
value_text = 'The message is clear'
|
|
49
|
+
inner_text = 'The message is recursive'
|
|
50
|
+
|
|
51
|
+
html: chameleon_partials.HTML = chameleon_partials.render_partial(
|
|
52
|
+
'render/recursive.pt', message=value_text, inner=inner_text
|
|
53
|
+
)
|
|
54
|
+
assert value_text in html.html_text
|
|
55
|
+
assert inner_text in html.html_text
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_missing_template(registered_extension):
|
|
59
|
+
with pytest.raises(ValueError):
|
|
60
|
+
chameleon_partials.render_partial('no-way.pt', message=7)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_not_registered():
|
|
64
|
+
with pytest.raises(chameleon_partials.PartialsException):
|
|
65
|
+
chameleon_partials.render_partial('doesnt-matter.pt', message=7)
|