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.
@@ -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
+ ![Video image, author, and view count HTML reused across pages of the demo app](https://raw.githubusercontent.com/mikeckennedy/chameleon_partials/main/readme_resources/reused-html-visual.png)
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
+ ![Video image, author, and view count HTML reused across pages of the demo app](https://raw.githubusercontent.com/mikeckennedy/chameleon_partials/main/readme_resources/reused-html-visual.png)
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
+ ![Video image, author, and view count HTML reused across pages of the demo app](https://raw.githubusercontent.com/mikeckennedy/chameleon_partials/main/readme_resources/reused-html-visual.png)
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,10 @@
1
+ chameleon
2
+
3
+ [dev]
4
+ pytest
5
+ pytest-clarity
6
+ ty
7
+ pyrefly
8
+
9
+ [dev:python_version >= "3.11"]
10
+ great-docs
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)