Skip to content

Commit 493699d

Browse files
committed
Remove automatic extension dependencies
1 parent 4df0d23 commit 493699d

7 files changed

Lines changed: 143 additions & 27 deletions

File tree

CHANGELOG.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ Change log
44
Next version
55
~~~~~~~~~~~~
66

7+
- **Backwards incompatible**: Removed the automatic dependency management of
8+
some extensions. For example, adding ``BulletList`` would automatically add
9+
the ``ListItem`` extension. This didn't work nicely when replacing the
10+
``ListItem`` extension with a hypothetical ``ExtendedListItem`` extension
11+
because then the configuration would contain several list item nodes. The
12+
config resolver tries detecting invalid configurations and warns if it
13+
suspects missing extensions, but that's not completely watertight. The
14+
warnings will be removed after a few releases.
715
- Added a ``NodeClass`` extension for applying CSS classes to nodes.
816

917

django_prose_editor/config.py

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
corresponding sanitization rules for server-side HTML cleaning.
66
"""
77

8+
import warnings
89
from typing import Any
910

1011
from django.conf import settings
@@ -246,8 +247,9 @@ def process_node_class(config, shared_config):
246247
"Placeholder": html_tags([]),
247248
}
248249

249-
# Automatic dependencies (extensions that require other extensions)
250-
EXTENSION_DEPENDENCIES = {
250+
# DEPRECATED: This dictionary is no longer used for automatic dependency management
251+
# but kept for backwards compatibility checking
252+
LEGACY_EXTENSION_DEPENDENCIES = {
251253
"BulletList": ["ListItem"],
252254
"OrderedList": ["ListItem"],
253255
"Table": ["TableRow", "TableHeader", "TableCell"],
@@ -260,7 +262,7 @@ def process_node_class(config, shared_config):
260262

261263
def expand_extensions(extensions: dict[str, Any]) -> dict[str, Any]:
262264
"""
263-
Expand extension configuration by applying defaults and resolving dependencies.
265+
Expand extension configuration by applying defaults.
264266
265267
Args:
266268
extensions: Dictionary of extension configurations
@@ -292,16 +294,52 @@ def expand_extensions(extensions: dict[str, Any]) -> dict[str, Any]:
292294
if config is not False
293295
}
294296

295-
# Resolve dependencies
296-
for extension, deps in EXTENSION_DEPENDENCIES.items():
297-
if extension in expanded:
298-
for dep in deps:
299-
if dep not in expanded:
300-
expanded[dep] = True
297+
# Check for legacy dependency issues and warn
298+
dependency_warnings = check_legacy_dependencies(extensions)
299+
for warning_msg in dependency_warnings:
300+
warnings.warn(
301+
warning_msg,
302+
UserWarning,
303+
stacklevel=3, # Point to the calling code, not this function
304+
)
301305

302306
return expanded
303307

304308

309+
def check_legacy_dependencies(extensions: dict[str, Any]) -> list[str]:
310+
"""
311+
Check if the extension configuration relies on legacy automatic dependency management.
312+
313+
This function identifies configurations that would have worked with automatic
314+
dependency resolution but may now be incomplete.
315+
316+
Args:
317+
extensions: Dictionary of extension configurations (raw, not expanded)
318+
319+
Returns:
320+
List of warning messages about missing dependencies
321+
"""
322+
warnings = []
323+
324+
# Filter enabled extensions (don't call expand_extensions to avoid recursion)
325+
enabled_extensions = {
326+
extension: config
327+
for extension, config in extensions.items()
328+
if config is not False
329+
}
330+
331+
for extension, deps in LEGACY_EXTENSION_DEPENDENCIES.items():
332+
if extension in enabled_extensions:
333+
for dep in deps:
334+
if dep not in enabled_extensions:
335+
warnings.append(
336+
f"Extension '{extension}' typically requires '{dep}' to function properly. "
337+
f"Consider adding '{dep}': True to your configuration."
338+
)
339+
340+
return warnings
341+
342+
305343
def js_from_extensions(
306344
extensions: dict[str, Any],
307345
) -> list[str]:

tests/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Testing
2+
3+
## Running Tests
4+
5+
To run all tests:
6+
```bash
7+
tox -e py313-dj52
8+
```
9+
10+
To run specific test files:
11+
```bash
12+
tox -e py313-dj52 -- tests/testapp/test_config.py -v
13+
```
14+
15+
To run specific test methods:
16+
```bash
17+
tox -e py313-dj52 -- tests/testapp/test_config.py::ConfigFunctionsTestCase::test_expand_extensions_without_auto_dependencies -v
18+
```
19+
20+
You can pass any pytest arguments after the double dash (`--`).

tests/testapp/models.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,16 @@ class TableProseEditorModel(models.Model):
2929
"HorizontalRule",
3030
"Italic",
3131
"Link",
32+
"ListItem",
3233
"OrderedList",
3334
"Strike",
3435
"Subscript",
3536
"Superscript",
3637
"Underline",
3738
"Table",
39+
"TableRow",
40+
"TableHeader",
41+
"TableCell",
3842
],
3943
"history": True,
4044
"html": True,
@@ -52,7 +56,10 @@ class ConfigurableProseEditorModel(models.Model):
5256
"extensions": {
5357
"Bold": True,
5458
"Italic": True,
55-
"Table": True, # This should automatically include TableRow, TableHeader, TableCell
59+
"Table": True,
60+
"TableRow": True,
61+
"TableHeader": True,
62+
"TableCell": True,
5663
"Heading": {"levels": [1, 2, 3]}, # Limit to h1, h2, h3
5764
"BlueBold": True,
5865
"HTML": True,

tests/testapp/test_config.py

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from django_prose_editor.config import (
77
allowlist_from_extensions,
8+
check_legacy_dependencies,
89
expand_extensions,
910
html_tags,
1011
js_from_extensions,
@@ -132,8 +133,8 @@ def test_js_from_extensions_with_invalid_extension(self):
132133
assert isinstance(js_modules, list)
133134
assert js_modules == []
134135

135-
def test_expand_extensions_with_dependencies(self):
136-
"""Test that expand_extensions correctly adds dependent extensions."""
136+
def test_expand_extensions_without_auto_dependencies(self):
137+
"""Test that expand_extensions no longer automatically adds dependent extensions."""
137138
extensions = {
138139
"Bold": True,
139140
"Table": True,
@@ -147,17 +148,59 @@ def test_expand_extensions_with_dependencies(self):
147148
assert "Table" in expanded
148149
assert "Figure" in expanded
149150

150-
# Dependencies should be added
151-
assert "TableRow" in expanded
152-
assert "TableHeader" in expanded
153-
assert "TableCell" in expanded
154-
assert "Caption" in expanded
155-
assert "Image" in expanded
151+
# Dependencies should NOT be automatically added anymore
152+
assert "TableRow" not in expanded
153+
assert "TableHeader" not in expanded
154+
assert "TableCell" not in expanded
155+
assert "Caption" not in expanded
156+
assert "Image" not in expanded
156157

157158
# Core extensions should always be included
158159
assert "Document" in expanded
159160
assert "Paragraph" in expanded
160-
assert "Text" in expanded
161+
162+
def test_check_legacy_dependencies(self):
163+
"""Test that check_legacy_dependencies correctly identifies missing dependencies."""
164+
extensions = {
165+
"Bold": True,
166+
"BulletList": True,
167+
"Table": True,
168+
}
169+
170+
warnings = check_legacy_dependencies(extensions)
171+
172+
# Should warn about missing ListItem for BulletList
173+
assert any(
174+
"BulletList" in warning and "ListItem" in warning for warning in warnings
175+
)
176+
177+
# Should warn about missing Table dependencies
178+
assert any("Table" in warning and "TableRow" in warning for warning in warnings)
179+
assert any(
180+
"Table" in warning and "TableHeader" in warning for warning in warnings
181+
)
182+
assert any(
183+
"Table" in warning and "TableCell" in warning for warning in warnings
184+
)
185+
186+
# Should not warn about Bold (no dependencies)
187+
assert not any("Bold" in warning for warning in warnings)
188+
189+
def test_check_legacy_dependencies_with_explicit_deps(self):
190+
"""Test that no warnings are generated when dependencies are explicitly included."""
191+
extensions = {
192+
"BulletList": True,
193+
"ListItem": True,
194+
"Table": True,
195+
"TableRow": True,
196+
"TableHeader": True,
197+
"TableCell": True,
198+
}
199+
200+
warnings = check_legacy_dependencies(extensions)
201+
202+
# Should have no warnings since all dependencies are explicitly included
203+
assert len(warnings) == 0
161204

162205
def test_disabled_extensions(self):
163206
"""Test that disabled extensions are properly handled."""

tests/testapp/test_configurable.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
class ConfigurableFormTestCase(TestCase):
1212
"""Tests for the configurable field form rendering."""
1313

14-
def test_dependencies_expanded(self):
15-
"""Test that all dependencies are properly expanded in raw_extensions."""
14+
def test_extensions_explicit_configuration(self):
15+
"""Test that extensions are configured as explicitly specified."""
1616

1717
class TestForm(ModelForm):
1818
class Meta:
@@ -27,15 +27,18 @@ class Meta:
2727
context["widget"]["attrs"]["data-django-prose-editor-configurable"]
2828
)
2929

30-
# The original extensions only had Bold, Italic, and Table
30+
# The original extensions should be there
3131
assert "Bold" in config["extensions"]
3232
assert "Italic" in config["extensions"]
3333
assert "Table" in config["extensions"]
3434

3535
# The custom BlueBold extension should also be included
3636
assert "BlueBold" in config["extensions"]
3737

38-
# The following should be included as dependencies
38+
# ListItem should be there because it's explicitly configured
39+
assert "ListItem" in config["extensions"]
40+
41+
# Table dependencies are now explicitly configured in the model
3942
assert "TableRow" in config["extensions"]
4043
assert "TableHeader" in config["extensions"]
4144
assert "TableCell" in config["extensions"]
@@ -77,9 +80,7 @@ class Meta:
7780
"NoSpellCheck": True,
7881
# Enable history by default unless explicitly disabled
7982
"History": True,
80-
"TableRow": True,
81-
"TableCell": True,
82-
"TableHeader": True,
83+
# No automatic dependencies are added
8384
}
8485

8586
def test_sanitization_works(self):

tests/testapp/test_prose_editor_e2e.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,6 @@ def test_nodeclass_textclass(live_server, page):
599599
editor_container = page.locator(".prose-editor")
600600
expect(editor_container).to_be_visible()
601601

602-
page.get_by_role("paragraph").click()
603602
page.get_by_role("textbox").fill("Blubbering Hello World")
604603
page.locator("div").filter(has_text=re.compile(r"^default$")).click()
605604
page.locator("div").filter(has_text=re.compile(r"^Block style$")).click()

0 commit comments

Comments
 (0)