diff --git a/README.md b/README.md
index 9c940b84..ead11709 100644
--- a/README.md
+++ b/README.md
@@ -144,10 +144,10 @@ Django-components supports all supported combinations versions of [Django](https
| Python version | Django version |
|----------------|--------------------------|
-| 3.8 | 3.2, 4.0, 4.1, 4.2 |
-| 3.9 | 3.2, 4.0, 4.1, 4.2 |
-| 3.10 | 3.2, 4.0, 4.1, 4.2, 5.0 |
-| 3.11 | 4.1, 4.2, 5.0 |
+| 3.8 | 4.2 |
+| 3.9 | 4.2 |
+| 3.10 | 4.2, 5.0 |
+| 3.11 | 4.2, 5.0 |
| 3.12 | 4.2, 5.0 |
## Create your first component
diff --git a/benchmarks/component_rendering.py b/benchmarks/component_rendering.py
index 5d8f834c..1ecf12ec 100644
--- a/benchmarks/component_rendering.py
+++ b/benchmarks/component_rendering.py
@@ -6,8 +6,7 @@ from django.test import override_settings
from django_components import component
from django_components.middleware import CSS_DEPENDENCY_PLACEHOLDER, JS_DEPENDENCY_PLACEHOLDER
from tests.django_test_setup import * # NOQA
-from tests.testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
-from tests.testutils import create_and_process_template_response
+from tests.testutils import BaseTestCase, create_and_process_template_response
class SlottedComponent(component.Component):
@@ -67,7 +66,7 @@ EXPECTED_JS = """"""
@override_settings(COMPONENTS={"RENDER_DEPENDENCIES": True})
-class RenderBenchmarks(SimpleTestCase):
+class RenderBenchmarks(BaseTestCase):
def setUp(self):
component.registry.clear()
component.registry.register("test_component", SlottedComponent)
diff --git a/pyproject.toml b/pyproject.toml
index 2e50375d..e5ebed9d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,9 +14,6 @@ authors = [
]
classifiers = [
"Framework :: Django",
- "Framework :: Django :: 3.2",
- "Framework :: Django :: 4.0",
- "Framework :: Django :: 4.1",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.0",
"Operating System :: OS Independent",
@@ -29,7 +26,7 @@ classifiers = [
"Programming Language :: Python :: 3.12",
]
dependencies = [
- 'Django>=3.2',
+ 'Django>=4.2',
]
license = {text = "MIT"}
diff --git a/scripts/supported_versions.py b/scripts/supported_versions.py
index 0a27e2ad..645af072 100644
--- a/scripts/supported_versions.py
+++ b/scripts/supported_versions.py
@@ -1,7 +1,7 @@
import re
import textwrap
from collections import defaultdict
-from typing import Dict, List, Tuple
+from typing import Any, Callable, Dict, List, Tuple
from urllib import request
Version = Tuple[int, ...]
@@ -37,7 +37,7 @@ def get_python_supported_version(url: str) -> list[Version]:
return parse_supported_versions(content)
-def get_supported_versions(url: str):
+def get_django_to_pythoon_versions(url: str):
with request.urlopen(url) as response:
response_content = response.read()
@@ -66,6 +66,32 @@ def get_supported_versions(url: str):
return parse_supported_versions(content)
+def get_django_supported_versions(url: str) -> List[Tuple[int, ...]]:
+ """Extract Django versions from the HTML content, e.g. `5.0` or `4.2`"""
+ with request.urlopen(url) as response:
+ response_content = response.read()
+
+ content = response_content.decode("utf-8")
+ content = cut_by_content(
+ content,
+ "
",
+ )
+
+ rows = re.findall(r"(.*?)
", content.replace("\n", " "))
+ versions: List[Tuple[int, ...]] = []
+ # NOTE: Skip first row as that's headers
+ for row in rows[1:]:
+ data: List[str] = re.findall(r"(.*?) | ", row)
+ # NOTE: First column is version like `5.0` or `4.2 LTS`
+ version_with_test = data[0]
+ version = version_with_test.split(" ")[0]
+ version_tuple = tuple(map(int, version.split(".")))
+ versions.append(version_tuple)
+
+ return versions
+
+
def get_latest_version(url: str):
with request.urlopen(url) as response:
response_content = response.read()
@@ -200,14 +226,20 @@ def build_ci_python_versions(python_to_django: Dict[str, str]):
return lines_formatted
+def filter_dict(d: Dict, filter_fn: Callable[[Any], bool]):
+ return dict(filter(filter_fn, d.items()))
+
+
def main():
active_python = get_python_supported_version("https://devguide.python.org/versions/")
- django_to_python = get_supported_versions("https://docs.djangoproject.com/en/dev/faq/install/")
+ django_to_python = get_django_to_pythoon_versions("https://docs.djangoproject.com/en/dev/faq/install/")
+ django_supported_versions = get_django_supported_versions("https://www.djangoproject.com/download/")
latest_version = get_latest_version("https://www.djangoproject.com/download/")
- python_to_django = build_python_to_django(django_to_python, latest_version)
+ supported_django_to_python = filter_dict(django_to_python, lambda item: item[0] in django_supported_versions)
+ python_to_django = build_python_to_django(supported_django_to_python, latest_version)
- python_to_django = dict(filter(lambda item: item[0] in active_python, python_to_django.items()))
+ python_to_django = filter_dict(python_to_django, lambda item: item[0] in active_python)
tox_envlist = build_tox_envlist(python_to_django)
print("Add this to tox.ini:\n")
diff --git a/tests/test_autodiscover.py b/tests/test_autodiscover.py
index e9c789ca..9abde896 100644
--- a/tests/test_autodiscover.py
+++ b/tests/test_autodiscover.py
@@ -6,7 +6,7 @@ from django.urls import include, path
# isort: off
from .django_test_setup import settings
-from .testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
+from .testutils import BaseTestCase
# isort: on
@@ -18,7 +18,7 @@ urlpatterns = [
]
-class TestAutodiscover(SimpleTestCase):
+class TestAutodiscover(BaseTestCase):
def setUp(self):
settings.SETTINGS_MODULE = "tests.test_autodiscover" # noqa
@@ -38,7 +38,7 @@ class TestAutodiscover(SimpleTestCase):
self.assertEqual(imported_components_count, 1)
-class TestLoaderSettingsModule(SimpleTestCase):
+class TestLoaderSettingsModule(BaseTestCase):
def tearDown(self) -> None:
del settings.SETTINGS_MODULE # noqa
@@ -106,7 +106,7 @@ class TestLoaderSettingsModule(SimpleTestCase):
)
-class TestBaseDir(SimpleTestCase):
+class TestBaseDir(BaseTestCase):
def setUp(self):
settings.BASE_DIR = Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" # noqa
settings.SETTINGS_MODULE = "tests_fake.test_autodiscover_fake" # noqa
@@ -125,7 +125,7 @@ class TestBaseDir(SimpleTestCase):
self.assertEqual(sorted(dirs), sorted(expected))
-class TestFilepathToPythonModule(SimpleTestCase):
+class TestFilepathToPythonModule(BaseTestCase):
def test_prepares_path(self):
self.assertEqual(
_filepath_to_python_module(Path("tests.py")),
diff --git a/tests/test_component.py b/tests/test_component.py
index 0dbff29b..e48965c2 100644
--- a/tests/test_component.py
+++ b/tests/test_component.py
@@ -8,14 +8,14 @@ from django.test import override_settings
# isort: off
from .django_test_setup import * # NOQA
-from .testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
+from .testutils import BaseTestCase
# isort: on
from django_components import component
-class ComponentTest(SimpleTestCase):
+class ComponentTest(BaseTestCase):
def test_empty_component(self):
class EmptyComponent(component.Component):
pass
@@ -260,7 +260,7 @@ class ComponentTest(SimpleTestCase):
)
-class InlineComponentTest(SimpleTestCase):
+class InlineComponentTest(BaseTestCase):
def test_inline_html_component(self):
class InlineHTMLComponent(component.Component):
template = "Hello Inline
"
@@ -382,7 +382,7 @@ class InlineComponentTest(SimpleTestCase):
)
-class ComponentMediaTests(SimpleTestCase):
+class ComponentMediaTests(BaseTestCase):
def test_component_media_with_strings(self):
class SimpleComponent(component.Component):
class Media:
@@ -491,7 +491,7 @@ class ComponentMediaTests(SimpleTestCase):
)
-class ComponentIsolationTests(SimpleTestCase):
+class ComponentIsolationTests(BaseTestCase):
def setUp(self):
class SlottedComponent(component.Component):
template_name = "slotted_template.html"
@@ -539,7 +539,7 @@ class ComponentIsolationTests(SimpleTestCase):
)
-class SlotBehaviorTests(SimpleTestCase):
+class SlotBehaviorTests(BaseTestCase):
def setUp(self):
class SlottedComponent(component.Component):
template_name = "slotted_template.html"
diff --git a/tests/test_component_as_view.py b/tests/test_component_as_view.py
index aa9eaad8..614d9c1a 100644
--- a/tests/test_component_as_view.py
+++ b/tests/test_component_as_view.py
@@ -7,7 +7,7 @@ from django.urls import include, path
# isort: off
from .django_test_setup import * # noqa
-from .testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
+from .testutils import BaseTestCase
# isort: on
@@ -110,7 +110,7 @@ class CustomClient(Client):
super().__init__(*args, **kwargs)
-class TestComponentAsView(SimpleTestCase):
+class TestComponentAsView(BaseTestCase):
def setUp(self):
self.client = CustomClient()
diff --git a/tests/test_context.py b/tests/test_context.py
index 4a58a0cc..4c2629e5 100644
--- a/tests/test_context.py
+++ b/tests/test_context.py
@@ -6,7 +6,7 @@ from django.test import override_settings
from django_components import component
from .django_test_setup import * # NOQA
-from .testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
+from .testutils import BaseTestCase
class SimpleComponent(component.Component):
@@ -77,7 +77,7 @@ component.registry.register(name="simple_component", component=SimpleComponent)
component.registry.register(name="outer_context_component", component=OuterContextComponent)
-class ContextTests(SimpleTestCase):
+class ContextTests(BaseTestCase):
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(
self,
):
@@ -198,7 +198,7 @@ class ContextTests(SimpleTestCase):
self.assertNotIn("Shadowing variable = NOT SHADOWED
", rendered, rendered)
-class ParentArgsTests(SimpleTestCase):
+class ParentArgsTests(BaseTestCase):
def test_parent_args_can_be_drawn_from_context(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
@@ -241,7 +241,7 @@ class ParentArgsTests(SimpleTestCase):
self.assertNotIn("Shadowing variable = NOT SHADOWED
", rendered, rendered)
-class ContextCalledOnceTests(SimpleTestCase):
+class ContextCalledOnceTests(BaseTestCase):
def test_one_context_call_with_simple_component(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
@@ -302,7 +302,7 @@ class ContextCalledOnceTests(SimpleTestCase):
)
-class ComponentsCanAccessOuterContext(SimpleTestCase):
+class ComponentsCanAccessOuterContext(BaseTestCase):
def test_simple_component_can_use_outer_context(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
@@ -312,7 +312,7 @@ class ComponentsCanAccessOuterContext(SimpleTestCase):
self.assertIn("outer_value", rendered, rendered)
-class IsolatedContextTests(SimpleTestCase):
+class IsolatedContextTests(BaseTestCase):
def test_simple_component_can_pass_outer_context_in_args(self):
template = Template(
"{% load component_tags %}{% component_dependencies %}"
@@ -330,7 +330,7 @@ class IsolatedContextTests(SimpleTestCase):
self.assertNotIn("outer_value", rendered, rendered)
-class IsolatedContextSettingTests(SimpleTestCase):
+class IsolatedContextSettingTests(BaseTestCase):
def setUp(self):
self.patcher = patch(
"django_components.app_settings.AppSettings.CONTEXT_BEHAVIOR",
@@ -385,7 +385,7 @@ class IsolatedContextSettingTests(SimpleTestCase):
self.assertNotIn("outer_value", rendered, rendered)
-class OuterContextPropertyTests(SimpleTestCase):
+class OuterContextPropertyTests(BaseTestCase):
@override_settings(
COMPONENTS={"context_behavior": "global"},
)
diff --git a/tests/test_dependency_rendering.py b/tests/test_dependency_rendering.py
index b91ce8be..c78038d8 100644
--- a/tests/test_dependency_rendering.py
+++ b/tests/test_dependency_rendering.py
@@ -9,8 +9,7 @@ from django_components.middleware import ComponentDependencyMiddleware
from .django_test_setup import * # NOQA
from .test_templatetags import SimpleComponent
-from .testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
-from .testutils import create_and_process_template_response
+from .testutils import BaseTestCase, create_and_process_template_response
class SimpleComponentAlternate(component.Component):
@@ -44,7 +43,7 @@ class MultistyleComponent(component.Component):
@override_settings(COMPONENTS={"RENDER_DEPENDENCIES": True})
-class ComponentMediaRenderingTests(SimpleTestCase):
+class ComponentMediaRenderingTests(BaseTestCase):
def setUp(self):
# NOTE: component.registry is global, so need to clear before each test
component.registry.clear()
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 2badf400..46f93490 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -1,10 +1,10 @@
from django.conf import settings
from .django_test_setup import * # NOQA
-from .testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
+from .testutils import BaseTestCase
-class ValidateWrongContextBehaviorValueTestCase(SimpleTestCase):
+class ValidateWrongContextBehaviorValueTestCase(BaseTestCase):
def setUp(self) -> None:
settings.COMPONENTS["context_behavior"] = "invalid_value"
return super().setUp()
@@ -20,7 +20,7 @@ class ValidateWrongContextBehaviorValueTestCase(SimpleTestCase):
app_settings.CONTEXT_BEHAVIOR
-class ValidateCorrectContextBehaviorValueTestCase(SimpleTestCase):
+class ValidateCorrectContextBehaviorValueTestCase(BaseTestCase):
def setUp(self) -> None:
settings.COMPONENTS["context_behavior"] = "isolated"
return super().setUp()
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py
index 62c71b0c..937a91b2 100644
--- a/tests/test_templatetags.py
+++ b/tests/test_templatetags.py
@@ -6,7 +6,7 @@ from django.template import Context, Template, TemplateSyntaxError
# isort: off
from .django_test_setup import * # NOQA
-from .testutils import Django30CompatibleSimpleTestCase as SimpleTestCase
+from .testutils import BaseTestCase
# isort: on
@@ -85,7 +85,7 @@ class ComponentWithDefaultAndRequiredSlot(component.Component):
template_name = "template_with_default_and_required_slot.html"
-class ComponentTemplateTagTest(SimpleTestCase):
+class ComponentTemplateTagTest(BaseTestCase):
def setUp(self):
# NOTE: component.registry is global, so need to clear before each test
component.registry.clear()
@@ -202,7 +202,7 @@ class ComponentTemplateTagTest(SimpleTestCase):
template.render(Context({}))
-class ComponentSlottedTemplateTagTest(SimpleTestCase):
+class ComponentSlottedTemplateTagTest(BaseTestCase):
def setUp(self):
# NOTE: component.registry is global, so need to clear before each test
component.registry.clear()
@@ -522,7 +522,7 @@ class ComponentSlottedTemplateTagTest(SimpleTestCase):
raise e
-class SlottedTemplateRegressionTests(SimpleTestCase):
+class SlottedTemplateRegressionTests(BaseTestCase):
def setUp(self):
# NOTE: component.registry is global, so need to clear before each test
component.registry.clear()
@@ -565,7 +565,7 @@ class SlottedTemplateRegressionTests(SimpleTestCase):
)
-class MultiComponentTests(SimpleTestCase):
+class MultiComponentTests(BaseTestCase):
def setUp(self):
component.registry.clear()
@@ -623,7 +623,7 @@ class MultiComponentTests(SimpleTestCase):
self.assertHTMLEqual(rendered, self.expected_result("", second_slot_content))
-class TemplateInstrumentationTest(SimpleTestCase):
+class TemplateInstrumentationTest(BaseTestCase):
saved_render_method: Callable # Assigned during setup.
@classmethod
@@ -685,7 +685,7 @@ class TemplateInstrumentationTest(SimpleTestCase):
self.assertIn("simple_template.html", templates_used)
-class NestedSlotTests(SimpleTestCase):
+class NestedSlotTests(BaseTestCase):
class NestedComponent(component.Component):
template_name = "nested_slot_template.html"
@@ -744,7 +744,7 @@ class NestedSlotTests(SimpleTestCase):
self.assertHTMLEqual(rendered, "Override
")
-class ConditionalSlotTests(SimpleTestCase):
+class ConditionalSlotTests(BaseTestCase):
class ConditionalComponent(component.Component):
template_name = "conditional_template.html"
@@ -819,7 +819,7 @@ class ConditionalSlotTests(SimpleTestCase):
self.assertHTMLEqual(rendered, 'Override A
Override B
')
-class SlotSuperTests(SimpleTestCase):
+class SlotSuperTests(BaseTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
@@ -906,7 +906,7 @@ class SlotSuperTests(SimpleTestCase):
)
-class TemplateSyntaxErrorTests(SimpleTestCase):
+class TemplateSyntaxErrorTests(BaseTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
@@ -1013,7 +1013,7 @@ class TemplateSyntaxErrorTests(SimpleTestCase):
).render(Context({}))
-class ComponentNestingTests(SimpleTestCase):
+class ComponentNestingTests(BaseTestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
@@ -1081,7 +1081,7 @@ class ComponentNestingTests(SimpleTestCase):
self.assertHTMLEqual(rendered, expected)
-class ConditionalIfFilledSlotsTests(SimpleTestCase):
+class ConditionalIfFilledSlotsTests(BaseTestCase):
class ComponentWithConditionalSlots(component.Component):
template_name = "template_with_conditional_slots.html"
@@ -1197,7 +1197,7 @@ class ConditionalIfFilledSlotsTests(SimpleTestCase):
self.assertHTMLEqual(rendered, expected)
-class RegressionTests(SimpleTestCase):
+class RegressionTests(BaseTestCase):
"""Ensure we don't break the same thing AGAIN."""
def setUp(self):
@@ -1244,7 +1244,7 @@ class RegressionTests(SimpleTestCase):
self.assertHTMLEqual(rendered, expected)
-class IterationFillTest(SimpleTestCase):
+class IterationFillTest(BaseTestCase):
"""Tests a behaviour of {% fill .. %} tag which is inside a template {% for .. %} loop."""
class ComponentSimpleSlotInALoop(django_components.component.Component):
diff --git a/tests/testutils.py b/tests/testutils.py
index da49f405..f45b335f 100644
--- a/tests/testutils.py
+++ b/tests/testutils.py
@@ -2,7 +2,7 @@ from unittest.mock import Mock
from django.template import Context
from django.template.response import TemplateResponse
-from django.test import SimpleTestCase, TestCase
+from django.test import SimpleTestCase
from django_components.middleware import ComponentDependencyMiddleware
@@ -11,21 +11,8 @@ response_stash = None
middleware = ComponentDependencyMiddleware(get_response=lambda _: response_stash)
-class Django30CompatibleSimpleTestCase(SimpleTestCase):
- def assertHTMLEqual(self, left, right):
- left = left.replace(' type="text/javascript"', "")
- left = left.replace(' type="text/css"', "")
- right = right.replace(' type="text/javascript"', "")
- right = right.replace(' type="text/css"', "")
- super(Django30CompatibleSimpleTestCase, self).assertHTMLEqual(left, right)
-
- def assertInHTML(self, needle, haystack, count=None, msg_prefix=""):
- haystack = haystack.replace(' type="text/javascript"', "")
- haystack = haystack.replace(' type="text/css"', "")
- super().assertInHTML(needle, haystack, count, msg_prefix)
-
-
-class Django30CompatibleTestCase(Django30CompatibleSimpleTestCase, TestCase):
+# TODO: Use this class to manage component registry cleanup before/after tests.
+class BaseTestCase(SimpleTestCase):
pass
diff --git a/tox.ini b/tox.ini
index dcd768ee..4c60f1b0 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,10 +4,10 @@
[tox]
envlist =
- py38-django{32,40,41,42}
- py39-django{32,40,41,42}
- py310-django{32,40,41,42,50}
- py311-django{41,42,50}
+ py38-django{42}
+ py39-django{42}
+ py310-django{42,50}
+ py311-django{42,50}
py312-django{42,50}
flake8
isort
@@ -16,10 +16,10 @@ envlist =
[gh-actions]
python =
- 3.8: py38-django{32,40,41,42}
- 3.9: py39-django{32,40,41,42}
- 3.10: py310-django{32,40,41,42,50}
- 3.11: py311-django{41,42,50}
+ 3.8: py38-django{42}
+ 3.9: py39-django{42}
+ 3.10: py310-django{42,50}
+ 3.11: py311-django{42,50}
3.12: py312-django{42,50}, flake8, isort, coverage, mypy
fail_on_no_env = True
@@ -29,9 +29,6 @@ isolated_build = true
package = wheel
wheel_build_env = .pkg
deps =
- django32: Django>=3.2,<3.3
- django40: Django>=4.0,<4.1
- django41: Django>=4.1,<4.2
django42: Django>=4.2,<4.3
django50: Django>=5.0,<5.1
pytest