pax_global_header00006660000000000000000000000064152125561420014515gustar00rootroot0000000000000052 comment=7c024a3ef83a1c2951f2ab3925e85b81ebbc8bc0 python-django-js-asset-4.0.1/000077500000000000000000000000001521255614200160275ustar00rootroot00000000000000python-django-js-asset-4.0.1/.editorconfig000066400000000000000000000003101521255614200204760ustar00rootroot00000000000000# top-most EditorConfig file root = true [*] end_of_line = lf insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true indent_style = space indent_size = 2 [*.py] indent_size = 4 python-django-js-asset-4.0.1/.github/000077500000000000000000000000001521255614200173675ustar00rootroot00000000000000python-django-js-asset-4.0.1/.github/dependabot.yml000066400000000000000000000005241521255614200222200ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" groups: github-actions: patterns: - "*" - package-ecosystem: "pip" directory: "/" schedule: interval: "monthly" groups: python-deps: patterns: - "*" python-django-js-asset-4.0.1/.github/workflows/000077500000000000000000000000001521255614200214245ustar00rootroot00000000000000python-django-js-asset-4.0.1/.github/workflows/publish.yml000066400000000000000000000014171521255614200236200ustar00rootroot00000000000000name: Publish to PyPI on: push: tags: - '*' jobs: build: name: Build distribution runs-on: ubuntu-latest if: github.event.repository.fork == false steps: - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: python-version: 3.x - name: Build run: | python -m pip install --upgrade build python -m build - uses: actions/upload-artifact@v7 with: name: dist path: dist/ publish: name: Publish to PyPI needs: build runs-on: ubuntu-latest environment: release permissions: id-token: write steps: - uses: actions/download-artifact@v8 with: name: dist path: dist/ - uses: pypa/gh-action-pypi-publish@release/v1 python-django-js-asset-4.0.1/.github/workflows/tests.yml000066400000000000000000000016131521255614200233120ustar00rootroot00000000000000name: Tests on: push: branches: [main] pull_request: branches: [main] schedule: - cron: 37 1 1 * * jobs: tests: name: Python ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: - '3.10' - '3.11' - '3.12' - '3.13' - '3.14' steps: - uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip wheel setuptools tox - name: Run tox targets for ${{ matrix.python-version }} run: | ENV_PREFIX=$(tr -C -d "0-9" <<< "${{ matrix.python-version }}") TOXENV=$(tox --listenvs | grep "^py$ENV_PREFIX" | tr '\n' ',') python -m tox python-django-js-asset-4.0.1/.gitignore000066400000000000000000000002301521255614200200120ustar00rootroot00000000000000*.py? *~ *.sw? \#*# /secrets.py .DS_Store ._* /*.egg-info /MANIFEST /_build /build /dist tests/test.zip /docs/_build /.eggs .coverage htmlcov venv .tox python-django-js-asset-4.0.1/.pre-commit-config.yaml000066400000000000000000000016711521255614200223150ustar00rootroot00000000000000exclude: ".yarn/|yarn.lock|\\.min\\.(css|js)$" repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-added-large-files - id: check-builtin-literals - id: check-executables-have-shebangs - id: check-merge-conflict - id: check-toml - id: check-yaml - id: detect-private-key - id: end-of-file-fixer - id: mixed-line-ending - id: trailing-whitespace - repo: https://github.com/adamchainz/django-upgrade rev: 1.30.0 hooks: - id: django-upgrade args: [--target-version, "3.2"] - repo: https://github.com/astral-sh/ruff-pre-commit rev: "v0.15.16" hooks: - id: ruff - id: ruff-format - repo: https://github.com/tox-dev/pyproject-fmt rev: v2.23.0 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject rev: v0.25 hooks: - id: validate-pyproject python-django-js-asset-4.0.1/AGENTS.md000066400000000000000000000067321521255614200173420ustar00rootroot00000000000000# django-js-asset — agent notes `JS`, `CSS`, `JSON` support for `django.forms.Media`, plus importmap and CSP nonce support. ## Running tests Use **tox** (configured with `tox-uv`, so it provisions the right Python/Django automatically — do not hand-roll a venv): ```bash tox -e py312-dj42 # lowest supported combination tox -e py314-dj62 # a recent combination tox run-parallel # the whole matrix ``` The matrix lives in `tox.ini` (`tests/manage.py test testapp`). ## Compatibility (hard constraint) - Python `>=3.10`, Django `>=4.2`. **Django 6.2+ is NOT an acceptable floor.** - `JS`/`CSS` *produce* Django's own `Script`/`Stylesheet` (see Layout). Where Django lacks them they are **backported** in `js_asset/_compat.py`, so the 4.2 floor holds — we provide the machinery, we don't *depend* on Django having it. Availability in real Django: `Script` (5.2+), `Stylesheet` + `MediaAsset.render(attrs=)` (6.1+), attribute-aware `__eq__`/`__hash__` (6.2+). The `<5.2` backport mirrors the **6.2** contract. - Don't rely on Django's `MediaAsset.render(attrs=...)` existing: it is absent on 5.2/6.0. `media.py:_render_asset` injects the nonce itself by rebuilding the tag from `element_template` + `flatatt`, which works on every version. - Cross-version gotcha: Django >= 6.2 wraps bare js/css path strings into `Script`/`Stylesheet` in `Media._js`/`._css`; older Django keeps raw strings. `media.py:_render_{js,css}` wrap any leftover strings via `JS()`/`CSS()`, so `_render_asset` always sees a `MediaAsset` (or `JSON`/`ImportMap`). ## Layout - `js_asset/_compat.py` — `MediaAsset`/`Script`/`Stylesheet`: imported from Django where present, backported (with the 6.2 contract) below 5.2/6.1. - `js_asset/js.py` — `JS`/`CSS` are **factories** (a `_ProducesAsset` metaclass): calling them returns a Django `Script`/`Stylesheet`/`InlineStyle` so they dedup in `forms.Media.merge` against native assets *and* bare path strings; `isinstance(x, JS)` still works via `__instancecheck__`. `JSON` and `ImportMap` have no Django counterpart and stay standalone `@html_safe` objects with `render(*, nonce="")`. Output is byte-identical to native Django assets (flatatt sorts attributes), which the exact-string tests depend on. Equality is Django's, so dedup is attribute-aware on 4.2-5.1 + 6.2+ and path-only on 5.2-6.1 (`test_set` derives its expectation from this). - `js_asset/media.py` — `Media(forms.Media)` subclass: merges embedded `ImportMap`s into one tag and applies a nonce. Implements `__add__` **and** `__radd__` so it keeps its type (and nonce) when combined with plain `forms.Media` from either side. The nonce lives on the instance (constructor `nonce=` or `with_nonce()` returning a copy); `render()` reads it, since templates call `render()` with no arguments. - Django >= 6.2 has built-in CSP support: the `{% csp_nonce_attr media %}` tag (`django.utils.csp.nonce_attr`) renders media via `media.render(attrs={"nonce": nonce})`. `Media.render()` therefore accepts `attrs=` and honours its nonce — so our `Media` plugs into that tag on 6.2, while `with_nonce()`/constructor cover older Django. ## Docs - `README.rst` — the single doc: assets, import-map merging via `js_asset.Media`, rendering in views/admin, and CSP nonces across Django 4.2 → main. ## Lint `prek` / `ruff` (config in `pyproject.toml`). ## Commit Style Commit without the `Co-Authored-By` attribution line (no `--co-author` / no `Co-Authored-By: Claude` trailer). python-django-js-asset-4.0.1/CHANGELOG.rst000066400000000000000000000162131521255614200200530ustar00rootroot00000000000000 .. _changelog: Change log ========== Next version ~~~~~~~~~~~~ - Fixed ``js_asset.Media`` crashing with an ``AttributeError`` when rendering an asset that only implements Django's plain ``__html__`` contract (neither a ``MediaAsset`` nor one of our ``ImportMap``/``JSON`` types). Such assets now fall back to ``__html__()`` exactly like ``django.forms.Media`` does; the nonce cannot be threaded into an opaque ``__html__`` asset, same as with stock Django. 4.0 (2026-06-11) ~~~~~~~~~~~~~~~~ This is a major release. Most code keeps working unchanged and the rendered HTML is the same, but **if you use import maps the upgrade is not automatic** -- see the import-map note at the end. - Added ``js_asset.Media``, a ``django.forms.Media`` subclass with two extra abilities. First, it merges every embedded ``ImportMap`` into a single `` The attributes are automatically escaped. The data attributes may now be accessed inside ``asset.js``: .. code-block:: javascript let answer = document.querySelector("#asset-script").dataset.answer; Also, because the implementation of ``static`` differs between supported Django versions (older do not take the presence of ``django.contrib.staticfiles`` in ``INSTALLED_APPS`` into account), a ``js_asset.static`` function is provided which does the right thing automatically. CSS and JSON support ==================== Since 3.0 django-js-asset also ships a ``CSS`` and ``JSON`` media object which can be used to ship stylesheets, inline styles and JSON blobs to the frontend. It's recommended to pass those through ``forms.Media(js=[])`` as well since ``js`` is a simple list while ``css`` uses a dictionary keyed with the media to use for the stylesheet. So, you can add everything at once: .. code-block:: python from js_asset import CSS, JS, JSON forms.Media(js=[ JSON({"configuration": 42}, id="widget-configuration"), CSS("widget/style.css"), CSS("p{color:red;}", inline=True), JS("widget/script.js", {"type": "module"}), ]) This produces: .. code-block:: html Compatibility ============= At the time of writing this app is compatible with Django 4.2 and better (up to and including the Django main branch), but have a look at the `tox configuration `_ for definitive answers. Deduplication ============= ``JS`` and ``CSS`` produce Django's own ``Script`` and ``Stylesheet`` objects (backported on Django versions that lack them), so they de-duplicate against each other, against plain path strings, and against Django's native assets when ``forms.Media`` merges the media of several forms and widgets. The rules for what counts as a duplicate are Django's, and Django changed them between releases: * **Django 4.2 - 5.1 and 6.2+:** two assets are equal when both the path *and* the attributes match, so ``JS("a.js", {"id": "x"})`` and ``JS("a.js", {"id": "y"})`` are distinct and both render. * **Django 5.2 - 6.1:** Django compares the **path only**, so those same two assets de-duplicate to one (whichever wins the merge) -- exactly as Django's own ``Script``/``Stylesheet`` behave on those versions. A given file referenced both as a bare string and via ``JS``/``CSS`` always de-duplicates, and nothing is ever rendered twice for the same path. The per-version difference only affects the unusual case of the same path carried with *different* attributes in a single merge. Import maps =========== django-js-asset can ship `import maps `__. They let your modules import short names (``import { Stuff } from "my-library"``) and have the browser resolve them to the real, possibly hashed, URLs produced by Django's ``ManifestStaticFilesStorage`` -- without rewriting the imports in your JavaScript. Browsers do not reliably support more than one import map per page, so all of them have to be merged into one. ``js_asset.Media`` does this for you: drop ``ImportMap`` objects into your media wherever they are relevant -- typically next to the module that needs them -- and they are combined into a single ``` (identical). - `CSS("a.css", media="all")` and `Stylesheet("a.css", media="all")` → `` (identical, including the `nonce` slot which sorts between `media` and `rel`). The existing exact-string tests (`test_css`, `test_json`, `test_boolean_attributes`, `test_asset`) are therefore expected to keep passing, which satisfies the AGENTS.md byte-identity constraint — **but this must be re-verified per supported Django version**, because the base class differs. ### Nonce rendering (the part that varies by version) `js_asset` historically renders the nonce itself (so it works on 4.2). Django 6.2 renders it via `asset.render(attrs={"nonce": ...})`; 5.2–6.1 assets have only `__str__`; <5.2 has no asset class. Because we produce **native** objects on 6.2+, the clean path there is to let Django render: `media.render(attrs={"nonce": nonce})` is exactly what the built-in `csp_nonce_attr` tag does, and our `Media.render()` already forwards `attrs`. On 5.2–6.1 (and via the backport on <5.2) js_asset's `Media` renders the nonce by rebuilding the tag from the asset's `element_template` and `flatatt({**asset.attributes, "nonce": nonce})` — the asset itself stays a plain native `Script`. This keeps the *asset* objects pure Django and confines the version-specific logic to js_asset's `Media._render_asset`. `JSON` / `ImportMap` keep their own `render(*, nonce=...)` (they are not `MediaAsset`s). ### Equality / hash — inherited, not reimplemented Because `JS`/`CSS` *produce* `Script`/`Stylesheet`, equality and hashing are **Django's**, version-appropriate by construction — we write none of it on 6.2+. Consequences, all acceptable per the recorded decision to "match Django": - 6.2+: path + attributes identity, plus the bare-string branch. A path with differing attributes is a distinct asset; a path vs the same bare string dedups. Verified against existing tests: `test_set` → 2, `test_asset_merging` → 3 still hold. - 5.2–6.1: path-only identity (attributes ignored) — slightly looser dedup than 6.2, accepted under "be reasonable on older Django". - The backport (<5.2) should mirror **6.2** semantics so the floor matches the target rather than the awkward middle. ### The hard problems (call out explicitly) 1. **Inline CSS has no Django equivalent.** `CSS(src, inline=True)` renders a `` block whose `src` is *CSS text*, not a path; `Stylesheet` always renders `` and runs `_path` through `static()`. So `CSS` is a factory that returns a small dedicated `InlineStyle(MediaAsset)` with a `` template and a `path` override that returns the source verbatim. It is still a `MediaAsset`, so it flows through the same render path; its `_path`-keyed equality is on CSS text, which is odd but harmless (inline blocks are rarely deduped). 2. **`JSON` and `ImportMap` stay custom.** No Django counterpart (`json_script` / `' def __init__(self, src, **attributes): # Alter the signature to allow src to be passed as a keyword argument. super().__init__(src, **attributes) if django.VERSION >= (6, 1): from django.forms.widgets import Stylesheet else: class Stylesheet(MediaAsset): element_template = '' def __init__(self, href, **attributes): super().__init__(href, rel="stylesheet", **attributes) python-django-js-asset-4.0.1/js_asset/js.py000066400000000000000000000113421521255614200206310ustar00rootroot00000000000000import json from dataclasses import dataclass, field from typing import Any from django.core.serializers.json import DjangoJSONEncoder from django.forms.utils import flatatt from django.templatetags.static import static from django.utils.functional import lazy from django.utils.html import html_safe, json_script, mark_safe from js_asset._compat import MediaAsset, Script, Stylesheet __all__ = [ "CSS", "JS", "JSON", "ImportMap", "InlineStyle", "MediaAsset", "Script", "Stylesheet", "static", "static_lazy", ] static_lazy = lazy(static, str) def _canonical_hash(data): # Hash an order-insensitive canonical form so the hash stays consistent # with dict equality (``a == b`` must imply ``hash(a) == hash(b)``). # ``DjangoJSONEncoder`` resolves lazy values, e.g. ``static_lazy`` paths. return hash(json.dumps(data, sort_keys=True, cls=DjangoJSONEncoder)) class InlineStyle(MediaAsset): """ An inline ``" def __init__(self, css, *, media="all", **attributes): super().__init__(css, media=media, **attributes) @property def path(self): return self._path class _ProducesAsset(type): """ Metaclass turning ``JS``/``CSS`` into factories: calling them returns a Django asset (``Script``/``Stylesheet``/``InlineStyle``) so they share ``forms.Media.merge`` buckets -- and dedup -- with Django's own assets and with bare path strings. ``isinstance(x, JS)`` keeps answering truthfully by delegating to the produced type(s). """ def __call__(cls, *args, **kwargs): return cls._produce(*args, **kwargs) def __instancecheck__(cls, instance): return isinstance(instance, cls._produces) def __subclasscheck__(cls, subclass): return issubclass(subclass, cls._produces) class JS(metaclass=_ProducesAsset): _produces = Script @staticmethod def _produce(src, attrs=None): return Script(src, **(attrs or {})) class CSS(metaclass=_ProducesAsset): _produces = (Stylesheet, InlineStyle) @staticmethod def _produce(src, media="all", *, inline=False): if inline: return InlineStyle(src, media=media) return Stylesheet(src, media=media) @html_safe @dataclass(eq=True) class JSON: data: dict[str, Any] id: str | None = field(default="", kw_only=True) def __hash__(self): # ``__eq__`` (dataclass) compares ``data`` order-insensitively, so the # hash must too -- see ``_canonical_hash``. return hash((_canonical_hash(self.data), self.id)) def render(self, *, nonce=""): # A type="application/json" block is data, not executed JavaScript, so # it is not governed by CSP and needs no nonce. return json_script(self.data, self.id) def __str__(self): return self.render() @html_safe class ImportMap: def __init__(self, importmap): self._importmap = importmap def __eq__(self, other): return isinstance(other, ImportMap) and self._importmap == other._importmap def __hash__(self): # ``__eq__`` compares the underlying dict order-insensitively, so the # hash must too -- see ``_canonical_hash``. return _canonical_hash(self._importmap) def render(self, *, nonce=""): if self._importmap: nonce_attr = mark_safe(flatatt({"nonce": nonce})) if nonce else "" html = json_script(self._importmap).removeprefix( '""", ) c = ImportMap( { "imports": { "/app/": "./original-app/", "/app/helper": "./helper/index.mjs", }, "scopes": {"/js": {"/app/": "./js-app/"}}, } ) self.assertEqual( str(a | b | c), """\ """, ) python-django-js-asset-4.0.1/tests/testapp/test_js_asset.py000066400000000000000000000102611521255614200240750ustar00rootroot00000000000000from django.forms import Media from django.test import TestCase from js_asset.js import CSS, JS, JSON, InlineStyle, Script, Stylesheet class AssetTest(TestCase): def test_asset(self): media = Media( css={"print": ["app/print.css"]}, js=[ "app/test.js", JS("app/asset.js", {"id": "asset-script", "data-the-answer": 42}), JS("app/asset-without.js", {}), ], ) html = str(media) # print(html) self.assertInHTML( '', html, ) self.assertInHTML( '', html, ) self.assertInHTML( '', html, ) self.assertInHTML( '', html, ) def test_absolute(self): media = Media(js=[JS("https://cdn.example.org/script.js")]) html = str(media) self.assertInHTML( '', html, ) def test_asset_merging(self): media1 = Media(js=["thing.js", JS("other.js"), "some.js"]) media2 = Media(js=["thing.js", JS("other.js"), "some.js"]) media = media1 + media2 self.assertEqual(len(media._js), 3) self.assertEqual(media._js[0], "thing.js") self.assertEqual(media._js[2], "some.js") def test_set(self): media = [ JS("app/asset.js", {"id": "asset-script", "data-the-answer": 42}), JS("app/asset.js", {"id": "asset-script", "data-the-answer": 42}), JS("app/asset.js", {"id": "asset-script", "data-the-answer": 43}), ] # ``JS`` produces a Django ``Script``, so identity follows Django's own # contract: 6.2+ (and the < 5.2 backport) fold attributes into equality # -- the differing third asset stays distinct -> 2. Django 5.2 - 6.1 # dedup on the path alone, so all three collapse -> 1. attribute_aware = media[0] != media[2] self.assertEqual(len(set(media)), 2 if attribute_aware else 1) def test_boolean_attributes(self): self.assertEqual( str(JS("app/asset.js", {"bool": True, "cool": False})), '', ) def test_css(self): self.assertEqual( str(CSS("app/style.css")), '', ) self.assertEqual( str(CSS("app/style.css", media="screen")), '', ) self.assertEqual( str(CSS("p{color:red}", inline=True)), '', ) def test_js_produces_script(self): asset = JS("app/asset.js", {"id": "x"}) # ``JS`` is a factory producing a Django ``Script`` ... self.assertIsInstance(asset, Script) self.assertNotIsInstance(asset, Stylesheet) # ... while ``isinstance(x, JS)`` keeps answering truthfully. self.assertIsInstance(asset, JS) self.assertNotIsInstance(CSS("app/style.css"), JS) def test_css_produces_stylesheet_or_inline(self): link = CSS("app/style.css") inline = CSS("p{color:red}", inline=True) self.assertIsInstance(link, Stylesheet) self.assertIsInstance(inline, InlineStyle) # Both flavours answer ``isinstance(x, CSS)``. self.assertIsInstance(link, CSS) self.assertIsInstance(inline, CSS) self.assertNotIsInstance(JS("app/asset.js"), CSS) def test_json(self): self.assertEqual( str(JSON({"hello": "world"}, id="hello")), '', ) self.assertEqual( str(JSON({"hello": "world"})), '', ) python-django-js-asset-4.0.1/tests/testapp/test_media.py000066400000000000000000000157001521255614200233440ustar00rootroot00000000000000from django.forms import Media as DjangoMedia from django.test import TestCase from django.utils.html import html_safe from js_asset import CSS, JS, JSON, ImportMap, Media @html_safe class HTMLOnlyAsset: """ A media asset using Django's plain ``__html__`` contract -- not a ``MediaAsset`` and not one of our ``ImportMap``/``JSON`` types. Stock ``forms.Media`` renders such assets via ``__html__()``. """ def __init__(self, markup): self._markup = markup def __str__(self): return self._markup class MediaTest(TestCase): def test_nonce_applied_to_assets(self): media = Media( nonce="r@nd0m", css={"all": [CSS("app.css")]}, js=[JS("app.js")], ) html = media.render() self.assertInHTML( '', html, ) self.assertInHTML( '', html, ) def test_no_nonce_keeps_plain_output(self): media = Media(js=[JS("app.js")]) self.assertEqual(media.render(), '') def test_importmaps_are_merged_and_rendered_first(self): media = Media( nonce="r@nd0m", js=[ ImportMap({"imports": {"a": "/static/a.js"}}), JS("app.js", {"type": "module"}), ImportMap({"imports": {"b": "/static/b.js"}}), ], ) self.assertEqual( media.render(), '\n' '', ) def test_type_and_nonce_preserved_when_merging(self): ours = Media(nonce="abc", js=[ImportMap({"imports": {"a": "/static/a.js"}})]) plain = DjangoMedia(js=["app.js"]) # Our Media on the right-hand side (the case plain forms.Media drops). merged = plain + ours self.assertIsInstance(merged, Media) self.assertEqual(merged.nonce, "abc") # ... and on the left-hand side. merged = ours + plain self.assertIsInstance(merged, Media) self.assertEqual(merged.nonce, "abc") def test_bare_string_assets_get_nonce(self): # Bare paths stay strings on Django < 6.2 and are wrapped into # MediaAsset objects on >= 6.2; both paths must receive the nonce. media = Media(nonce="n0nce", css={"all": ["plain.css"]}, js=["plain.js"]) html = media.render() self.assertInHTML( '', html, ) self.assertInHTML( '', html, ) def test_merging_combines_css_and_js(self): ours = Media(nonce="n", css={"all": [CSS("a.css")]}, js=[JS("a.js")]) plain = DjangoMedia(css={"all": ["b.css"]}, js=["b.js"]) merged = plain + ours # via __radd__ self.assertIsInstance(merged, Media) html = merged.render() for snippet in ( '', '', '', '', ): self.assertInHTML(snippet, html) def test_render_accepts_attrs_nonce(self): # Django >= 6.2's CSP integration calls media.render(attrs={"nonce": ...}). media = Media(js=[JS("app.js")]) self.assertInHTML( '', media.render(attrs={"nonce": "from-attrs"}), ) # A passed nonce overrides the stored one. media = Media(nonce="stored", js=[JS("app.js")]) self.assertInHTML( '', media.render(attrs={"nonce": "from-attrs"}), ) def test_html_only_asset_renders_via_html(self): # A plain ``__html__``-only asset (e.g. a build-tool helper) is neither # a MediaAsset nor one of our types; it must fall back to ``__html__()`` # the way stock ``forms.Media`` does, instead of crashing on a missing # ``render`` method. media = Media(js=[HTMLOnlyAsset('')]) self.assertEqual(media.render(), '') def test_html_only_asset_renders_with_nonce_set(self): # The nonce cannot be threaded into an opaque ``__html__`` asset (same # as stock Django), but its presence must not break rendering. media = Media( nonce="n0nce", js=[HTMLOnlyAsset('')], ) self.assertEqual(media.render(), '') def test_json_asset_rendered_through_media(self): # A JSON block carried as a JS asset renders via its own ``render``. # It is data, not executed script, so it stays nonce-free even when a # nonce is set on the media. media = Media(nonce="n0nce", js=[JSON({"a": 1}, id="cfg")]) self.assertInHTML( '', media.render(), ) def test_adding_non_media_is_not_supported(self): with self.assertRaises(TypeError): Media() + 3 # The reverse case dispatches to Media.__radd__, which must also return # NotImplemented (a clean TypeError) rather than raise AttributeError. with self.assertRaises(TypeError): 3 + Media() def test_from_media_wraps_existing_instance(self): plain = DjangoMedia( js=[ImportMap({"imports": {"a": "/static/a.js"}}), JS("app.js")] ) wrapped = Media.from_media(plain, nonce="w") self.assertIsInstance(wrapped, Media) self.assertEqual(wrapped.nonce, "w") html = wrapped.render() self.assertInHTML( '', html, ) self.assertInHTML( '', html, ) def test_with_nonce_returns_a_copy(self): media = Media(js=[JS("app.js")]) request_media = media.with_nonce("xyz") self.assertIsInstance(request_media, Media) self.assertEqual(request_media.nonce, "xyz") # The shared (e.g. cached widget) media is left untouched. self.assertEqual(media.nonce, "") self.assertInHTML( '', request_media.render(), ) python-django-js-asset-4.0.1/tox.ini000066400000000000000000000010271521255614200173420ustar00rootroot00000000000000[tox] envlist = py{310,311}-dj{42} py{312}-dj{42,52,60,main} py{313}-dj{52,60,61,main} py{314}-dj{52,60,61,main} [testenv] usedevelop = true extras = tests commands = python -Wd {envbindir}/coverage run tests/manage.py test -v2 --keepdb {posargs:testapp} coverage report -m deps = dj42: Django>=4.2,<5.0 dj50: Django>=5.0,<5.1 dj51: Django>=5.1,<5.2 dj52: Django>=5.2,<6.0 dj60: Django>=6.0,<6.1 dj61: Django>=6.1a1,<6.2 djmain: https://github.com/django/django/archive/main.tar.gz