Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-django-js-asset for
openSUSE:Factory checked in at 2026-08-22 21:35:11
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-django-js-asset (Old)
and /work/SRC/openSUSE:Factory/.python-django-js-asset.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-django-js-asset"
Sat Aug 22 21:35:11 2026 rev:10 rq:1372942 version:4.0.2
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-django-js-asset/python-django-js-asset.changes
2026-06-22 17:29:45.010395344 +0200
+++
/work/SRC/openSUSE:Factory/.python-django-js-asset.new.1258/python-django-js-asset.changes
2026-08-22 21:37:20.721797582 +0200
@@ -1,0 +2,19 @@
+Fri Aug 21 20:53:01 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 4.0.2:
+ * Fixed js_asset.Media treating html-safe strings -- e.g.
+ mark_safe('<script defer src="..."></script>'), a
+ long-documented Django idiom for embedding a complete asset tag
+ -- as asset paths, so they were run through static() and
+ percent-encoded instead of being rendered verbatim. Anything
+ providing __html__() now takes the verbatim path, matching
+ django.forms.Media (see Django's ticket #37262).
+ * js_asset.Media additionally renders html-safe strings
+ correctly on Django 6.1, whose own forms.Media mangles them
+ (fixed in Django for 6.1.1). Only media built through
+ js_asset.Media benefits; assets adopted from a plain
+ forms.Media -- including widget Media declarations, which
+ Django always builds with forms.Media -- are already normalized
+ before we see them.
+
+-------------------------------------------------------------------
Old:
----
4.0.1.tar.gz
New:
----
4.0.2.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-django-js-asset.spec ++++++
--- /var/tmp/diff_new_pack.DMqEA3/_old 2026-08-22 21:37:21.526826358 +0200
+++ /var/tmp/diff_new_pack.DMqEA3/_new 2026-08-22 21:37:21.528826429 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-django-js-asset
-Version: 4.0.1
+Version: 4.0.2
Release: 0
Summary: Script tag with additional attributes for django.formsMedia
License: BSD-3-Clause
++++++ 4.0.1.tar.gz -> 4.0.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/.github/workflows/publish.yml
new/django-js-asset-4.0.2/.github/workflows/publish.yml
--- old/django-js-asset-4.0.1/.github/workflows/publish.yml 2026-06-11
18:03:46.000000000 +0200
+++ new/django-js-asset-4.0.2/.github/workflows/publish.yml 2026-08-18
08:50:13.000000000 +0200
@@ -11,8 +11,8 @@
runs-on: ubuntu-latest
if: github.event.repository.fork == false
steps:
- - uses: actions/checkout@v6
- - uses: actions/setup-python@v6
+ - uses: actions/checkout@v7
+ - uses: actions/setup-python@v7
with:
python-version: 3.x
- name: Build
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/.github/workflows/tests.yml
new/django-js-asset-4.0.2/.github/workflows/tests.yml
--- old/django-js-asset-4.0.1/.github/workflows/tests.yml 2026-06-11
18:03:46.000000000 +0200
+++ new/django-js-asset-4.0.2/.github/workflows/tests.yml 2026-08-18
08:50:13.000000000 +0200
@@ -23,9 +23,9 @@
- '3.14'
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v6
+ uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/.pre-commit-config.yaml
new/django-js-asset-4.0.2/.pre-commit-config.yaml
--- old/django-js-asset-4.0.1/.pre-commit-config.yaml 2026-06-11
18:03:46.000000000 +0200
+++ new/django-js-asset-4.0.2/.pre-commit-config.yaml 2026-08-18
08:50:13.000000000 +0200
@@ -14,17 +14,17 @@
- id: mixed-line-ending
- id: trailing-whitespace
- repo: https://github.com/adamchainz/django-upgrade
- rev: 1.30.0
+ rev: 1.31.1
hooks:
- id: django-upgrade
args: [--target-version, "3.2"]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: "v0.15.16"
+ rev: "v0.16.3"
hooks:
- id: ruff
- id: ruff-format
- repo: https://github.com/tox-dev/pyproject-fmt
- rev: v2.23.0
+ rev: v2.28.0
hooks:
- id: pyproject-fmt
- repo: https://github.com/abravalheri/validate-pyproject
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/.readthedocs.yaml
new/django-js-asset-4.0.2/.readthedocs.yaml
--- old/django-js-asset-4.0.1/.readthedocs.yaml 1970-01-01 01:00:00.000000000
+0100
+++ new/django-js-asset-4.0.2/.readthedocs.yaml 2026-08-18 08:50:13.000000000
+0200
@@ -0,0 +1,12 @@
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+version: 2
+
+build:
+ os: ubuntu-24.04
+ tools:
+ python: "3.13"
+
+sphinx:
+ configuration: docs/conf.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/AGENTS.md
new/django-js-asset-4.0.2/AGENTS.md
--- old/django-js-asset-4.0.1/AGENTS.md 2026-06-11 18:03:46.000000000 +0200
+++ new/django-js-asset-4.0.2/AGENTS.md 2026-08-18 08:50:13.000000000 +0200
@@ -32,6 +32,30 @@
`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`).
+- **Never test an asset with `isinstance(item, str)`.** `SafeString` is a `str`
+ subclass, so `mark_safe('<script src=...></script>')` — a complete tag that
+ must render verbatim — would be resolved through `static()` and
+ percent-encoded. The predicate is `hasattr(item, "__html__")`; only bare
+ paths get wrapped. Django made exactly this mistake in 6.1 (ticket #37262,
+ fixed on its 6.1.x branch for 6.1.1), and we had it independently in
+ `_render_{js,css}`.
+- Because Django 6.1 mangles such strings in `forms.Media.__init__` itself,
+ `media.py` also overrides `_normalize_{js,css}` with the `__html__`
+ predicate — those hooks are called through `self`, so a subclass can fix
+ them, and `_compat` gives us `Script`/`Stylesheet` on every supported Django
+ so the wrapped output is unchanged. This covers media built *through* our
+ class; assets adopted from a foreign `forms.Media` (`from_media`, `__add__`,
+ and Django's widget `media_property`, which always instantiates
+ `forms.Media`) arrive already normalized and cannot be recovered on 6.1.
+ `test_html_safe_strings_adopted_from_foreign_media` pins that boundary via
+ the `DJANGO_KEEPS_HTML_SAFE_STRINGS` probe.
+- `tests/testapp/test_media.py` carries `JS_ASSETS`/`CSS_ASSETS`: one row per
+ asset kind, each with its exact rendering with and without a nonce. **Add a
+ row whenever a new asset kind appears** — that table is the guard against a
+ rendering branch quietly mishandling one of them.
+ `test_matches_django_rendering` compares us against stock `forms.Media` for
+ path assets; it is what catches Django changing its normalization or tag
+ format under our overrides.
## Layout
@@ -47,7 +71,8 @@
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**
+ `ImportMap`s into one tag, applies a nonce, and normalizes js/css entries by
+ the `__html__` predicate (see the html-safe-string note above). 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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/CHANGELOG.rst
new/django-js-asset-4.0.2/CHANGELOG.rst
--- old/django-js-asset-4.0.1/CHANGELOG.rst 2026-06-11 18:03:46.000000000
+0200
+++ new/django-js-asset-4.0.2/CHANGELOG.rst 2026-08-18 08:50:13.000000000
+0200
@@ -13,6 +13,20 @@
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.
+- Fixed ``js_asset.Media`` treating html-safe strings -- e.g.
+ ``mark_safe('<script defer src="..."></script>')``, a long-documented Django
+ idiom for embedding a complete asset tag -- as asset paths, so they were run
+ through ``static()`` and percent-encoded instead of being rendered verbatim.
+ Anything providing ``__html__()`` now takes the verbatim path, matching
+ ``django.forms.Media`` (see Django's ticket #37262).
+- ``js_asset.Media`` additionally renders html-safe strings correctly on Django
+ 6.1, whose own ``forms.Media`` mangles them (fixed in Django for 6.1.1). Only
+ media built through ``js_asset.Media`` benefits; assets adopted from a plain
+ ``forms.Media`` -- including widget ``Media`` declarations, which Django
+ always builds with ``forms.Media`` -- are already normalized before we see
+ them.
+- Corrected the docs around Django 6.1 support. Thanks James Bligh!
+- Finally set up a documentation site at Read the Docs for django-js-asset.
4.0 (2026-06-11)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/README.rst
new/django-js-asset-4.0.2/README.rst
--- old/django-js-asset-4.0.1/README.rst 2026-06-11 18:03:46.000000000
+0200
+++ new/django-js-asset-4.0.2/README.rst 2026-08-18 08:50:13.000000000
+0200
@@ -269,14 +269,15 @@
applied when the media is rendered, not when it is constructed.
``js_asset.Media`` stores an optional nonce and applies it to every script and
stylesheet it renders (a ``JSON`` block is data, not executable, and
-deliberately gets none). There are three ways to get the nonce in, depending on
+deliberately gets none). There are a few ways to get the nonce in, depending on
your Django version.
-Django 6.2 and newer (built-in CSP)
+Django 6.1 and newer (built-in CSP)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Django 6.2 ships CSP support, and ``js_asset.Media`` plugs straight into it --
-no extra wiring. Configure CSP as usual:
+Django 6.1 added the ``csp_nonce_attr`` template tag with ``forms.Media``
+support, and ``js_asset.Media`` plugs straight into it -- no extra wiring.
+Configure CSP as usual:
.. code-block:: python
@@ -314,10 +315,47 @@
That single tag emits the merged import map and every script/stylesheet, each
carrying the per-request nonce.
-Django 4.2 to 6.1 (with ``django-csp``)
+Django 6.0 (built-in CSP, no ``csp_nonce_attr``)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Django 6.0 has the built-in CSP middleware, the ``SECURE_CSP`` setting and the
+``csp`` context processor, but not the ``csp_nonce_attr`` tag (that arrived in
+6.1). Configure CSP exactly as for `Django 6.1 and newer (built-in CSP)`_
+above; the only difference is that you apply the nonce yourself.
+
+Drop in a small tag. The built-in ``{{ csp_nonce }}`` value is lazy and reads
as
+falsy until first used, so the tag wraps it in ``str()``. It also copes with a
+plain ``forms.Media`` -- ``Media(form.media)`` does **not** work, because
+``forms.Media`` copies assets from a media *definition*, not an *instance*, so
+use ``from_media``:
+
+.. code-block:: python
+
+ # yourapp/templatetags/js_asset_csp.py
+ from django import template
+ from js_asset import Media
+
+ register = template.Library()
+
+ @register.simple_tag(takes_context=True)
+ def media_with_nonce(context, media):
+ nonce = context.get("csp_nonce", "")
+ if not isinstance(media, Media):
+ media = Media.from_media(media)
+ return media.with_nonce(str(nonce)).render()
+
+.. code-block:: html
+
+ {% load js_asset_csp %}
+ {% media_with_nonce form.media %}
+
+``with_nonce()`` returns a *copy*, so a shared/cached widget ``media`` object
is
+never mutated and one request's nonce can never leak into another.
+
+Django 4.2 to 5.x (with ``django-csp``)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Older Django has no built-in nonce, so use the third-party `django-csp
+Older Django has no built-in CSP, so use the third-party `django-csp
<https://django-csp.readthedocs.io/>`__ package. Install it, add its
middleware, and make sure the nonce is part of the relevant directives.
@@ -372,32 +410,14 @@
{{ form_media }}
-``with_nonce()`` returns a *copy*, so a shared/cached widget ``media`` object
is
-never mutated and one request's nonce can never leak into another. If you would
-rather stay in the template, drop in a small tag (the ``request`` context
-processor must be enabled). It also copes with a plain ``forms.Media`` --
-``Media(form.media)`` does **not** work, because ``forms.Media`` copies assets
-from a media *definition*, not an *instance*, so use ``from_media``:
+If you would rather stay in the template, the ``media_with_nonce()`` approach
+is exactly the same as for `Django 6.0 (built-in CSP, no csp_nonce_attr)`_
above,
+only its *source* changes. The template tag's nonce line can read
+``request.csp_nonce`` (the ``request`` context processor must be enabled):
.. code-block:: python
- # yourapp/templatetags/js_asset_csp.py
- from django import template
- from js_asset import Media
-
- register = template.Library()
-
- @register.simple_tag(takes_context=True)
- def media_with_nonce(context, media):
- nonce = getattr(context.get("request"), "csp_nonce", "")
- if not isinstance(media, Media):
- media = Media.from_media(media)
- return media.with_nonce(nonce).render()
-
-.. code-block:: html
-
- {% load js_asset_csp %}
- {% media_with_nonce form.media %}
+ nonce = getattr(context.get("request"), "csp_nonce", "")
Anywhere: set the nonce explicitly
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/docs/.gitignore
new/django-js-asset-4.0.2/docs/.gitignore
--- old/django-js-asset-4.0.1/docs/.gitignore 1970-01-01 01:00:00.000000000
+0100
+++ new/django-js-asset-4.0.2/docs/.gitignore 2026-08-18 08:50:13.000000000
+0200
@@ -0,0 +1 @@
+build
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/docs/Makefile
new/django-js-asset-4.0.2/docs/Makefile
--- old/django-js-asset-4.0.1/docs/Makefile 1970-01-01 01:00:00.000000000
+0100
+++ new/django-js-asset-4.0.2/docs/Makefile 2026-08-18 08:50:13.000000000
+0200
@@ -0,0 +1,177 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = build
+
+# User-friendly check for sphinx-build
+ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
+$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx
installed, then set the SPHINXBUILD environment variable to point to the full
path of the '$(SPHINXBUILD)' executable. Alternatively you can add the
directory with the executable to your PATH. If you don't have Sphinx installed,
grab it from http://sphinx-doc.org/)
+endif
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp
epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or
PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " latexpdfja to make LaTeX files and run them through
platex/dvipdfmx"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated
items"
+ @echo " xml to make Docutils-native XML files"
+ @echo " pseudoxml to make pseudoxml-XML files for display purposes"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation
(if enabled)"
+
+clean:
+ rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the"
\
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DjangoCTEForest.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile
$(BUILDDIR)/qthelp/DjangoCTEForest.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/DjangoCTEForest"
+ @echo "# ln -s $(BUILDDIR)/devhelp
$$HOME/.local/share/devhelp/DjangoCTEForest"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+latexpdfja:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through platex and dvipdfmx..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
+
+xml:
+ $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
+ @echo
+ @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
+
+pseudoxml:
+ $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
+ @echo
+ @echo "Build finished. The pseudo-XML files are in
$(BUILDDIR)/pseudoxml."
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/docs/conf.py
new/django-js-asset-4.0.2/docs/conf.py
--- old/django-js-asset-4.0.1/docs/conf.py 1970-01-01 01:00:00.000000000
+0100
+++ new/django-js-asset-4.0.2/docs/conf.py 2026-08-18 08:50:13.000000000
+0200
@@ -0,0 +1,59 @@
+import os
+import sys
+
+
+sys.path.append(os.path.abspath(".."))
+
+extensions = []
+
+templates_path = ["_templates"]
+
+source_suffix = ".rst"
+
+master_doc = "index"
+
+project = "django-js-asset"
+copyright = "2017 - {dt.date.today().year} Feinheit AG"
+
+version = __import__("js_asset").__version__
+release = version
+
+pygments_style = "sphinx"
+
+html_theme = "alabaster"
+
+html_static_path = ["_static"]
+
+htmlhelp_basename = "django-js-assetdoc"
+
+latex_documents = [
+ (
+ "index",
+ "django-js-asset.tex",
+ "form-designer Documentation",
+ "Feinheit AG",
+ "manual",
+ )
+]
+
+man_pages = [
+ (
+ "index",
+ "django-js-asset",
+ "form-designer Documentation",
+ ["Feinheit AG"],
+ 1,
+ )
+]
+
+texinfo_documents = [
+ (
+ "index",
+ "django-js-asset",
+ "form-designer Documentation",
+ "Feinheit AG",
+ "django-js-asset",
+ "A simple form designer for Django",
+ "Miscellaneous",
+ )
+]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/docs/index.rst
new/django-js-asset-4.0.2/docs/index.rst
--- old/django-js-asset-4.0.1/docs/index.rst 1970-01-01 01:00:00.000000000
+0100
+++ new/django-js-asset-4.0.2/docs/index.rst 2026-08-18 08:50:13.000000000
+0200
@@ -0,0 +1,2 @@
+.. include:: ../README.rst
+.. include:: ../CHANGELOG.rst
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/docs/make.bat
new/django-js-asset-4.0.2/docs/make.bat
--- old/django-js-asset-4.0.1/docs/make.bat 1970-01-01 01:00:00.000000000
+0100
+++ new/django-js-asset-4.0.2/docs/make.bat 2026-08-18 08:50:13.000000000
+0200
@@ -0,0 +1,242 @@
+@ECHO OFF
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
+set I18NSPHINXOPTS=%SPHINXOPTS% .
+if NOT "%PAPER%" == "" (
+ set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
+ set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
+)
+
+if "%1" == "" goto help
+
+if "%1" == "help" (
+ :help
+ echo.Please use `make ^<target^>` where ^<target^> is one of
+ echo. html to make standalone HTML files
+ echo. dirhtml to make HTML files named index.html in directories
+ echo. singlehtml to make a single large HTML file
+ echo. pickle to make pickle files
+ echo. json to make JSON files
+ echo. htmlhelp to make HTML files and a HTML help project
+ echo. qthelp to make HTML files and a qthelp project
+ echo. devhelp to make HTML files and a Devhelp project
+ echo. epub to make an epub
+ echo. latex to make LaTeX files, you can set PAPER=a4 or
PAPER=letter
+ echo. text to make text files
+ echo. man to make manual pages
+ echo. texinfo to make Texinfo files
+ echo. gettext to make PO message catalogs
+ echo. changes to make an overview over all changed/added/deprecated
items
+ echo. xml to make Docutils-native XML files
+ echo. pseudoxml to make pseudoxml-XML files for display purposes
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if
enabled
+ goto end
+)
+
+if "%1" == "clean" (
+ for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
+ del /q /s %BUILDDIR%\*
+ goto end
+)
+
+
+%SPHINXBUILD% 2> nul
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively
you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+if "%1" == "html" (
+ %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/html.
+ goto end
+)
+
+if "%1" == "dirhtml" (
+ %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
+ goto end
+)
+
+if "%1" == "singlehtml" (
+ %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
+ goto end
+)
+
+if "%1" == "pickle" (
+ %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the pickle files.
+ goto end
+)
+
+if "%1" == "json" (
+ %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can process the JSON files.
+ goto end
+)
+
+if "%1" == "htmlhelp" (
+ %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run HTML Help Workshop with the ^
+.hhp project file in %BUILDDIR%/htmlhelp.
+ goto end
+)
+
+if "%1" == "qthelp" (
+ %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; now you can run "qcollectiongenerator" with the ^
+.qhcp project file in %BUILDDIR%/qthelp, like this:
+ echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DjangoCTEForest.qhcp
+ echo.To view the help file:
+ echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DjangoCTEForest.ghc
+ goto end
+)
+
+if "%1" == "devhelp" (
+ %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished.
+ goto end
+)
+
+if "%1" == "epub" (
+ %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The epub file is in %BUILDDIR%/epub.
+ goto end
+)
+
+if "%1" == "latex" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdf" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "latexpdfja" (
+ %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
+ cd %BUILDDIR%/latex
+ make all-pdf-ja
+ cd %BUILDDIR%/..
+ echo.
+ echo.Build finished; the PDF files are in %BUILDDIR%/latex.
+ goto end
+)
+
+if "%1" == "text" (
+ %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The text files are in %BUILDDIR%/text.
+ goto end
+)
+
+if "%1" == "man" (
+ %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The manual pages are in %BUILDDIR%/man.
+ goto end
+)
+
+if "%1" == "texinfo" (
+ %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
+ goto end
+)
+
+if "%1" == "gettext" (
+ %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
+ goto end
+)
+
+if "%1" == "changes" (
+ %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.The overview file is in %BUILDDIR%/changes.
+ goto end
+)
+
+if "%1" == "linkcheck" (
+ %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Link check complete; look for any errors in the above output ^
+or in %BUILDDIR%/linkcheck/output.txt.
+ goto end
+)
+
+if "%1" == "doctest" (
+ %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Testing of doctests in the sources finished, look at the ^
+results in %BUILDDIR%/doctest/output.txt.
+ goto end
+)
+
+if "%1" == "xml" (
+ %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The XML files are in %BUILDDIR%/xml.
+ goto end
+)
+
+if "%1" == "pseudoxml" (
+ %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml
+ if errorlevel 1 exit /b 1
+ echo.
+ echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.
+ goto end
+)
+
+:end
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/js_asset/__init__.py
new/django-js-asset-4.0.2/js_asset/__init__.py
--- old/django-js-asset-4.0.1/js_asset/__init__.py 2026-06-11
18:03:46.000000000 +0200
+++ new/django-js-asset-4.0.2/js_asset/__init__.py 2026-08-18
08:50:13.000000000 +0200
@@ -1,4 +1,4 @@
-__version__ = "4.0.1"
+__version__ = "4.0.2"
import contextlib
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/js_asset/js.py
new/django-js-asset-4.0.2/js_asset/js.py
--- old/django-js-asset-4.0.1/js_asset/js.py 2026-06-11 18:03:46.000000000
+0200
+++ new/django-js-asset-4.0.2/js_asset/js.py 2026-08-18 08:50:13.000000000
+0200
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
import json
from dataclasses import dataclass, field
from typing import Any
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/js_asset/media.py
new/django-js-asset-4.0.2/js_asset/media.py
--- old/django-js-asset-4.0.1/js_asset/media.py 2026-06-11 18:03:46.000000000
+0200
+++ new/django-js-asset-4.0.2/js_asset/media.py 2026-08-18 08:50:13.000000000
+0200
@@ -5,7 +5,7 @@
from django.forms.utils import flatatt
from django.utils.html import format_html, mark_safe
-from js_asset._compat import MediaAsset
+from js_asset._compat import MediaAsset, Script, Stylesheet
from js_asset.js import CSS, JS, JSON, ImportMap
@@ -34,6 +34,41 @@
self.nonce = nonce
super().__init__(media=media, css=css, js=js)
+ # -- Normalization ----------------------------------------------------
+
+ # ``forms.Media.__init__`` calls these through ``self``, so a subclass can
+ # override them. They only exist -- and are only reached -- on Django >=
+ # 6.1; older versions keep js/css entries as given and ``_render_{js,css}``
+ # wraps them at render time instead.
+ #
+ # Django 6.1 normalizes with ``isinstance(path, str)``, which swallows
+ # html-safe strings: a ``SafeString`` such as
+ # ``mark_safe('<script defer src="..."></script>')`` is a complete tag, but
+ # being a ``str`` subclass it got turned into a ``Script`` and resolved
+ # through ``static()`` (ticket #37262, fixed for 6.1.1). Normalizing with
+ # the ``__html__`` predicate instead keeps such entries verbatim on 6.1
too.
+ # ``Script``/``Stylesheet`` come from ``_compat``, so the wrapped output is
+ # the same on every supported Django.
+ #
+ # This can only help media built *through* this class. Assets adopted from
+ # a foreign ``forms.Media`` (``from_media``, ``__add__``, and Django's
+ # widget ``media_property``, which always instantiates ``forms.Media``) are
+ # already normalized by the time we see them.
+
+ @staticmethod
+ def _normalize_js(js):
+ return [path if hasattr(path, "__html__") else Script(path) for path
in js]
+
+ @staticmethod
+ def _normalize_css(css):
+ return {
+ medium: [
+ path if hasattr(path, "__html__") else Stylesheet(path,
media=medium)
+ for path in paths
+ ]
+ for medium, paths in css.items()
+ }
+
@classmethod
def from_media(cls, media, *, nonce=""):
"""
@@ -108,7 +143,13 @@
for item in self._js:
if isinstance(item, ImportMap):
continue
- asset = JS(item) if isinstance(item, str) else item
+ # ``hasattr(item, "__html__")`` -- not ``isinstance(item, str)``:
+ # ``SafeString`` is a ``str`` subclass, and html-safe strings such
+ # as ``mark_safe("<script defer src=...></script>")`` are complete
+ # tags which must render verbatim instead of being resolved through
+ # ``static()``. Only bare paths are wrapped. (Django hit the same
+ # trap in 6.1, fixed for 6.1.1 -- ticket #37262.)
+ asset = item if hasattr(item, "__html__") else JS(item)
rendered.append(self._render_asset(asset, nonce))
return rendered
@@ -116,7 +157,8 @@
rendered = []
for medium in sorted(self._css):
for item in self._css[medium]:
- asset = CSS(item, media=medium) if isinstance(item, str) else
item
+ # See ``_render_js`` on the ``__html__`` check.
+ asset = item if hasattr(item, "__html__") else CSS(item,
media=medium)
rendered.append(self._render_asset(asset, nonce))
return rendered
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/tests/testapp/test_media.py
new/django-js-asset-4.0.2/tests/testapp/test_media.py
--- old/django-js-asset-4.0.1/tests/testapp/test_media.py 2026-06-11
18:03:46.000000000 +0200
+++ new/django-js-asset-4.0.2/tests/testapp/test_media.py 2026-08-18
08:50:13.000000000 +0200
@@ -1,8 +1,9 @@
from django.forms import Media as DjangoMedia
from django.test import TestCase
from django.utils.html import html_safe
+from django.utils.safestring import mark_safe
-from js_asset import CSS, JS, JSON, ImportMap, Media
+from js_asset import CSS, JS, JSON, ImportMap, Media, Script, Stylesheet
@html_safe
@@ -20,6 +21,161 @@
return self._markup
+# Django 6.1 normalizes *every* js/css string -- html-safe ones included --
+# into ``Script``/``Stylesheet`` inside ``forms.Media.__init__`` (ticket
#37262,
+# fixed on the 6.1.x branch for 6.1.1). Our ``Media`` overrides the normalizers
+# so media built through *our* class is unaffected, but assets adopted from a
+# foreign ``forms.Media`` arrive already mangled. This probe marks that window.
+_PROBE = mark_safe('<script src="/probe.js"></script>')
+DJANGO_KEEPS_HTML_SAFE_STRINGS = str(DjangoMedia(js=[_PROBE])) == _PROBE
+
+
+HTML_SAFE_JS = mark_safe('<script defer
src="https://example.org/asset.js"></script>')
+HTML_SAFE_CSS = mark_safe(
+ '<link href="https://example.org/asset.css" rel="stylesheet">'
+)
+
+# (label, asset, rendering without a nonce, rendering with nonce="n0nce").
+#
+# One row per kind of asset a ``Media`` can carry. Add a row whenever a new
+# asset kind appears -- this is the guard against a rendering branch (such as
+# the "is it a bare path?" test in ``Media._render_js``/``._render_css``)
+# quietly mishandling one of them.
+JS_ASSETS = [
+ (
+ "bare path string",
+ "app.js",
+ '<script src="/static/app.js"></script>',
+ '<script src="/static/app.js" nonce="n0nce"></script>',
+ ),
+ (
+ "JS factory",
+ JS("app.js"),
+ '<script src="/static/app.js"></script>',
+ '<script src="/static/app.js" nonce="n0nce"></script>',
+ ),
+ (
+ "JS factory with attributes",
+ JS("app.js", {"type": "module"}),
+ '<script src="/static/app.js" type="module"></script>',
+ '<script src="/static/app.js" nonce="n0nce" type="module"></script>',
+ ),
+ (
+ "Script object",
+ Script("app.js"),
+ '<script src="/static/app.js"></script>',
+ '<script src="/static/app.js" nonce="n0nce"></script>',
+ ),
+ (
+ "JSON block",
+ JSON({"a": 1}, id="cfg"),
+ # Data, not executed script: no nonce either way.
+ '<script id="cfg" type="application/json">{"a": 1}</script>',
+ '<script id="cfg" type="application/json">{"a": 1}</script>',
+ ),
+ (
+ "ImportMap",
+ ImportMap({"imports": {"a": "/static/a.js"}}),
+ '<script type="importmap">{"imports": {"a": "/static/a.js"}}</script>',
+ (
+ '<script type="importmap" nonce="n0nce">'
+ '{"imports": {"a": "/static/a.js"}}</script>'
+ ),
+ ),
+ (
+ "object with only __html__",
+ HTMLOnlyAsset('<script src="/bundle.js"></script>'),
+ # Opaque markup: rendered verbatim, and the nonce cannot be threaded in
+ # (same as stock ``forms.Media``).
+ '<script src="/bundle.js"></script>',
+ '<script src="/bundle.js"></script>',
+ ),
+ (
+ "html-safe string",
+ HTML_SAFE_JS,
+ # A ``SafeString`` is a ``str``, but it is a complete tag rather than a
+ # path: it must never be resolved through ``static()``.
+ HTML_SAFE_JS,
+ HTML_SAFE_JS,
+ ),
+]
+
+CSS_ASSETS = [
+ (
+ "bare path string",
+ "app.css",
+ '<link href="/static/app.css" media="all" rel="stylesheet">',
+ '<link href="/static/app.css" media="all" nonce="n0nce"
rel="stylesheet">',
+ ),
+ (
+ "CSS factory",
+ CSS("app.css"),
+ '<link href="/static/app.css" media="all" rel="stylesheet">',
+ '<link href="/static/app.css" media="all" nonce="n0nce"
rel="stylesheet">',
+ ),
+ (
+ # Django's ``Stylesheet`` has no implicit ``media``; unlike the ``CSS``
+ # factory it only emits the attribute when one is passed. The dict key
+ # it is filed under does not add one.
+ "Stylesheet object",
+ Stylesheet("app.css"),
+ '<link href="/static/app.css" rel="stylesheet">',
+ '<link href="/static/app.css" nonce="n0nce" rel="stylesheet">',
+ ),
+ (
+ "inline CSS",
+ CSS("body{color:red}", inline=True),
+ '<style media="all">body{color:red}</style>',
+ '<style media="all" nonce="n0nce">body{color:red}</style>',
+ ),
+ (
+ "object with only __html__",
+ HTMLOnlyAsset('<link href="/bundle.css" rel="stylesheet">'),
+ '<link href="/bundle.css" rel="stylesheet">',
+ '<link href="/bundle.css" rel="stylesheet">',
+ ),
+ (
+ "html-safe string",
+ HTML_SAFE_CSS,
+ HTML_SAFE_CSS,
+ HTML_SAFE_CSS,
+ ),
+]
+
+
+class AssetRenderingTest(TestCase):
+ """
+ Exhaustive per-asset-kind rendering, with and without a CSP nonce.
+ """
+
+ def _check(self, rows, kwargs_for):
+ for label, asset, plain, with_nonce in rows:
+ with self.subTest(asset=label):
+ self.assertEqual(Media(**kwargs_for(asset)).render(), plain)
+ self.assertEqual(
+ Media(nonce="n0nce", **kwargs_for(asset)).render(),
with_nonce
+ )
+
+ def test_js_assets(self):
+ self._check(JS_ASSETS, lambda asset: {"js": [asset]})
+
+ def test_css_assets(self):
+ self._check(CSS_ASSETS, lambda asset: {"css": {"all": [asset]}})
+
+ def test_matches_django_rendering(self):
+ # Our rendering must not drift from stock ``forms.Media`` for the asset
+ # kinds Django itself understands -- this is what catches a future
+ # change to Django's normalization or tag format that our overridden
+ # ``_normalize_{js,css}`` would otherwise silently skip. (Excludes
+ # ``ImportMap``, which we deliberately merge and hoist, and html-safe
+ # strings, which Django 6.1 itself gets wrong.)
+ kwargs = {
+ "css": {"all": ["a.css", CSS("b.css"), Stylesheet("c.css")]},
+ "js": ["a.js", JS("b.js"), Script("c.js"), JS("d.js", {"defer":
True})],
+ }
+ self.assertEqual(str(Media(**kwargs)), str(DjangoMedia(**kwargs)))
+
+
class MediaTest(TestCase):
def test_nonce_applied_to_assets(self):
media = Media(
@@ -181,3 +337,47 @@
'<script src="/static/app.js" nonce="xyz"></script>',
request_media.render(),
)
+
+ def test_html_safe_strings_dedupe_and_merge(self):
+ # Mirrors Django's own #37262 tests: html-safe strings survive
+ # deduplication and media merging alongside regular path assets.
+ first = Media(
+ css={"all": [HTML_SAFE_CSS, "a.css"]},
+ js=["a.js", HTML_SAFE_JS],
+ )
+ second = Media(
+ nonce="n0nce",
+ css={"all": [HTML_SAFE_CSS]},
+ js=[HTML_SAFE_JS, JS("b.js")],
+ )
+ merged = first + second
+
+ self.assertEqual(merged.nonce, "n0nce")
+ self.assertEqual(
+ merged.render(),
+ f"{HTML_SAFE_CSS}\n"
+ '<link href="/static/a.css" media="all" nonce="n0nce"
rel="stylesheet">\n'
+ '<script src="/static/a.js" nonce="n0nce"></script>\n'
+ f"{HTML_SAFE_JS}\n"
+ '<script src="/static/b.js" nonce="n0nce"></script>',
+ )
+
+ def test_html_safe_strings_normalized_through_our_class(self):
+ # We override ``_normalize_{js,css}`` so html-safe strings survive
+ # construction even on Django 6.1, whose own normalizers mangle them.
+ self.assertEqual(Media(js=[HTML_SAFE_JS]).render(), HTML_SAFE_JS)
+ self.assertEqual(
+ Media(media=type("Def", (), {"js": [HTML_SAFE_JS]})).render(),
+ HTML_SAFE_JS,
+ )
+
+ def test_html_safe_strings_adopted_from_foreign_media(self):
+ # ``from_media`` adopts assets a plain ``forms.Media`` already
+ # normalized, so on Django 6.1 the damage is done before we see them.
+ # Nothing to fix from here -- just pin which side the boundary is on.
+ adopted = Media.from_media(DjangoMedia(js=[HTML_SAFE_JS]))
+ if DJANGO_KEEPS_HTML_SAFE_STRINGS:
+ self.assertEqual(adopted.render(), HTML_SAFE_JS)
+ else:
+ self.assertNotEqual(adopted.render(), HTML_SAFE_JS)
+ self.assertEqual(adopted.render(),
str(DjangoMedia(js=[HTML_SAFE_JS])))
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/django-js-asset-4.0.1/tox.ini
new/django-js-asset-4.0.2/tox.ini
--- old/django-js-asset-4.0.1/tox.ini 2026-06-11 18:03:46.000000000 +0200
+++ new/django-js-asset-4.0.2/tox.ini 2026-08-18 08:50:13.000000000 +0200
@@ -19,3 +19,11 @@
dj60: Django>=6.0,<6.1
dj61: Django>=6.1a1,<6.2
djmain: https://github.com/django/django/archive/main.tar.gz
+
+[testenv:docs]
+deps =
+ Sphinx
+changedir = docs
+commands = make html
+skip_install = true
+allowlist_externals = make