Skip to content

Environment API

The QuartAssets class is the main entry point for integrating webassets with your Quart application.

QuartAssets

quart_assets.QuartAssets

Bases: BaseEnvironment

This object is used to hold a collection of bundles and configuration.

If initialized with a Quart app instance then a webassets Jinja2 extension is automatically registered.

Source code in src/quart_assets/extension.py
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
class QuartAssets(BaseEnvironment):
    """This object is used to hold a collection of bundles and configuration.

    If initialized with a Quart app instance then a webassets Jinja2 extension
    is automatically registered.
    """

    config_storage_class: Any = QuartConfigStorage
    resolver_class = QuartResolver

    def __init__(self, app: Quart | None = None) -> None:
        self.app = app
        super().__init__()
        if app:
            self.init_app(app)

    @property
    def _app(self) -> Quart:
        """The application object; this is either the app that has been bound
        to, or the current application.
        """
        if self.app is not None:
            return self.app

        if has_request_context():
            return request_ctx.app

        if has_app_context():
            return app_ctx.app

        raise RuntimeError(
            "Assets instance not bound to an application, "
            + "and no application in current context"
        )

    @property
    def directory(self) -> str:
        """The base directory to which all paths will be relative."""
        if self.config.get("directory") is not None:
            return self.config["directory"]
        return get_static_folder(self._app)

    @directory.setter
    def directory(self, value: str) -> None:
        self.config["directory"] = value

    @property
    def url(self) -> str | None:
        """The base url to which all static urls will be relative."""
        if self.config.get("url") is not None:
            return self.config["url"]
        return self._app.static_url_path

    @url.setter
    def url(self, value: str) -> None:
        self.config["url"] = value

    def init_app(self, app: Quart) -> None:
        # Use our custom async-aware extension instead of the default webassets
        # extension
        app.jinja_env.add_extension(AsyncAssetsExtension)
        app.jinja_env.assets_environment = self  # ty: ignore[unresolved-attribute]

    def from_yaml(self, path: str) -> None:
        """Register bundles from a YAML configuration file."""
        self.register(YAMLLoader(path).load_bundles())

    def from_module(self, path: str | ModuleType) -> None:
        """Register bundles from a Python module."""
        self.register(PythonLoader(path).load_bundles())

directory property writable

The base directory to which all paths will be relative.

url property writable

The base url to which all static urls will be relative.

from_module(path)

Register bundles from a Python module.

Source code in src/quart_assets/extension.py
351
352
353
def from_module(self, path: str | ModuleType) -> None:
    """Register bundles from a Python module."""
    self.register(PythonLoader(path).load_bundles())

from_yaml(path)

Register bundles from a YAML configuration file.

Source code in src/quart_assets/extension.py
347
348
349
def from_yaml(self, path: str) -> None:
    """Register bundles from a YAML configuration file."""
    self.register(YAMLLoader(path).load_bundles())

Configuration Storage

The configuration storage classes handle how webassets configuration is stored and accessed within Quart applications.

QuartConfigStorage

quart_assets.extension.QuartConfigStorage

Bases: ConfigStorage

Uses the config object of a Quart app as the backend: either the app instance bound to the extension directly, or the current Quart app on the stack. Also provides per-application defaults for some values.

Source code in src/quart_assets/extension.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
class QuartConfigStorage(ConfigStorage):
    """Uses the config object of a Quart app as the backend: either the app
    instance bound to the extension directly, or the current Quart app on
    the stack. Also provides per-application defaults for some values.
    """

    def __init__(self, *a: Any, **kw: Any) -> None:
        self._defaults: dict[str, Any] = {}
        ConfigStorage.__init__(self, *a, **kw)

    def _transform_key(self, key: str) -> str:
        if key.lower() in env_options:
            return f"ASSETS_{key.upper()}"

        return key.upper()

    def setdefault(self, key: str, value: Any) -> None:
        """We may not always be connected to an app, but we still need
        to provide a way to the base environment to set its defaults.
        """
        try:
            super().setdefault(key, value)
        except RuntimeError:
            self._defaults[key] = value

    def __contains__(self, key: str) -> bool:
        return self._transform_key(key) in self.env._app.config

    def __getitem__(self, key: str) -> Any:
        value = self._get_deprecated(key)
        if value is not None:
            return value

        # First try the current app's config
        public_key = self._transform_key(key)
        if self.env._app:
            if public_key in self.env._app.config:
                return self.env._app.config[public_key]

        # Try a non-app specific default value
        if key in self._defaults:
            return self._defaults.__getitem__(key)

        # We've run out of options
        raise KeyError()

    def __setitem__(self, key: str, value: Any) -> None:
        if not self._set_deprecated(key, value):
            self.env._app.config[self._transform_key(key)] = value

    def __delitem__(self, key: str) -> None:
        del self.env._app.config[self._transform_key(key)]
setdefault(key, value)

We may not always be connected to an app, but we still need to provide a way to the base environment to set its defaults.

Source code in src/quart_assets/extension.py
119
120
121
122
123
124
125
126
def setdefault(self, key: str, value: Any) -> None:
    """We may not always be connected to an app, but we still need
    to provide a way to the base environment to set its defaults.
    """
    try:
        super().setdefault(key, value)
    except RuntimeError:
        self._defaults[key] = value

Resolvers

Resolvers handle how asset files are located and how URLs are generated for them.

QuartResolver

quart_assets.extension.QuartResolver

Bases: Resolver

Adds support for Quart blueprints.

This resolver is designed to use the Quart staticfile system to locate files, by looking at directory prefixes. (foo/bar.png looks in the static folder of the foo blueprint. url_for is used to generate urls to these files.)

This default behaviour changes when you start setting certain standard webassets path and url configuration values:

If a :attr:Environment.directory is set, output files will always be written there, while source files still use the Quart system.

If a :attr:Environment.load_path is set, it is used to look up source files, replacing the Quart system. Blueprint prefixes are no longer resolved.

Source code in src/quart_assets/extension.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
class QuartResolver(Resolver):
    """Adds support for Quart blueprints.

    This resolver is designed to use the Quart staticfile system to
    locate files, by looking at directory prefixes. (``foo/bar.png``
    looks in the static folder of the ``foo`` blueprint. ``url_for``
    is used to generate urls to these files.)

    This default behaviour changes when you start setting certain
    standard *webassets* path and url configuration values:

    If a :attr:`Environment.directory` is set, output files will
    always be written there, while source files still use the Quart
    system.

    If a :attr:`Environment.load_path` is set, it is used to look
    up source files, replacing the Quart system. Blueprint prefixes
    are no longer resolved.
    """

    def split_prefix(self, ctx: Any, item: str) -> tuple[str, str, str]:
        """Split a blueprint-prefixed asset path.

        Returns ``(directory, relative_path, endpoint)``. If ``item`` has a
        ``blueprint_name/...`` prefix that matches a registered blueprint with
        a static folder, the blueprint's static folder and endpoint are used.
        Otherwise the app's static folder is used and ``item`` is returned
        unchanged.

        Raises:
            ValueError: If ``item`` is empty or no app context is available.
            TypeError: If the matched blueprint, or the app, has no static folder.
        """
        if not item:
            raise ValueError("Asset item cannot be empty")

        app = ctx._app
        if app is None:
            raise ValueError("No app context available")

        if "/" in item:
            blueprint_name, name = item.split("/", 1)
            if blueprint_name and name and blueprint_name in app.blueprints:
                try:
                    directory = get_static_folder(app.blueprints[blueprint_name])
                except TypeError as e:
                    raise TypeError(f"Blueprint '{blueprint_name}' has no static folder") from e
                return directory, name, f"{blueprint_name}.static"

        try:
            directory = get_static_folder(app)
        except TypeError as e:
            raise TypeError("App has no static folder configured") from e
        return directory, item, "static"

    def use_webassets_system_for_output(self, ctx: Any) -> bool:
        return ctx.config.get("directory") is not None or ctx.config.get("url") is not None

    def use_webassets_system_for_sources(self, ctx: Any) -> bool:
        return bool(ctx.load_path)

    def search_for_source(self, ctx: Any, item: str) -> Any:
        if self.use_webassets_system_for_sources(ctx):
            return Resolver.search_for_source(self, ctx, item)

        directory, item, _ = self.split_prefix(ctx, item)
        try:
            return self.consider_single_directory(directory, item)
        except IOError:
            # Return the would-be path so webassets can report a useful
            # "missing source" error later instead of an opaque IOError here.
            return path.normpath(path.join(directory, item))

    def resolve_output_to_path(self, ctx: Any, target: str, bundle: Any) -> Any:
        if self.use_webassets_system_for_output(ctx):
            return Resolver.resolve_output_to_path(self, ctx, target, bundle)

        directory, rel_path, _ = self.split_prefix(ctx, target)
        return path.normpath(path.join(directory, rel_path))

    def resolve_source_to_url(self, ctx: Any, filepath: str, item: str) -> str:
        if self.use_webassets_system_for_sources(ctx):
            return super().resolve_source_to_url(ctx, filepath, item)
        return self.convert_item_to_quart_url(ctx, item, filepath)

    def resolve_output_to_url(self, ctx: Any, target: str) -> str:
        if self.use_webassets_system_for_output(ctx):
            return Resolver.resolve_output_to_url(self, ctx, target)
        return self.convert_item_to_quart_url(ctx, target)

    def convert_item_to_quart_url(self, ctx: Any, item: str, filepath: str | None = None) -> str:
        """Build a Quart URL for an asset reference.

        Resolves blueprint prefixes via :meth:`split_prefix` and asks the
        app's URL map to construct the URL for the matching static endpoint.
        Works both inside a request context (via :func:`quart.url_for`) and
        outside one (e.g. during ``quart assets build``), correctly honouring
        blueprint ``static_url_path`` values and any custom URL routing.

        If ``filepath`` is provided it overrides the relative path returned by
        :meth:`split_prefix`; this is needed when ``item`` is a glob that was
        resolved to multiple files.
        """
        directory, rel_path, endpoint = self.split_prefix(ctx, item)

        if filepath is not None:
            filename = path.relpath(filepath, directory)
        else:
            filename = rel_path
        filename = filename.replace("\\", "/")

        if has_request_context():
            url = url_for(endpoint, filename=filename)
        else:
            # `url_for` outside a request context needs SERVER_NAME; bind the
            # adapter directly so the URL map still resolves the static rules.
            app = ctx.environment._app
            server_name = app.config.get("SERVER_NAME") or ""
            url_adapter = app.url_map.bind(server_name, url_scheme="http")
            url = url_adapter.build(endpoint, {"filename": filename}, force_external=False)

        # Scheme is unknown during build; emit scheme-relative URLs.
        if url:
            url = url.removeprefix("http:")
        return url
convert_item_to_quart_url(ctx, item, filepath=None)

Build a Quart URL for an asset reference.

Resolves blueprint prefixes via :meth:split_prefix and asks the app's URL map to construct the URL for the matching static endpoint. Works both inside a request context (via :func:quart.url_for) and outside one (e.g. during quart assets build), correctly honouring blueprint static_url_path values and any custom URL routing.

If filepath is provided it overrides the relative path returned by :meth:split_prefix; this is needed when item is a glob that was resolved to multiple files.

Source code in src/quart_assets/extension.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def convert_item_to_quart_url(self, ctx: Any, item: str, filepath: str | None = None) -> str:
    """Build a Quart URL for an asset reference.

    Resolves blueprint prefixes via :meth:`split_prefix` and asks the
    app's URL map to construct the URL for the matching static endpoint.
    Works both inside a request context (via :func:`quart.url_for`) and
    outside one (e.g. during ``quart assets build``), correctly honouring
    blueprint ``static_url_path`` values and any custom URL routing.

    If ``filepath`` is provided it overrides the relative path returned by
    :meth:`split_prefix`; this is needed when ``item`` is a glob that was
    resolved to multiple files.
    """
    directory, rel_path, endpoint = self.split_prefix(ctx, item)

    if filepath is not None:
        filename = path.relpath(filepath, directory)
    else:
        filename = rel_path
    filename = filename.replace("\\", "/")

    if has_request_context():
        url = url_for(endpoint, filename=filename)
    else:
        # `url_for` outside a request context needs SERVER_NAME; bind the
        # adapter directly so the URL map still resolves the static rules.
        app = ctx.environment._app
        server_name = app.config.get("SERVER_NAME") or ""
        url_adapter = app.url_map.bind(server_name, url_scheme="http")
        url = url_adapter.build(endpoint, {"filename": filename}, force_external=False)

    # Scheme is unknown during build; emit scheme-relative URLs.
    if url:
        url = url.removeprefix("http:")
    return url
split_prefix(ctx, item)

Split a blueprint-prefixed asset path.

Returns (directory, relative_path, endpoint). If item has a blueprint_name/... prefix that matches a registered blueprint with a static folder, the blueprint's static folder and endpoint are used. Otherwise the app's static folder is used and item is returned unchanged.

Raises:

Type Description
ValueError

If item is empty or no app context is available.

TypeError

If the matched blueprint, or the app, has no static folder.

Source code in src/quart_assets/extension.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
def split_prefix(self, ctx: Any, item: str) -> tuple[str, str, str]:
    """Split a blueprint-prefixed asset path.

    Returns ``(directory, relative_path, endpoint)``. If ``item`` has a
    ``blueprint_name/...`` prefix that matches a registered blueprint with
    a static folder, the blueprint's static folder and endpoint are used.
    Otherwise the app's static folder is used and ``item`` is returned
    unchanged.

    Raises:
        ValueError: If ``item`` is empty or no app context is available.
        TypeError: If the matched blueprint, or the app, has no static folder.
    """
    if not item:
        raise ValueError("Asset item cannot be empty")

    app = ctx._app
    if app is None:
        raise ValueError("No app context available")

    if "/" in item:
        blueprint_name, name = item.split("/", 1)
        if blueprint_name and name and blueprint_name in app.blueprints:
            try:
                directory = get_static_folder(app.blueprints[blueprint_name])
            except TypeError as e:
                raise TypeError(f"Blueprint '{blueprint_name}' has no static folder") from e
            return directory, name, f"{blueprint_name}.static"

    try:
        directory = get_static_folder(app)
    except TypeError as e:
        raise TypeError("App has no static folder configured") from e
    return directory, item, "static"

Jinja2 Integration

Custom filters and extensions for Jinja2 template integration.

Jinja2Filter

quart_assets.extension.Jinja2Filter

Bases: Filter

Compiles all source files as Jinja2 templates using Quart contexts.

Source code in src/quart_assets/extension.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
class Jinja2Filter(Filter):
    """Compiles all source files as Jinja2 templates using Quart contexts."""

    name: str = "jinja2"
    max_debug_level = None

    def __init__(self, context: dict[str, Any] | None = None) -> None:
        super().__init__()
        self.context = context or {}

    def input(self, _in: Any, out: Any, **kw: Any) -> None:
        out.write(render_template_string(_in.read(), **self.context))

AsyncAssetsExtension

quart_assets.extension.AsyncAssetsExtension

Bases: AssetsExtension

Async-aware webassets Jinja2 extension for Quart's async Jinja environment.

Source code in src/quart_assets/extension.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class AsyncAssetsExtension(AssetsExtension):
    """Async-aware webassets Jinja2 extension for Quart's async Jinja environment."""

    def _render_assets(
        self, filter: Any, output: Any, dbg: Any, depends: Any, files: Any, caller: Any = None
    ) -> Any:
        if self.environment.is_async:
            return self._render_assets_async(filter, output, dbg, depends, files, caller)
        return self._render_assets_sync(filter, output, dbg, depends, files, caller)

    def _build_bundle(
        self, filter: Any, output: Any, dbg: Any, depends: Any, files: Any
    ) -> tuple[Any, Any]:
        env = self.environment.assets_environment  # ty: ignore[unresolved-attribute]
        if env is None:
            raise RuntimeError("No assets environment configured in Jinja2 environment")

        bundle_kwargs = {
            "output": output,
            "filters": filter,
            "debug": dbg,
            "depends": depends,
        }
        bundle = self.BundleClass(*self.resolve_contents(files, env), **bundle_kwargs)

        with bundle.bind(env):
            urls = bundle.urls(calculate_sri=True)
        return bundle, urls

    def _render_assets_sync(
        self, filter: Any, output: Any, dbg: Any, depends: Any, files: Any, caller: Any
    ) -> str:
        bundle, urls = self._build_bundle(filter, output, dbg, depends, files)
        parts: list[str] = []
        for entry in urls:
            if isinstance(entry, dict):
                parts.append(caller(entry["uri"], entry.get("sri", None), bundle.extra))
            else:
                parts.append(caller(entry, None, bundle.extra))
        return "".join(parts)

    async def _render_assets_async(
        self, filter: Any, output: Any, dbg: Any, depends: Any, files: Any, caller: Any
    ) -> str:
        bundle, urls = self._build_bundle(filter, output, dbg, depends, files)
        parts: list[str] = []
        for entry in urls:
            if isinstance(entry, dict):
                caller_result = caller(entry["uri"], entry.get("sri", None), bundle.extra)
            else:
                caller_result = caller(entry, None, bundle.extra)
            if inspect.iscoroutine(caller_result):
                caller_result = await caller_result
            parts.append(caller_result)
        return "".join(parts)

Utility Functions

get_static_folder

quart_assets.extension.get_static_folder(app_or_blueprint)

Return the static folder of the given Quart app instance, or module/blueprint.

Source code in src/quart_assets/extension.py
23
24
25
26
27
28
29
def get_static_folder(app_or_blueprint: Any) -> str:
    """Return the static folder of the given Quart app
    instance, or module/blueprint.
    """
    if not app_or_blueprint.has_static_folder:
        raise TypeError(f"The referenced blueprint {app_or_blueprint} has no static folder.")
    return app_or_blueprint.static_folder