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