python-fragments 0.33__tar.gz → 0.36__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.
Files changed (53) hide show
  1. python_fragments-0.36/PKG-INFO +112 -0
  2. python_fragments-0.36/README.md +62 -0
  3. {python_fragments-0.33 → python_fragments-0.36}/fragments/ast_nodes.py +9 -7
  4. {python_fragments-0.33 → python_fragments-0.36}/fragments/grammar.py +2 -2
  5. {python_fragments-0.33 → python_fragments-0.36}/fragments/html/elements.py +2 -2
  6. {python_fragments-0.33 → python_fragments-0.36}/pyproject.toml +18 -1
  7. python_fragments-0.36/python_fragments.egg-info/PKG-INFO +112 -0
  8. {python_fragments-0.33 → python_fragments-0.36}/tests/test_end_to_end.py +10 -2
  9. {python_fragments-0.33 → python_fragments-0.36}/tests/test_grammar.py +11 -11
  10. python_fragments-0.36/tests/test_html_elements.py +55 -0
  11. python_fragments-0.33/PKG-INFO +0 -88
  12. python_fragments-0.33/README.md +0 -51
  13. python_fragments-0.33/python_fragments.egg-info/PKG-INFO +0 -88
  14. python_fragments-0.33/tests/test_html_elements.py +0 -21
  15. {python_fragments-0.33 → python_fragments-0.36}/LICENSE +0 -0
  16. {python_fragments-0.33 → python_fragments-0.36}/fragments/__init__.py +0 -0
  17. {python_fragments-0.33 → python_fragments-0.36}/fragments/cli.py +0 -0
  18. {python_fragments-0.33 → python_fragments-0.36}/fragments/html/__init__.py +0 -0
  19. {python_fragments-0.33 → python_fragments-0.36}/fragments/loader.py +0 -0
  20. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/__init__.py +0 -0
  21. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/based_proxy.py +0 -0
  22. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/__init__.py +0 -0
  23. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/code_actions.py +0 -0
  24. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/completion.py +0 -0
  25. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/definition.py +0 -0
  26. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/diagnostics.py +0 -0
  27. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/document_highlight.py +0 -0
  28. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/document_symbols.py +0 -0
  29. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/folding_range.py +0 -0
  30. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/hover.py +0 -0
  31. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/inlay_hints.py +0 -0
  32. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/lifecycle.py +0 -0
  33. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/references.py +0 -0
  34. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/rename.py +0 -0
  35. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/semantic_tokens.py +0 -0
  36. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/client_message_handlers/signature_help.py +0 -0
  37. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/file_state.py +0 -0
  38. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/message_queue.py +0 -0
  39. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/pyright_notification_handlers/__init__.py +0 -0
  40. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/pyright_notification_handlers/capability.py +0 -0
  41. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/pyright_notification_handlers/configuration.py +0 -0
  42. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/pyright_notification_handlers/diagnostics.py +0 -0
  43. {python_fragments-0.33 → python_fragments-0.36}/fragments/lsp/types.py +0 -0
  44. {python_fragments-0.33 → python_fragments-0.36}/fragments/source.py +0 -0
  45. {python_fragments-0.33 → python_fragments-0.36}/fragments/transpiler.py +0 -0
  46. {python_fragments-0.33 → python_fragments-0.36}/fragments/types.py +0 -0
  47. {python_fragments-0.33 → python_fragments-0.36}/python_fragments.egg-info/SOURCES.txt +0 -0
  48. {python_fragments-0.33 → python_fragments-0.36}/python_fragments.egg-info/dependency_links.txt +0 -0
  49. {python_fragments-0.33 → python_fragments-0.36}/python_fragments.egg-info/entry_points.txt +0 -0
  50. {python_fragments-0.33 → python_fragments-0.36}/python_fragments.egg-info/requires.txt +0 -0
  51. {python_fragments-0.33 → python_fragments-0.36}/python_fragments.egg-info/top_level.txt +0 -0
  52. {python_fragments-0.33 → python_fragments-0.36}/setup.cfg +0 -0
  53. {python_fragments-0.33 → python_fragments-0.36}/tests/test_source_map.py +0 -0
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-fragments
3
+ Version: 0.36
4
+ Summary: Modern HTML template rendering in Python
5
+ Author-email: The Running Algorithm <services@therunningalgorithm.info>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 The Running Algorithm
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://python-fragments.io
29
+ Project-URL: Documentation, https://python-fragments.io
30
+ Project-URL: Source, https://github.com/TheRunningAlgorithm2/python-fragments
31
+ Project-URL: Issues, https://github.com/TheRunningAlgorithm2/python-fragments/issues
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
39
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
40
+ Classifier: Topic :: Text Processing :: Markup :: HTML
41
+ Requires-Python: >=3.12
42
+ Description-Content-Type: text/markdown
43
+ License-File: LICENSE
44
+ Provides-Extra: lsp
45
+ Requires-Dist: basedpyright>=1.39.0; extra == "lsp"
46
+ Requires-Dist: lsprotocol>=2025.0.0; extra == "lsp"
47
+ Provides-Extra: dev
48
+ Requires-Dist: pytest>=8.4.1; extra == "dev"
49
+ Dynamic: license-file
50
+
51
+ <p align="center">
52
+ <img src="logo.svg" alt="Python Fragments" width="300" />
53
+ </p>
54
+
55
+ <p align="center">
56
+ <a href="https://github.com/TheRunningAlgorithm2/python-fragments/actions/workflows/test.yml"><img src="https://github.com/TheRunningAlgorithm2/python-fragments/actions/workflows/test.yml/badge.svg" alt="Tests" /></a>
57
+ <a href="https://pypi.org/project/python-fragments/"><img src="https://img.shields.io/pypi/v/python-fragments" alt="PyPI version" /></a>
58
+ <a href="https://pypi.org/project/python-fragments/"><img src="https://img.shields.io/pypi/pyversions/python-fragments" alt="Python versions" /></a>
59
+ <a href="https://github.com/TheRunningAlgorithm2/python-fragments/blob/main/LICENSE"><img src="https://img.shields.io/github/license/TheRunningAlgorithm2/python-fragments" alt="License" /></a>
60
+ <a href="https://python-fragments.io"><img src="https://img.shields.io/badge/docs-python--fragments.io-blue" alt="Documentation" /></a>
61
+ <a href="https://marketplace.visualstudio.com/items?itemName=tra-technologies-ltd.python-fragments"><img src="https://img.shields.io/badge/VS%20Code-Extension-blue?logo=visualstudiocode" alt="VS Code Extension" /></a>
62
+ </p>
63
+
64
+ > **This package is in early development and not yet stable. The API may change without notice between releases.**
65
+
66
+ Modern HTML template rendering in Python. No build step, no template files, and native HTML awareness out of the box. [Read More](https://python-fragments.io)
67
+
68
+ ```python
69
+ from fastapi import APIRouter
70
+
71
+ router = APIRouter()
72
+
73
+ @router.get("/", response_class=HTMLResponse)
74
+ async def index() -> str:
75
+ published = [p for p in POSTS if p.published]
76
+ return <>
77
+ <Layout title="My Blog">
78
+ <h1>Latest Posts</h1>
79
+ <PostCard for={{ post in published }} post={{ post }} />
80
+ </Layout>
81
+ </>
82
+ ```
83
+
84
+ ## IDE Support
85
+
86
+ Type checking, completions, hover docs, go-to-definition, rename, and semantic highlighting. All working inside fragment syntax.
87
+
88
+ Install the [Python Fragments VS Code extension](https://marketplace.visualstudio.com/items?itemName=tra-technologies-ltd.python-fragments) to get started.
89
+
90
+ ![VS Code completions demo](docs/assets/vscode.gif)
91
+
92
+ ## Installation
93
+
94
+ ```bash
95
+ pip install python-fragments
96
+ ```
97
+
98
+ Register the loader at your application's entry point, before importing any modules that contain fragments:
99
+
100
+ ```python
101
+ from fragments import loader # isort: skip
102
+ ```
103
+
104
+ Any `.py` file containing `<>` is transpiled automatically. Nothing else to configure.
105
+
106
+ ## Documentation
107
+
108
+ Full documentation is available at [python-fragments.io](https://python-fragments.io).
109
+
110
+ ## Contributing
111
+
112
+ Bug reports, feature requests, and documentation improvements are all welcome. Code contributions aren't open yet while we work toward v1. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
@@ -0,0 +1,62 @@
1
+ <p align="center">
2
+ <img src="logo.svg" alt="Python Fragments" width="300" />
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://github.com/TheRunningAlgorithm2/python-fragments/actions/workflows/test.yml"><img src="https://github.com/TheRunningAlgorithm2/python-fragments/actions/workflows/test.yml/badge.svg" alt="Tests" /></a>
7
+ <a href="https://pypi.org/project/python-fragments/"><img src="https://img.shields.io/pypi/v/python-fragments" alt="PyPI version" /></a>
8
+ <a href="https://pypi.org/project/python-fragments/"><img src="https://img.shields.io/pypi/pyversions/python-fragments" alt="Python versions" /></a>
9
+ <a href="https://github.com/TheRunningAlgorithm2/python-fragments/blob/main/LICENSE"><img src="https://img.shields.io/github/license/TheRunningAlgorithm2/python-fragments" alt="License" /></a>
10
+ <a href="https://python-fragments.io"><img src="https://img.shields.io/badge/docs-python--fragments.io-blue" alt="Documentation" /></a>
11
+ <a href="https://marketplace.visualstudio.com/items?itemName=tra-technologies-ltd.python-fragments"><img src="https://img.shields.io/badge/VS%20Code-Extension-blue?logo=visualstudiocode" alt="VS Code Extension" /></a>
12
+ </p>
13
+
14
+ > **This package is in early development and not yet stable. The API may change without notice between releases.**
15
+
16
+ Modern HTML template rendering in Python. No build step, no template files, and native HTML awareness out of the box. [Read More](https://python-fragments.io)
17
+
18
+ ```python
19
+ from fastapi import APIRouter
20
+
21
+ router = APIRouter()
22
+
23
+ @router.get("/", response_class=HTMLResponse)
24
+ async def index() -> str:
25
+ published = [p for p in POSTS if p.published]
26
+ return <>
27
+ <Layout title="My Blog">
28
+ <h1>Latest Posts</h1>
29
+ <PostCard for={{ post in published }} post={{ post }} />
30
+ </Layout>
31
+ </>
32
+ ```
33
+
34
+ ## IDE Support
35
+
36
+ Type checking, completions, hover docs, go-to-definition, rename, and semantic highlighting. All working inside fragment syntax.
37
+
38
+ Install the [Python Fragments VS Code extension](https://marketplace.visualstudio.com/items?itemName=tra-technologies-ltd.python-fragments) to get started.
39
+
40
+ ![VS Code completions demo](docs/assets/vscode.gif)
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install python-fragments
46
+ ```
47
+
48
+ Register the loader at your application's entry point, before importing any modules that contain fragments:
49
+
50
+ ```python
51
+ from fragments import loader # isort: skip
52
+ ```
53
+
54
+ Any `.py` file containing `<>` is transpiled automatically. Nothing else to configure.
55
+
56
+ ## Documentation
57
+
58
+ Full documentation is available at [python-fragments.io](https://python-fragments.io).
59
+
60
+ ## Contributing
61
+
62
+ Bug reports, feature requests, and documentation improvements are all welcome. Code contributions aren't open yet while we work toward v1. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
@@ -253,6 +253,7 @@ class ASTComponent:
253
253
  name: "ASTComponentName"
254
254
  arguments: dict[str, "ASTComponentArgument"]
255
255
  children: Sequence["ASTHTMLChild"]
256
+ self_closing: bool
256
257
 
257
258
  transpiled_content: str = field(init=False)
258
259
  transpiled_start: int = field(init=False)
@@ -263,13 +264,14 @@ class ASTComponent:
263
264
  def transpile(self, transpiled_start: int) -> None:
264
265
  self.transpiled_start = transpiled_start
265
266
  self.name.transpile(self.transpiled_start)
266
- self.transpiled_content = self.name.transpiled_content + '(""'
267
-
268
- for child in self.children:
269
- child.transpile(self.transpiled_start + len(self.transpiled_content))
270
- self.transpiled_content += "+" + child.transpiled_content
271
-
272
- self.transpiled_content += ","
267
+ self.transpiled_content = self.name.transpiled_content + "("
268
+
269
+ if not self.self_closing:
270
+ self.transpiled_content += 'children=""'
271
+ for child in self.children:
272
+ child.transpile(self.transpiled_start + len(self.transpiled_content))
273
+ self.transpiled_content += "+" + child.transpiled_content
274
+ self.transpiled_content += ","
273
275
 
274
276
  for argument in self.arguments.values():
275
277
  argument.transpile(self.transpiled_start + len(self.transpiled_content))
@@ -180,7 +180,7 @@ def expect_component(source: Source) -> tuple[Source, ASTComponent | ASTControlN
180
180
  if source.starts_with("/>"):
181
181
  source = expect_string(source, "/>")
182
182
  return source, ASTControlNode[ASTComponent].wrap_child(
183
- ASTComponent(source_start, source.offset, name, arguments, []),
183
+ ASTComponent(source_start, source.offset, name, arguments, [], self_closing=True),
184
184
  if_argument.interpolation if if_argument is not None else None,
185
185
  for_argument.interpolation if for_argument is not None else None,
186
186
  )
@@ -196,7 +196,7 @@ def expect_component(source: Source) -> tuple[Source, ASTComponent | ASTControlN
196
196
  raise ParsingError(f"Element closed ({closing_name.name}) is not the same as currently opened element ({name.name})", source.offset)
197
197
 
198
198
  return source, ASTControlNode[ASTComponent].wrap_child(
199
- ASTComponent(source_start=source_start, source_end=source.offset, name=name, arguments=arguments, children=children),
199
+ ASTComponent(source_start=source_start, source_end=source.offset, name=name, arguments=arguments, children=children, self_closing=False),
200
200
  if_argument.interpolation if if_argument is not None else None,
201
201
  for_argument.interpolation if for_argument is not None else None,
202
202
  )
@@ -21,12 +21,12 @@ def attribute_to_string(name: str, value: Any) -> str:
21
21
  return style_to_string(value)
22
22
 
23
23
  if isinstance(value, dict) or isinstance(value, list):
24
- value = html.escape(json.dumps(value))
24
+ value = json.dumps(value)
25
25
 
26
26
  if isinstance(value, bool):
27
27
  value = str(value).lower()
28
28
 
29
- return f'{name}="{value}"'
29
+ return f'{name}="{html.escape(str(value))}"'
30
30
 
31
31
 
32
32
  def className_to_string(contents: list[str] | str) -> str:
@@ -4,13 +4,30 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-fragments"
7
- version = "0.33"
7
+ version = "0.36"
8
8
  description = "Modern HTML template rendering in Python"
9
9
  authors = [{ name = "The Running Algorithm", email = "services@therunningalgorithm.info" }]
10
10
  readme = "README.md"
11
11
  license = { file = "LICENSE" }
12
12
  requires-python = ">=3.12"
13
13
  dependencies = []
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
22
+ "Topic :: Software Development :: Libraries :: Python Modules",
23
+ "Topic :: Text Processing :: Markup :: HTML",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://python-fragments.io"
28
+ Documentation = "https://python-fragments.io"
29
+ Source = "https://github.com/TheRunningAlgorithm2/python-fragments"
30
+ Issues = "https://github.com/TheRunningAlgorithm2/python-fragments/issues"
14
31
 
15
32
  [project.optional-dependencies]
16
33
  lsp = [
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.4
2
+ Name: python-fragments
3
+ Version: 0.36
4
+ Summary: Modern HTML template rendering in Python
5
+ Author-email: The Running Algorithm <services@therunningalgorithm.info>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 The Running Algorithm
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://python-fragments.io
29
+ Project-URL: Documentation, https://python-fragments.io
30
+ Project-URL: Source, https://github.com/TheRunningAlgorithm2/python-fragments
31
+ Project-URL: Issues, https://github.com/TheRunningAlgorithm2/python-fragments/issues
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Operating System :: OS Independent
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
39
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
40
+ Classifier: Topic :: Text Processing :: Markup :: HTML
41
+ Requires-Python: >=3.12
42
+ Description-Content-Type: text/markdown
43
+ License-File: LICENSE
44
+ Provides-Extra: lsp
45
+ Requires-Dist: basedpyright>=1.39.0; extra == "lsp"
46
+ Requires-Dist: lsprotocol>=2025.0.0; extra == "lsp"
47
+ Provides-Extra: dev
48
+ Requires-Dist: pytest>=8.4.1; extra == "dev"
49
+ Dynamic: license-file
50
+
51
+ <p align="center">
52
+ <img src="logo.svg" alt="Python Fragments" width="300" />
53
+ </p>
54
+
55
+ <p align="center">
56
+ <a href="https://github.com/TheRunningAlgorithm2/python-fragments/actions/workflows/test.yml"><img src="https://github.com/TheRunningAlgorithm2/python-fragments/actions/workflows/test.yml/badge.svg" alt="Tests" /></a>
57
+ <a href="https://pypi.org/project/python-fragments/"><img src="https://img.shields.io/pypi/v/python-fragments" alt="PyPI version" /></a>
58
+ <a href="https://pypi.org/project/python-fragments/"><img src="https://img.shields.io/pypi/pyversions/python-fragments" alt="Python versions" /></a>
59
+ <a href="https://github.com/TheRunningAlgorithm2/python-fragments/blob/main/LICENSE"><img src="https://img.shields.io/github/license/TheRunningAlgorithm2/python-fragments" alt="License" /></a>
60
+ <a href="https://python-fragments.io"><img src="https://img.shields.io/badge/docs-python--fragments.io-blue" alt="Documentation" /></a>
61
+ <a href="https://marketplace.visualstudio.com/items?itemName=tra-technologies-ltd.python-fragments"><img src="https://img.shields.io/badge/VS%20Code-Extension-blue?logo=visualstudiocode" alt="VS Code Extension" /></a>
62
+ </p>
63
+
64
+ > **This package is in early development and not yet stable. The API may change without notice between releases.**
65
+
66
+ Modern HTML template rendering in Python. No build step, no template files, and native HTML awareness out of the box. [Read More](https://python-fragments.io)
67
+
68
+ ```python
69
+ from fastapi import APIRouter
70
+
71
+ router = APIRouter()
72
+
73
+ @router.get("/", response_class=HTMLResponse)
74
+ async def index() -> str:
75
+ published = [p for p in POSTS if p.published]
76
+ return <>
77
+ <Layout title="My Blog">
78
+ <h1>Latest Posts</h1>
79
+ <PostCard for={{ post in published }} post={{ post }} />
80
+ </Layout>
81
+ </>
82
+ ```
83
+
84
+ ## IDE Support
85
+
86
+ Type checking, completions, hover docs, go-to-definition, rename, and semantic highlighting. All working inside fragment syntax.
87
+
88
+ Install the [Python Fragments VS Code extension](https://marketplace.visualstudio.com/items?itemName=tra-technologies-ltd.python-fragments) to get started.
89
+
90
+ ![VS Code completions demo](docs/assets/vscode.gif)
91
+
92
+ ## Installation
93
+
94
+ ```bash
95
+ pip install python-fragments
96
+ ```
97
+
98
+ Register the loader at your application's entry point, before importing any modules that contain fragments:
99
+
100
+ ```python
101
+ from fragments import loader # isort: skip
102
+ ```
103
+
104
+ Any `.py` file containing `<>` is transpiled automatically. Nothing else to configure.
105
+
106
+ ## Documentation
107
+
108
+ Full documentation is available at [python-fragments.io](https://python-fragments.io).
109
+
110
+ ## Contributing
111
+
112
+ Bug reports, feature requests, and documentation improvements are all welcome. Code contributions aren't open yet while we work toward v1. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
@@ -127,14 +127,22 @@ def test_for_loop_empty():
127
127
  # ---------------------------------------------------------------------------
128
128
 
129
129
 
130
- def test_component_no_children():
131
- def Badge(children: str, label: str) -> str:
130
+ def test_void_component():
131
+ def Badge(label: str) -> str:
132
132
  return f"<span>{label}</span>"
133
133
 
134
134
  result = render('<><Badge label="hi" /></>', Badge=Badge)
135
135
  assert result == "<span>hi</span>"
136
136
 
137
137
 
138
+ def test_component_no_children():
139
+ def Badge(children: str, label: str) -> str:
140
+ return f"<h1>{label}</h1><span>{children}</span>"
141
+
142
+ result = render('<><Badge label="hi"></Badge></>', Badge=Badge)
143
+ assert result == "<h1>hi</h1><span></span>"
144
+
145
+
138
146
  def test_component_with_children():
139
147
  from fragments.types import Children
140
148
 
@@ -263,7 +263,7 @@ def test_component_self_closing():
263
263
  source, fragment = grammar.expect_fragment(source)
264
264
  assert source.at_end()
265
265
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
266
- ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {}, [])
266
+ ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {}, [], self_closing=True)
267
267
  ]))
268
268
 
269
269
 
@@ -274,7 +274,7 @@ def test_component_with_double_quote_argument():
274
274
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
275
275
  ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {
276
276
  "name": ASTComponentArgument(-1, -1, "name", '"hello"', None)
277
- }, [])
277
+ }, [], self_closing=True)
278
278
  ]))
279
279
 
280
280
 
@@ -285,7 +285,7 @@ def test_component_with_single_quote_argument():
285
285
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
286
286
  ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {
287
287
  "name": ASTComponentArgument(-1, -1, "name", "'hello'", None)
288
- }, [])
288
+ }, [], self_closing=True)
289
289
  ]))
290
290
 
291
291
 
@@ -296,7 +296,7 @@ def test_component_with_interpolation_argument():
296
296
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
297
297
  ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {
298
298
  "value": ASTComponentArgument(-1, -1, "value", None, ASTInterpolation(-1, -1, "expr", 1, 1))
299
- }, [])
299
+ }, [], self_closing=True)
300
300
  ]))
301
301
 
302
302
 
@@ -307,7 +307,7 @@ def test_component_with_children():
307
307
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
308
308
  ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {}, [
309
309
  ASTHTMLElement(-1, -1, "p", {}, [ASTHTMLText(-1, -1, "text")], False)
310
- ])
310
+ ], self_closing=False)
311
311
  ]))
312
312
 
313
313
 
@@ -318,7 +318,7 @@ def test_component_whitespace_stripped_from_children():
318
318
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
319
319
  ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {}, [
320
320
  ASTHTMLElement(-1, -1, "p", {}, [ASTHTMLText(-1, -1, "text")], False)
321
- ])
321
+ ], self_closing=False)
322
322
  ]))
323
323
 
324
324
 
@@ -330,7 +330,7 @@ def test_component_with_if():
330
330
  ASTControlNode(-1, -1,
331
331
  ASTInterpolation(-1, -1, "condition", 1, 1),
332
332
  None,
333
- ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {}, []),
333
+ ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {}, [], self_closing=True),
334
334
  )
335
335
  ]))
336
336
 
@@ -343,7 +343,7 @@ def test_component_with_for():
343
343
  ASTControlNode(-1, -1,
344
344
  None,
345
345
  ASTInterpolation(-1, -1, "item in items", 1, 1),
346
- ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {}, []),
346
+ ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {}, [], self_closing=True),
347
347
  )
348
348
  ]))
349
349
 
@@ -482,7 +482,7 @@ def test_interpolation_no_trailing_whitespace():
482
482
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
483
483
  ASTComponent(-1, -1, ASTComponentName(-1, -1, "NotificationBar"), {
484
484
  "user": ASTComponentArgument(-1, -1, "user", None, ASTInterpolation(-1, -1, "user", 1, 0))
485
- }, [])
485
+ }, [], self_closing=True)
486
486
  ]))
487
487
 
488
488
 
@@ -493,7 +493,7 @@ def test_interpolation_no_leading_whitespace():
493
493
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
494
494
  ASTComponent(-1, -1, ASTComponentName(-1, -1, "NotificationBar"), {
495
495
  "user": ASTComponentArgument(-1, -1, "user", None, ASTInterpolation(-1, -1, "user", 0, 1))
496
- }, [])
496
+ }, [], self_closing=True)
497
497
  ]))
498
498
 
499
499
 
@@ -504,7 +504,7 @@ def test_interpolation_no_whitespace():
504
504
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
505
505
  ASTComponent(-1, -1, ASTComponentName(-1, -1, "NotificationBar"), {
506
506
  "user": ASTComponentArgument(-1, -1, "user", None, ASTInterpolation(-1, -1, "user", 0, 0))
507
- }, [])
507
+ }, [], self_closing=True)
508
508
  ]))
509
509
 
510
510
 
@@ -0,0 +1,55 @@
1
+ from fragments.html.elements import attribute_to_string, className_to_string
2
+
3
+
4
+ def test_attribute_plain_string():
5
+ assert attribute_to_string("href", "/path") == 'href="/path"'
6
+
7
+
8
+ def test_attribute_string_escapes_double_quote():
9
+ assert attribute_to_string("data-value", 'say "hello"') == 'data-value="say &quot;hello&quot;"'
10
+
11
+
12
+ def test_attribute_string_escapes_ampersand():
13
+ assert attribute_to_string("title", "cats & dogs") == 'title="cats &amp; dogs"'
14
+
15
+
16
+ def test_attribute_string_escapes_arrow_in_expression():
17
+ assert attribute_to_string("x-on:click", "items.filter(item => item.active)") == (
18
+ 'x-on:click="items.filter(item =&gt; item.active)"'
19
+ )
20
+
21
+
22
+ def test_attribute_dict_escapes_quotes_from_json():
23
+ assert attribute_to_string("data-config", {"key": "value"}) == 'data-config="{&quot;key&quot;: &quot;value&quot;}"'
24
+
25
+
26
+ def test_attribute_bool_true():
27
+ assert attribute_to_string("disabled", True) == 'disabled="true"'
28
+
29
+
30
+ def test_attribute_bool_false():
31
+ assert attribute_to_string("disabled", False) == 'disabled="false"'
32
+
33
+
34
+ def test_attribute_none_returns_empty():
35
+ assert attribute_to_string("hidden", None) == ""
36
+
37
+
38
+ def test_className_list_joined_with_spaces():
39
+ result = className_to_string(["foo", "bar", "baz"])
40
+ assert result == 'class="foo bar baz"'
41
+
42
+
43
+ def test_className_empty_list():
44
+ result = className_to_string([])
45
+ assert result == 'class=""'
46
+
47
+
48
+ def test_className_single_item_list():
49
+ result = className_to_string(["only"])
50
+ assert result == 'class="only"'
51
+
52
+
53
+ def test_className_string_passthrough():
54
+ result = className_to_string("already-a-string")
55
+ assert result == 'class="already-a-string"'
@@ -1,88 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: python-fragments
3
- Version: 0.33
4
- Summary: Modern HTML template rendering in Python
5
- Author-email: The Running Algorithm <services@therunningalgorithm.info>
6
- License: MIT License
7
-
8
- Copyright (c) 2026 The Running Algorithm
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
28
- Requires-Python: >=3.12
29
- Description-Content-Type: text/markdown
30
- License-File: LICENSE
31
- Provides-Extra: lsp
32
- Requires-Dist: basedpyright>=1.39.0; extra == "lsp"
33
- Requires-Dist: lsprotocol>=2025.0.0; extra == "lsp"
34
- Provides-Extra: dev
35
- Requires-Dist: pytest>=8.4.1; extra == "dev"
36
- Dynamic: license-file
37
-
38
- # Python Fragments
39
-
40
- > **This package is in early development and not yet stable. The API may change without notice between releases.**
41
-
42
- Modern HTML template rendering in Python — no build step, no template files, and native HTML awareness out of the box.
43
-
44
- ```python
45
- from fragments import loader # isort: skip
46
-
47
- from fastapi import FastAPI
48
- from fastapi.responses import HTMLResponse
49
- from components import Layout, PostCard
50
-
51
- app = FastAPI()
52
-
53
- POSTS = [...]
54
-
55
- @app.get("/", response_class=HTMLResponse)
56
- async def index() -> str:
57
- published = [p for p in POSTS if p.published]
58
- return <>
59
- <Layout title="My Blog">
60
- <h1>Latest Posts</h1>
61
- <PostCard for={{ post in published }} post={{ post }} />
62
- </Layout>
63
- </>
64
- ```
65
-
66
- ## IDE Support
67
-
68
- Type checking, completions, hover docs, go-to-definition, rename, and semantic highlighting — all working inside fragment syntax.
69
-
70
- ![VS Code completions demo](docs/assets/vscode.gif)
71
-
72
- ## Installation
73
-
74
- ```bash
75
- pip install python-fragments
76
- ```
77
-
78
- Register the loader at your application's entry point, before importing any modules that contain fragments:
79
-
80
- ```python
81
- from fragments import loader # isort: skip
82
- ```
83
-
84
- Any `.py` file containing `<>` is transpiled automatically. Nothing else to configure.
85
-
86
- ## Feedback and feature requests
87
-
88
- Bug reports and feature requests are welcome via [GitHub Issues](https://github.com/TheRunningAlgorithm2/python-fragments/issues). The project is not currently accepting code contributions.
@@ -1,51 +0,0 @@
1
- # Python Fragments
2
-
3
- > **This package is in early development and not yet stable. The API may change without notice between releases.**
4
-
5
- Modern HTML template rendering in Python — no build step, no template files, and native HTML awareness out of the box.
6
-
7
- ```python
8
- from fragments import loader # isort: skip
9
-
10
- from fastapi import FastAPI
11
- from fastapi.responses import HTMLResponse
12
- from components import Layout, PostCard
13
-
14
- app = FastAPI()
15
-
16
- POSTS = [...]
17
-
18
- @app.get("/", response_class=HTMLResponse)
19
- async def index() -> str:
20
- published = [p for p in POSTS if p.published]
21
- return <>
22
- <Layout title="My Blog">
23
- <h1>Latest Posts</h1>
24
- <PostCard for={{ post in published }} post={{ post }} />
25
- </Layout>
26
- </>
27
- ```
28
-
29
- ## IDE Support
30
-
31
- Type checking, completions, hover docs, go-to-definition, rename, and semantic highlighting — all working inside fragment syntax.
32
-
33
- ![VS Code completions demo](docs/assets/vscode.gif)
34
-
35
- ## Installation
36
-
37
- ```bash
38
- pip install python-fragments
39
- ```
40
-
41
- Register the loader at your application's entry point, before importing any modules that contain fragments:
42
-
43
- ```python
44
- from fragments import loader # isort: skip
45
- ```
46
-
47
- Any `.py` file containing `<>` is transpiled automatically. Nothing else to configure.
48
-
49
- ## Feedback and feature requests
50
-
51
- Bug reports and feature requests are welcome via [GitHub Issues](https://github.com/TheRunningAlgorithm2/python-fragments/issues). The project is not currently accepting code contributions.
@@ -1,88 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: python-fragments
3
- Version: 0.33
4
- Summary: Modern HTML template rendering in Python
5
- Author-email: The Running Algorithm <services@therunningalgorithm.info>
6
- License: MIT License
7
-
8
- Copyright (c) 2026 The Running Algorithm
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
28
- Requires-Python: >=3.12
29
- Description-Content-Type: text/markdown
30
- License-File: LICENSE
31
- Provides-Extra: lsp
32
- Requires-Dist: basedpyright>=1.39.0; extra == "lsp"
33
- Requires-Dist: lsprotocol>=2025.0.0; extra == "lsp"
34
- Provides-Extra: dev
35
- Requires-Dist: pytest>=8.4.1; extra == "dev"
36
- Dynamic: license-file
37
-
38
- # Python Fragments
39
-
40
- > **This package is in early development and not yet stable. The API may change without notice between releases.**
41
-
42
- Modern HTML template rendering in Python — no build step, no template files, and native HTML awareness out of the box.
43
-
44
- ```python
45
- from fragments import loader # isort: skip
46
-
47
- from fastapi import FastAPI
48
- from fastapi.responses import HTMLResponse
49
- from components import Layout, PostCard
50
-
51
- app = FastAPI()
52
-
53
- POSTS = [...]
54
-
55
- @app.get("/", response_class=HTMLResponse)
56
- async def index() -> str:
57
- published = [p for p in POSTS if p.published]
58
- return <>
59
- <Layout title="My Blog">
60
- <h1>Latest Posts</h1>
61
- <PostCard for={{ post in published }} post={{ post }} />
62
- </Layout>
63
- </>
64
- ```
65
-
66
- ## IDE Support
67
-
68
- Type checking, completions, hover docs, go-to-definition, rename, and semantic highlighting — all working inside fragment syntax.
69
-
70
- ![VS Code completions demo](docs/assets/vscode.gif)
71
-
72
- ## Installation
73
-
74
- ```bash
75
- pip install python-fragments
76
- ```
77
-
78
- Register the loader at your application's entry point, before importing any modules that contain fragments:
79
-
80
- ```python
81
- from fragments import loader # isort: skip
82
- ```
83
-
84
- Any `.py` file containing `<>` is transpiled automatically. Nothing else to configure.
85
-
86
- ## Feedback and feature requests
87
-
88
- Bug reports and feature requests are welcome via [GitHub Issues](https://github.com/TheRunningAlgorithm2/python-fragments/issues). The project is not currently accepting code contributions.
@@ -1,21 +0,0 @@
1
- from fragments.html.elements import className_to_string
2
-
3
-
4
- def test_className_list_joined_with_spaces():
5
- result = className_to_string(["foo", "bar", "baz"])
6
- assert result == 'class="foo bar baz"'
7
-
8
-
9
- def test_className_empty_list():
10
- result = className_to_string([])
11
- assert result == 'class=""'
12
-
13
-
14
- def test_className_single_item_list():
15
- result = className_to_string(["only"])
16
- assert result == 'class="only"'
17
-
18
-
19
- def test_className_string_passthrough():
20
- result = className_to_string("already-a-string")
21
- assert result == 'class="already-a-string"'
File without changes