citry-django 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- citry_django-0.1.0/.gitignore +11 -0
- citry_django-0.1.0/LICENSE +21 -0
- citry_django-0.1.0/PKG-INFO +71 -0
- citry_django-0.1.0/README.md +35 -0
- citry_django-0.1.0/pyproject.toml +45 -0
- citry_django-0.1.0/src/citry_django/__init__.py +23 -0
- citry_django-0.1.0/src/citry_django/backend.py +75 -0
- citry_django-0.1.0/src/citry_django/expressions.py +86 -0
- citry_django-0.1.0/src/citry_django/extension.py +571 -0
- citry_django-0.1.0/src/citry_django/nodes.py +128 -0
- citry_django-0.1.0/src/citry_django/py.typed +0 -0
- citry_django-0.1.0/src/citry_django/registry.py +64 -0
- citry_django-0.1.0/src/citry_django/rewrite.py +255 -0
- citry_django-0.1.0/src/citry_django/templatetags/__init__.py +0 -0
- citry_django-0.1.0/src/citry_django/templatetags/citry.py +30 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joey Jurjens
|
|
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,71 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: citry-django
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Citry and Django's template engines, composed: real Django tags and filters inside Citry components, and Citry components inside Django templates.
|
|
5
|
+
Project-URL: Repository, https://github.com/joeyjurjens/citry-django
|
|
6
|
+
Project-URL: Issues, https://github.com/joeyjurjens/citry-django/issues
|
|
7
|
+
Author-email: Joey Jurjens <joeyjurjens@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: citry,components,django,templates
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Web Environment
|
|
13
|
+
Classifier: Framework :: Django
|
|
14
|
+
Classifier: Framework :: Django :: 5.2
|
|
15
|
+
Classifier: Framework :: Django :: 6.0
|
|
16
|
+
Classifier: Framework :: Django :: 6.1
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.10
|
|
29
|
+
Requires-Dist: citry>=0.4.3
|
|
30
|
+
Requires-Dist: django>=5.2
|
|
31
|
+
Provides-Extra: compressor
|
|
32
|
+
Requires-Dist: citry-django-compressor; extra == 'compressor'
|
|
33
|
+
Provides-Extra: django-components
|
|
34
|
+
Requires-Dist: citry-django-djc; extra == 'django-components'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# citry-django
|
|
38
|
+
|
|
39
|
+
Citry and Django's template engines, composed.
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from citry import Citry
|
|
43
|
+
from citry_django import CitryDjangoExtension
|
|
44
|
+
|
|
45
|
+
app = Citry(extensions=[CitryDjangoExtension()])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
# settings.py
|
|
50
|
+
TEMPLATES = [{"BACKEND": "citry_django.backend.CitryTemplates", "APP_DIRS": True, ...}]
|
|
51
|
+
CITRY_APP = "myproject.app:app"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Any `{% tag %}` you can `{% load %}` in Django then works inside a Citry
|
|
55
|
+
component, and Citry components work in your existing Django templates. Neither
|
|
56
|
+
syntax changes, and the adapter names no tag: each engine's own parser decides
|
|
57
|
+
what belongs to it.
|
|
58
|
+
|
|
59
|
+
See the [repository README](https://github.com/joeyjurjens/citry-django) for the
|
|
60
|
+
full picture, the limits, and how it works.
|
|
61
|
+
|
|
62
|
+
## Optional extras
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install citry-django[compressor]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Routes each component's CSS and JS through [django-compressor], so assets are
|
|
69
|
+
preprocessed (SCSS, Less, ...) and minified. See `citry-django-compressor`.
|
|
70
|
+
|
|
71
|
+
[django-compressor]: https://django-compressor.readthedocs.io/
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# citry-django
|
|
2
|
+
|
|
3
|
+
Citry and Django's template engines, composed.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from citry import Citry
|
|
7
|
+
from citry_django import CitryDjangoExtension
|
|
8
|
+
|
|
9
|
+
app = Citry(extensions=[CitryDjangoExtension()])
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
# settings.py
|
|
14
|
+
TEMPLATES = [{"BACKEND": "citry_django.backend.CitryTemplates", "APP_DIRS": True, ...}]
|
|
15
|
+
CITRY_APP = "myproject.app:app"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Any `{% tag %}` you can `{% load %}` in Django then works inside a Citry
|
|
19
|
+
component, and Citry components work in your existing Django templates. Neither
|
|
20
|
+
syntax changes, and the adapter names no tag: each engine's own parser decides
|
|
21
|
+
what belongs to it.
|
|
22
|
+
|
|
23
|
+
See the [repository README](https://github.com/joeyjurjens/citry-django) for the
|
|
24
|
+
full picture, the limits, and how it works.
|
|
25
|
+
|
|
26
|
+
## Optional extras
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install citry-django[compressor]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Routes each component's CSS and JS through [django-compressor], so assets are
|
|
33
|
+
preprocessed (SCSS, Less, ...) and minified. See `citry-django-compressor`.
|
|
34
|
+
|
|
35
|
+
[django-compressor]: https://django-compressor.readthedocs.io/
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "citry-django"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Citry and Django's template engines, composed: real Django tags and filters inside Citry components, and Citry components inside Django templates."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Joey Jurjens", email = "joeyjurjens@gmail.com" }]
|
|
14
|
+
keywords = ["citry", "django", "templates", "components"]
|
|
15
|
+
dependencies = ["citry>=0.4.3", "django>=5.2"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Web Environment",
|
|
19
|
+
"Framework :: Django",
|
|
20
|
+
"Framework :: Django :: 5.2",
|
|
21
|
+
"Framework :: Django :: 6.0",
|
|
22
|
+
"Framework :: Django :: 6.1",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"License :: OSI Approved :: MIT License",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Programming Language :: Python :: 3.14",
|
|
32
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
33
|
+
"Typing :: Typed",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
django-components = ["citry-django-djc"]
|
|
38
|
+
compressor = ["citry-django-compressor"]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Repository = "https://github.com/joeyjurjens/citry-django"
|
|
42
|
+
Issues = "https://github.com/joeyjurjens/citry-django/issues"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/citry_django"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
citry_django -- Citry and Django's template engines, composed.
|
|
3
|
+
|
|
4
|
+
Each engine owns its own files and delegates the other's syntax to the other's
|
|
5
|
+
*real* engine, so both keep their full feature sets::
|
|
6
|
+
|
|
7
|
+
from citry import Citry
|
|
8
|
+
from citry_django import CitryDjangoExtension
|
|
9
|
+
|
|
10
|
+
app = Citry(extensions=[CitryDjangoExtension()])
|
|
11
|
+
|
|
12
|
+
Any ``{% tag %}`` you can ``{% load %}`` in Django then works inside a Citry
|
|
13
|
+
component, with no per-tag code. For the reverse direction -- Citry syntax in
|
|
14
|
+
your existing Django templates -- point ``settings.TEMPLATES`` at
|
|
15
|
+
``citry_django.backend.CitryTemplates`` and set ``CITRY_APP``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .extension import CitryDjangoExtension, ForeignNode
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"CitryDjangoExtension",
|
|
22
|
+
"ForeignNode",
|
|
23
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A Django template backend that understands Citry element syntax.
|
|
3
|
+
|
|
4
|
+
Swap ``BACKEND`` in ``settings.TEMPLATES`` and ``<c-hero/>`` works in every
|
|
5
|
+
Django template::
|
|
6
|
+
|
|
7
|
+
TEMPLATES = [{
|
|
8
|
+
"BACKEND": "citry_django.backend.CitryTemplates",
|
|
9
|
+
"DIRS": [...],
|
|
10
|
+
"APP_DIRS": True,
|
|
11
|
+
"OPTIONS": {"context_processors": [...]},
|
|
12
|
+
}]
|
|
13
|
+
|
|
14
|
+
This is still Django's own engine: same lexer, same tags, same inheritance. The
|
|
15
|
+
only difference is that source is rewritten as it is read, so ``<c-component/>``
|
|
16
|
+
becomes a tag Django can compile. A template with no Citry syntax in it is
|
|
17
|
+
returned unchanged.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from django.template.backends.django import DjangoTemplates
|
|
25
|
+
|
|
26
|
+
from .rewrite import rewrite_source
|
|
27
|
+
|
|
28
|
+
_TAGS = "citry_django.templatetags.citry"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _RewriteMixin:
|
|
32
|
+
"""Rewrites Citry element syntax as a loader reads a template."""
|
|
33
|
+
|
|
34
|
+
def get_contents(self, origin: Any) -> str:
|
|
35
|
+
return rewrite_source(super().get_contents(origin), origin=origin.name)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _enable_rewriting(loaders: Any) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Teach every configured loader to rewrite, whoever wrote it.
|
|
41
|
+
|
|
42
|
+
Django resolves ``get_contents`` on the loader that finally reads the file,
|
|
43
|
+
so rewriting is added there rather than by substituting loader classes. A
|
|
44
|
+
project that configures its own ``loaders`` -- for a component library, a
|
|
45
|
+
pattern library, or just to control caching -- keeps them and gets Citry
|
|
46
|
+
syntax in the templates they load.
|
|
47
|
+
"""
|
|
48
|
+
for loader in loaders:
|
|
49
|
+
nested = getattr(loader, "loaders", None)
|
|
50
|
+
if nested:
|
|
51
|
+
_enable_rewriting(nested)
|
|
52
|
+
elif not isinstance(loader, _RewriteMixin):
|
|
53
|
+
cls = type(loader)
|
|
54
|
+
loader.__class__ = type(f"Rewriting{cls.__name__}", (_RewriteMixin, cls), {})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CitryTemplates(DjangoTemplates):
|
|
58
|
+
def __init__(self, params: dict) -> None:
|
|
59
|
+
params = params.copy()
|
|
60
|
+
options = params.setdefault("OPTIONS", {}).copy()
|
|
61
|
+
params["OPTIONS"] = options
|
|
62
|
+
|
|
63
|
+
# The rewriter injects its tag into templates that have no `{% load %}`
|
|
64
|
+
# line of their own, so it has to resolve as a builtin.
|
|
65
|
+
builtins = list(options.get("builtins", []))
|
|
66
|
+
if _TAGS not in builtins:
|
|
67
|
+
builtins.append(_TAGS)
|
|
68
|
+
options["builtins"] = builtins
|
|
69
|
+
|
|
70
|
+
super().__init__(params)
|
|
71
|
+
_enable_rewriting(self.engine.template_loaders)
|
|
72
|
+
|
|
73
|
+
def from_string(self, template_code: str) -> Any:
|
|
74
|
+
# `from_string` never reaches a loader, so it rewrites here as well.
|
|
75
|
+
return super().from_string(rewrite_source(template_code, origin="<string template>"))
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Deciding who owns a ``{{ ... }}`` inside a Citry template.
|
|
3
|
+
|
|
4
|
+
Both engines spell interpolation the same way, so the decision is made per
|
|
5
|
+
expression, at compile time:
|
|
6
|
+
|
|
7
|
+
1. Not a valid Python expression -> Django (``{{ x|date:"Y-m-d" }}``).
|
|
8
|
+
2. A plain dotted path -> Django, whose lookup is a superset of Python's
|
|
9
|
+
attribute access: dictionary, then attribute, then index. ``{{ d.key }}``
|
|
10
|
+
on a dict is why this matters.
|
|
11
|
+
3. A filter chain whose filter names Django's live registry knows -> Django.
|
|
12
|
+
4. Otherwise -> Citry.
|
|
13
|
+
|
|
14
|
+
Rule 3 is the only genuine overlap: ``a|b`` is both a filter application and a
|
|
15
|
+
bitwise or, resolved by asking the registry the template's own ``{% load %}``
|
|
16
|
+
lines populate. A Python variable named after a registered filter is therefore
|
|
17
|
+
read as the filter.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import ast
|
|
23
|
+
from functools import cache
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def is_django_expression(expression: str, engine, extra_libraries: tuple[str, ...] = ()) -> bool:
|
|
27
|
+
"""Whether ``{{ expression }}`` should be handed to Django."""
|
|
28
|
+
text = expression.strip()
|
|
29
|
+
if not text:
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
parsed = ast.parse(text, mode="eval")
|
|
34
|
+
except SyntaxError:
|
|
35
|
+
# Not Python at all: filter arguments (`|date:"Y-m-d"`), translation
|
|
36
|
+
# syntax, and so on.
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
node = parsed.body
|
|
40
|
+
|
|
41
|
+
# A plain dotted path is Django's, because Django's lookup is a superset of
|
|
42
|
+
# Python's attribute access there: it tries dictionary, then attribute,
|
|
43
|
+
# then index. `{{ d.key }}` on a dict is the case that matters -- Python
|
|
44
|
+
# attribute access fails on it, Django resolves it, and every Django
|
|
45
|
+
# template in existence writes it that way.
|
|
46
|
+
if _is_dotted_path(node):
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
if not (isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr)):
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
known = _filter_names(engine, extra_libraries)
|
|
53
|
+
while isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
|
|
54
|
+
if not (isinstance(node.right, ast.Name) and node.right.id in known):
|
|
55
|
+
return False
|
|
56
|
+
node = node.left
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_dotted_path(node: ast.AST) -> bool:
|
|
61
|
+
"""
|
|
62
|
+
Whether the expression is only names joined by dots, e.g. ``a.b.c``.
|
|
63
|
+
|
|
64
|
+
A bare name is excluded: both engines resolve one identically, and leaving
|
|
65
|
+
it with Citry keeps Citry's strictness about unknown names. Anything with a
|
|
66
|
+
call, subscript or operator stays Citry's too, since Django cannot express
|
|
67
|
+
those at all.
|
|
68
|
+
"""
|
|
69
|
+
if not isinstance(node, ast.Attribute):
|
|
70
|
+
return False
|
|
71
|
+
while isinstance(node, ast.Attribute):
|
|
72
|
+
node = node.value
|
|
73
|
+
return isinstance(node, ast.Name)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@cache
|
|
77
|
+
def _filter_names(engine, extra_libraries: tuple[str, ...]) -> frozenset[str]:
|
|
78
|
+
"""Every filter name in scope: the engine's builtins plus loaded libraries."""
|
|
79
|
+
names: set[str] = set()
|
|
80
|
+
for library in engine.template_builtins:
|
|
81
|
+
names.update(library.filters)
|
|
82
|
+
for label in extra_libraries:
|
|
83
|
+
library = engine.template_libraries.get(label)
|
|
84
|
+
if library is not None:
|
|
85
|
+
names.update(library.filters)
|
|
86
|
+
return frozenset(names)
|