diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-09-03 07:47:36 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-09-03 07:47:36 +0000 |
commit | 1f6a9795ed529247bb3370c5efeb009a81d30c8b (patch) | |
tree | b0bb57ba2ba39cb69b7426cbc73632d1d0186c79 /src | |
parent | Adding debian version 0.45+dfsg-1. (diff) | |
download | jinjax-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.py | 224 |
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( |