summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-09-03 07:47:36 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-09-03 07:47:36 +0000
commit1f6a9795ed529247bb3370c5efeb009a81d30c8b (patch)
treeb0bb57ba2ba39cb69b7426cbc73632d1d0186c79 /src
parentAdding debian version 0.45+dfsg-1. (diff)
downloadjinjax-1f6a9795ed529247bb3370c5efeb009a81d30c8b.tar.xz
jinjax-1f6a9795ed529247bb3370c5efeb009a81d30c8b.zip
Merging upstream version 0.46.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src')
-rw-r--r--src/jinjax/catalog.py224
1 files changed, 162 insertions, 62 deletions
diff --git a/src/jinjax/catalog.py b/src/jinjax/catalog.py
index c70f81d..d0d0635 100644
--- a/src/jinjax/catalog.py
+++ b/src/jinjax/catalog.py
@@ -1,6 +1,7 @@
import os
import typing as t
from collections import UserString
+from contextvars import ContextVar
from hashlib import sha256
from pathlib import Path
@@ -22,6 +23,10 @@ DEFAULT_EXTENSION = ".jinja"
ARGS_ATTRS = "attrs"
ARGS_CONTENT = "content"
+# Create ContextVars containers at module level
+collected_css: dict[int, ContextVar[list[str]]] = {}
+collected_js: dict[int, ContextVar[list[str]]] = {}
+
class CallerWrapper(UserString):
def __init__(self, caller: t.Callable | None, content: str = "") -> None:
@@ -42,7 +47,6 @@ class CallerWrapper(UserString):
return self()
-
class Catalog:
"""
The object that manages the components and their global settings.
@@ -50,12 +54,12 @@ class Catalog:
Arguments:
globals:
- Dictionary of Jinja globals to add to the Catalog's Jinja environment
- (or the one passed in `jinja_env`).
+ Dictionary of Jinja globals to add to the Catalog's Jinja
+ environment (or the one passed in `jinja_env`).
filters:
- Dictionary of Jinja filters to add to the Catalog's Jinja environment
- (or the one passed in `jinja_env`).
+ Dictionary of Jinja filters to add to the Catalog's Jinja
+ environment (or the one passed in `jinja_env`).
tests:
Dictionary of Jinja tests to add to the Catalog's Jinja environment
@@ -63,34 +67,36 @@ class Catalog:
extensions:
List of Jinja extensions to add to the Catalog's Jinja environment
- (or the one passed in `jinja_env`). The `jinja2.ext.do` extension is
- always added at the end of these.
+ (or the one passed in `jinja_env`). The `jinja2.ext.do` extension
+ is always added at the end of these.
jinja_env:
- Custom Jinja environment to use. This argument is useful to reuse an
- existing Jinja Environment from your web framework.
+ Custom Jinja environment to use. This argument is useful to reuse
+ an existing Jinja Environment from your web framework.
root_url:
- Add this prefix to every asset URL of the static middleware. By default,
- it is `/static/components/`, so, for example, the URL of the CSS file of
- a `Card` component is `/static/components/Card.css`.
+ Add this prefix to every asset URL of the static middleware.
+ By default, it is `/static/components/`, so, for example,
+ the URL of the CSS file of a `Card` component is
+ `/static/components/Card.css`.
- You can also change this argument so the assets are requested from a
- Content Delivery Network (CDN) in production, for example,
+ You can also change this argument so the assets are requested from
+ a Content Delivery Network (CDN) in production, for example,
`root_url="https://example.my-cdn.com/"`.
file_ext:
The extensions the components files have. By default, ".jinja".
- This argument can also be a list to allow more than one type of file to
- be a component.
+ This argument can also be a list to allow more than one type of
+ file to be a component.
use_cache:
Cache the metadata of the component in memory.
auto_reload:
- Used with `use_cache`. If `True`, the last-modified date of the component
- file is checked every time to see if the cache is up-to-date.
+ Used with `use_cache`. If `True`, the last-modified date of the
+ component file is checked every time to see if the cache
+ is up-to-date.
Set to `False` in production.
@@ -98,12 +104,12 @@ class Catalog:
If `True`, inserts a hash of the updated time into the URL of the
asset files (after the name but before the extension).
- This strategy encourages long-term caching while ensuring that new copies
- are only requested when the content changes, as any modification alters the
- fingerprint and thus the filename.
+ This strategy encourages long-term caching while ensuring that
+ new copies are only requested when the content changes, as any
+ modification alters the fingerprint and thus the filename.
- **WARNING**: Only works if the server knows how to filter the fingerprint
- to get the real name of the file.
+ **WARNING**: Only works if the server knows how to filter the
+ fingerprint to get the real name of the file.
Attributes:
@@ -124,12 +130,11 @@ class Catalog:
"file_ext",
"jinja_env",
"fingerprint",
- "collected_css",
- "collected_js",
"auto_reload",
"use_cache",
"_tmpl_globals",
"_cache",
+ "_key",
)
def __init__(
@@ -141,14 +146,14 @@ class Catalog:
extensions: "list | None" = None,
jinja_env: "jinja2.Environment | None" = None,
root_url: str = DEFAULT_URL_ROOT,
- file_ext: "str | tuple[str, ...]" = DEFAULT_EXTENSION,
+ file_ext: "str | list[str] | tuple[str, ...]" = DEFAULT_EXTENSION,
use_cache: bool = True,
auto_reload: bool = True,
fingerprint: bool = False,
) -> None:
self.prefixes: dict[str, jinja2.FileSystemLoader] = {}
- self.collected_css: list[str] = []
- self.collected_js: list[str] = []
+ if isinstance(file_ext, list):
+ file_ext = tuple(file_ext)
self.file_ext = file_ext
self.use_cache = use_cache
self.auto_reload = auto_reload
@@ -186,11 +191,49 @@ class Catalog:
self._tmpl_globals: "t.MutableMapping[str, t.Any] | None" = None
self._cache: dict[str, dict] = {}
+ self._key = id(self)
+
+ def __del__(self) -> None:
+ name = f"collected_css_{self._key}"
+ if name in collected_css:
+ del collected_css[name]
+ name = f"collected_js_{self._key}"
+ if name in collected_js:
+ del collected_js[name]
+
+ @property
+ def collected_css(self) -> list[str]:
+ if self._key not in collected_css:
+ name = f"collected_css_{self._key}"
+ collected_css[self._key] = ContextVar(name, default=[])
+ return collected_css[self._key].get()
+
+ @collected_css.setter
+ def collected_css(self, value: list[str]) -> None:
+ if self._key not in collected_css:
+ name = f"collected_css_{self._key}"
+ collected_css[self._key] = ContextVar(name, default=[])
+ collected_css[self._key].set(list(value))
+
+ @property
+ def collected_js(self) -> list[str]:
+ if self._key not in collected_js:
+ name = f"collected_js_{self._key}"
+ collected_js[self._key] = ContextVar(name, default=[])
+ return collected_js[self._key].get()
+
+ @collected_js.setter
+ def collected_js(self, value: list[str]) -> None:
+ if self._key not in collected_js:
+ name = f"collected_js_{self._key}"
+ collected_js[self._key] = ContextVar(name, default=[])
+ collected_js[self._key].set(list(value))
@property
def paths(self) -> list[Path]:
"""
- A helper property that returns a list of all the components folder paths.
+ A helper property that returns a list of all the components
+ folder paths.
"""
_paths = []
for loader in self.prefixes.values():
@@ -204,15 +247,17 @@ class Catalog:
prefix: str = DEFAULT_PREFIX,
) -> None:
"""
- Add a folder path from where to search for components, optionally under a prefix.
+ Add a folder path from where to search for components, optionally
+ under a prefix.
The prefix acts like a namespace. For example, the name of a
`components/Card.jinja` component is, by default, "Card",
but under the prefix "common", it becomes "common.Card".
- The rule for subfolders remains the same: a `components/wrappers/Card.jinja`
- name is, by default, "wrappers.Card", but under the prefix "common",
- it becomes "common.wrappers.Card".
+ The rule for subfolders remains the same: a
+ `components/wrappers/Card.jinja` name is, by default,
+ "wrappers.Card", but under the prefix "common", it
+ becomes "common.wrappers.Card".
If there is more than one component with the same name in multiple
added folders under the same prefix, the one in the folder added
@@ -228,26 +273,35 @@ class Catalog:
have. The default is empty.
"""
- prefix = prefix.strip().strip(f"{DELIMITER}{SLASH}").replace(SLASH, DELIMITER)
+ prefix = (
+ prefix.strip()
+ .strip(f"{DELIMITER}{SLASH}")
+ .replace(SLASH, DELIMITER)
+ )
root_path = str(root_path)
if prefix in self.prefixes:
loader = self.prefixes[prefix]
if root_path in loader.searchpath:
return
- logger.debug(f"Adding folder `{root_path}` with the prefix `{prefix}`")
+ logger.debug(
+ f"Adding folder `{root_path}` with the prefix `{prefix}`"
+ )
loader.searchpath.append(root_path)
else:
- logger.debug(f"Adding folder `{root_path}` with the prefix `{prefix}`")
+ logger.debug(
+ f"Adding folder `{root_path}` with the prefix `{prefix}`"
+ )
self.prefixes[prefix] = jinja2.FileSystemLoader(root_path)
def add_module(self, module: t.Any, *, prefix: str | None = None) -> None:
"""
- Reads an absolute path from `module.components_path` and an optional prefix
- from `module.prefix`, then calls `Catalog.add_folder(path, prefix)`.
+ Reads an absolute path from `module.components_path` and an optional
+ prefix from `module.prefix`, then calls
+ `Catalog.add_folder(path, prefix)`.
- The prefix can also be passed as an argument instead of being read from
- the module.
+ The prefix can also be passed as an argument instead of being read
+ from the module.
This method exists to make it easy and consistent to have
components installable as Python libraries.
@@ -263,7 +317,9 @@ class Catalog:
"""
mprefix = (
- prefix if prefix is not None else getattr(module, "prefix", DEFAULT_PREFIX)
+ prefix
+ if prefix is not None
+ else getattr(module, "prefix", DEFAULT_PREFIX)
)
self.add_folder(module.components_path, prefix=mprefix)
@@ -314,16 +370,25 @@ class Catalog:
if source:
logger.debug("Rendering from source %s", __name)
- component = self._get_from_source(name=name, prefix=prefix, source=source)
+ component = self._get_from_source(
+ name=name, prefix=prefix, source=source
+ )
elif self.use_cache:
logger.debug("Rendering from cache or file %s", __name)
- component = self._get_from_cache(prefix=prefix, name=name, file_ext=file_ext)
+ component = self._get_from_cache(
+ prefix=prefix, name=name, file_ext=file_ext
+ )
else:
logger.debug("Rendering from file %s", __name)
- component = self._get_from_file(prefix=prefix, name=name, file_ext=file_ext)
+ component = self._get_from_file(
+ prefix=prefix, name=name, file_ext=file_ext
+ )
root_path = component.path.parent if component.path else None
+ css = self.collected_css
+ js = self.collected_js
+
for url in component.css:
if (
root_path
@@ -332,8 +397,8 @@ class Catalog:
):
url = self._fingerprint(root_path, url)
- if url not in self.collected_css:
- self.collected_css.append(url)
+ if url not in css:
+ css.append(url)
for url in component.js:
if (
@@ -343,8 +408,8 @@ class Catalog:
):
url = self._fingerprint(root_path, url)
- if url not in self.collected_js:
- self.collected_js.append(url)
+ if url not in js:
+ js.append(url)
attrs = attrs.as_dict if isinstance(attrs, HTMLAttrs) else attrs
attrs.update(kw)
@@ -368,7 +433,8 @@ class Catalog:
**kwargs,
) -> ComponentsMiddleware:
"""
- Wraps you application with [Withenoise](https://whitenoise.readthedocs.io/),
+ Wraps you application with
+ [Withenoise](https://whitenoise.readthedocs.io/),
a static file serving middleware.
Tecnically not neccesary if your components doesn't use static assets
@@ -380,13 +446,15 @@ class Catalog:
A WSGI application
allowed_ext:
- A list of file extensions the static middleware is allowed to read
- and return. By default, is just ".css", ".js", and ".mjs".
+ A list of file extensions the static middleware is allowed to
+ read and return. By default, is just ".css", ".js", and ".mjs".
"""
logger.debug("Creating middleware")
middleware = ComponentsMiddleware(
- application=application, allowed_ext=tuple(allowed_ext or []), **kwargs
+ application=application,
+ allowed_ext=tuple(allowed_ext or []),
+ **kwargs,
)
for prefix, loader in self.prefixes.items():
url_prefix = get_url_prefix(prefix)
@@ -396,7 +464,11 @@ class Catalog:
return middleware
- def get_source(self, cname: str, file_ext: "tuple[str, ...] | str" = "") -> str:
+ def get_source(
+ self,
+ cname: str,
+ file_ext: "tuple[str, ...] | str" = "",
+ ) -> str:
"""
A helper method that returns the source file of a component.
"""
@@ -445,12 +517,26 @@ class Catalog:
return f"{parent}{stem}-{fingerprint}{ext}"
- def _get_from_source(self, *, name: str, prefix: str, source: str) -> Component:
+ def _get_from_source(
+ self,
+ *,
+ name: str,
+ prefix: str,
+ source: str,
+ ) -> Component:
tmpl = self.jinja_env.from_string(source, globals=self._tmpl_globals)
- component = Component(name=name, prefix=prefix, source=source, tmpl=tmpl)
+ component = Component(
+ name=name, prefix=prefix, source=source, tmpl=tmpl
+ )
return component
- def _get_from_cache(self, *, prefix: str, name: str, file_ext: str) -> Component:
+ def _get_from_cache(
+ self,
+ *,
+ prefix: str,
+ name: str,
+ file_ext: str,
+ ) -> Component:
key = f"{prefix}.{name}.{file_ext}"
cache = self._from_cache(key)
if cache:
@@ -461,7 +547,9 @@ class Catalog:
return component
logger.debug("Loading %s", key)
- component = self._get_from_file(prefix=prefix, name=name, file_ext=file_ext)
+ component = self._get_from_file(
+ prefix=prefix, name=name, file_ext=file_ext
+ )
self._to_cache(key, component)
return component
@@ -475,10 +563,16 @@ class Catalog:
def _to_cache(self, key: str, component: Component) -> None:
self._cache[key] = component.serialize()
- def _get_from_file(self, *, prefix: str, name: str, file_ext: str) -> Component:
- path, tmpl_name = self._get_component_path(prefix, name, file_ext=file_ext)
+ def _get_from_file(
+ self, *, prefix: str, name: str, file_ext: str
+ ) -> Component:
+ path, tmpl_name = self._get_component_path(
+ prefix, name, file_ext=file_ext
+ )
component = Component(name=name, prefix=prefix, path=path)
- component.tmpl = self.jinja_env.get_template(tmpl_name, globals=self._tmpl_globals)
+ component.tmpl = self.jinja_env.get_template(
+ tmpl_name, globals=self._tmpl_globals
+ )
return component
def _split_name(self, cname: str) -> tuple[str, str]:
@@ -492,7 +586,10 @@ class Catalog:
return DEFAULT_PREFIX, cname
def _get_component_path(
- self, prefix: str, name: str, file_ext: "tuple[str, ...] | str" = ""
+ self,
+ prefix: str,
+ name: str,
+ file_ext: "tuple[str, ...] | str" = "",
) -> tuple[Path, str]:
name = name.replace(DELIMITER, SLASH)
root_paths = self.prefixes[prefix].searchpath
@@ -512,7 +609,10 @@ class Catalog:
filepath = f"{relfolder}/{filename}"
else:
filepath = filename
- if filepath.startswith(name_dot) and filepath.endswith(file_ext):
+ if (
+ filepath.startswith(name_dot) and
+ filepath.endswith(file_ext)
+ ):
return Path(curr_folder) / filename, filepath
raise ComponentNotFound(