diff --git a/dsfr/enums.py b/dsfr/enums.py
index f2f9960a5..cb6c7a04b 100644
--- a/dsfr/enums.py
+++ b/dsfr/enums.py
@@ -1,3 +1,4 @@
+from django import VERSION
from django.db import models
from django.utils.safestring import mark_safe
from django.utils.version import PY311
@@ -7,6 +8,10 @@
else:
from types import DynamicClassAttribute as enum_property
+if VERSION >= (5, 0):
+ from django.db.models.enums import ChoicesType
+else:
+ from django.db.models.enums import ChoicesMeta as ChoicesType
__all__ = ["ExtendedChoices", "RichRadioButtonChoices"]
@@ -31,7 +36,7 @@ def _is_sunder(name):
)
-class _ExtendedChoicesType(models.enums.ChoicesType):
+class _ExtendedChoicesType(ChoicesType):
@classmethod
def __prepare__(metacls, cls, bases, **kwds):
classdict = super().__prepare__(cls, bases, **kwds)
@@ -49,8 +54,22 @@ def __init__(self, old_classdict):
def __setitem__(self, member, value):
"""Allows to handle declaring enum members as dicts"""
- # _additional_attributes is also a dict, but we don't want it to be
- # processed like an enum member
+ if not PY311 and isinstance(value, (list, tuple)):
+ # Prior to Python 3.11, EnumDict does not interpret auto() when
+ # wrapped in a tuple so we need to set the value alone a first time
+ # before wrapping it in a tuple.
+ if len(value) == 1:
+ return super().__setitem__(member, value[0])
+
+ if len(value) == 2:
+ value, label = value
+ else:
+ *value, label = value
+
+ super().__setitem__(member, value)
+ dict.__setitem__(self, member, (self[member], label))
+ return
+
if not isinstance(value, dict):
return super().__setitem__(member, value)
@@ -62,12 +81,22 @@ def __setitem__(self, member, value):
)
)
- if "label" in value:
- super().__setitem__(
- member, (value.pop("value"), value.pop("label"))
- )
+ if PY311:
+ if "label" in value:
+ super().__setitem__(
+ member, (value.pop("value"), value.pop("label"))
+ )
+ else:
+ super().__setitem__(member, value.pop("value"))
else:
+ # Prior to Python 3.11, EnumDict does not interpret auto() when
+ # wrapped in a tuple so we need to set the value alone a first time
+ # before wrapping it in a tuple.
super().__setitem__(member, value.pop("value"))
+ if "label" in value:
+ dict.__setitem__(
+ self, member, (self[member], value.pop("label"))
+ )
for attr_name, attr_value in value.items():
if _is_sunder(attr_name) or _is_dunder(attr_name):
@@ -87,8 +116,27 @@ def __new__(metacls, classname, bases, classdict, **kwds):
cls = super().__new__(metacls, classname, bases, classdict, **kwds)
cls._additional_attributes = list(classdict._additional_attributes.keys())
- for attr_name, attr_value in classdict._additional_attributes.items():
- for instance in cls:
+ def instance_property_getter(name):
+ @enum_property
+ def _instance_property_getter(enum_item):
+ private_name = enum_item.private_variable_name(name)
+ if hasattr(enum_item, private_name):
+ return getattr(enum_item, private_name)
+ elif hasattr(enum_item, "dynamic_attribute_value"):
+ return enum_item.dynamic_attribute_value(name)
+ else:
+ raise AttributeError(
+ (
+ "{}.{} does not contain key '{}'. Please add key or "
+ "implement a 'dynamic_attribute_value(self, name)' method "
+ "in you enum to provide a value"
+ ).format(cls.__name__, instance.name, name)
+ )
+
+ return _instance_property_getter
+
+ for instance in cls:
+ for attr_name, attr_value in classdict._additional_attributes.items():
if hasattr(instance, cls.private_variable_name(attr_name)):
raise ValueError(
(
@@ -110,27 +158,8 @@ def __new__(metacls, classname, bases, classdict, **kwds):
attr_value[instance.name],
)
- def instance_property_getter(name):
- @enum_property
- def _instance_property_getter(enum_item):
- private_name = enum_item.private_variable_name(name)
- if hasattr(enum_item, private_name):
- return getattr(enum_item, private_name)
- elif hasattr(enum_item, "dynamic_attribute_value"):
- return enum_item.dynamic_attribute_value(name)
- else:
- raise AttributeError(
- (
- "{}.{} does not contain key '{}'. Please add key or "
- "implement a 'dynamic_attribute_value(self, name)' "
- "method in you enum to provide a value"
- ).format(cls.__name__, instance.name, name)
- )
-
- return _instance_property_getter
-
- if not hasattr(instance, attr_name):
- setattr(cls, attr_name, instance_property_getter(attr_name))
+ if not hasattr(instance, attr_name):
+ setattr(cls, attr_name, instance_property_getter(attr_name))
return cls
@@ -158,7 +187,7 @@ def pictogram_alt(self):
@enum_property
def html_label(self):
return (
- mark_safe(getattr(self, self.private_variable_name("html_label")))
+ mark_safe(getattr(self, self.private_variable_name("html_label"))) # nosec
if hasattr(self, self.private_variable_name("html_label"))
else self.label
)
diff --git a/dsfr/models.py b/dsfr/models.py
index 7f2af0173..4399a6f4c 100644
--- a/dsfr/models.py
+++ b/dsfr/models.py
@@ -1,144 +1,13 @@
-import functools
import os
-from typing import Any
+
from django.core.exceptions import ValidationError
from django.db import models
-from django.db.models import TextChoices
-from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
-from django.utils.version import PY311
from dsfr.constants import DJANGO_DSFR_LANGUAGES, NOTICE_TYPE_CHOICES
-if PY311:
- from enum import property as enum_property
-else:
- from types import DynamicClassAttribute as enum_property
-
-
-class _ExtendedChoicesType(models.enums.ChoicesType):
- def __new__(metacls, classname, bases, classdict, **kwds):
- dynamic_attributes = {}
- for member in classdict._member_names:
- value = classdict[member]
- if isinstance(value, dict):
- value = value.copy()
- if "value" not in value:
- raise ValueError(
- "enum value for {member} should contain member 'value' "
- "when using a dict as value; got {member} = {value}".format(
- member=member, value=repr(value)
- )
- )
-
- dict.__setitem__(classdict, member, metacls.get_value_from_dict(value))
- value.pop("value")
- value.pop("label", None)
-
- for k, v in value.items():
- if metacls.is_sunder(k) or metacls.is_dunder(k):
- raise ValueError(
- (
- "enum value for {member} contains key {key}. "
- "Names surrounded with single or double underscores are "
- "not authorized as dict values"
- ).format(member=member, key=k)
- )
- dynamic_attributes.setdefault(k, {})
- dynamic_attributes[k][member] = v
-
- classdict._last_values = [
- metacls.get_value_from_dict(item) for item in classdict._last_values
- ]
-
- cls = super().__new__(metacls, classname, bases, classdict, **kwds)
-
- metacls.set_dynamic_attributes(cls, dynamic_attributes)
-
- return cls
-
- @staticmethod
- def set_dynamic_attributes(cls, dynamic_attributes: dict[str, dict[str, Any]]):
- cls.NO_VALUE = object()
-
- for k, v in dynamic_attributes.items():
- variable = "_{}_".format(k)
- for instance in cls:
- if hasattr(instance, variable):
- raise ValueError(
- (
- "Can't set {} on {} members; please choose a different name "
- "or remove from the member value"
- ).format(variable, cls.__name__)
- )
- setattr(instance, variable, v.get(instance.name, cls.NO_VALUE))
-
- def _getter(name, self):
- result = getattr(self, name, cls.NO_VALUE)
- if result is cls.NO_VALUE:
- raise AttributeError(
- "{} not present in {}.{}".format(
- variable, cls.__name__, self.name
- )
- )
- return result
-
- setattr(cls, k, enum_property(functools.partial(_getter, variable)))
-
- @staticmethod
- def get_value_from_dict(value):
- if not isinstance(value, dict):
- return value
- elif "label" in value:
- return value["value"], value["label"]
- else:
- return value["value"]
-
- @staticmethod
- def is_dunder(name):
- """
- Returns True if a __dunder__ name, False otherwise.
- """
- return (
- len(name) > 4
- and name[:2] == name[-2:] == "__"
- and name[2] != "_"
- and name[-3] != "_"
- )
-
- @staticmethod
- def is_sunder(name):
- """
- Returns True if a _sunder_ name, False otherwise.
- """
- return (
- len(name) > 2
- and name[0] == name[-1] == "_"
- and name[1:2] != "_"
- and name[-2:-1] != "_"
- )
-
-
-class ExtendedChoices(models.Choices, metaclass=_ExtendedChoicesType):
- ...
-
-
-class RichRadioButton(ExtendedChoices, TextChoices):
- @enum_property
- def pictogram(self):
- return self._pictogram_ if hasattr(self, "_pictogram_") else ""
-
- @enum_property
- def html_label(self):
- return (
- mark_safe(self._html_label_)
- if hasattr(self, "_html_label_")
- else self.label
- )
-
-
def validate_image_extension(value):
ext = os.path.splitext(value.name)[1] # [0] returns path+filename
valid_extensions = [".jpg", ".jpeg", ".png", ".svg"]
diff --git a/dsfr/test/test_enums.py b/dsfr/test/test_enums.py
index d78152d5c..c53e9c22e 100644
--- a/dsfr/test/test_enums.py
+++ b/dsfr/test/test_enums.py
@@ -1,4 +1,5 @@
-from enum import auto, nonmember
+from enum import auto
+from unittest import skipIf
from django.db.models import IntegerChoices
from django.test import SimpleTestCase
@@ -8,7 +9,7 @@
from dsfr.enums import ExtendedChoices
if PY311:
- from enum import property as enum_property
+ from enum import property as enum_property, nonmember
else:
from types import DynamicClassAttribute as enum_property
@@ -62,6 +63,7 @@ class TestExtendedChoices(ExtendedChoices, IntegerChoices):
{it.additionnal_attribute_2 for it in TestExtendedChoices},
)
+ @skipIf(not PY311, "'enum.nonmember' was added to Python 3.11")
def test_nonmember_attributes(self):
class TestExtendedChoices(ExtendedChoices, IntegerChoices):
TEST_1 = {"value": auto()}
@@ -102,11 +104,10 @@ class TestExtendedChoices(ExtendedChoices, IntegerChoices):
TEST_2 = auto()
def dynamic_attribute_value(self, name):
- match name:
- case "additionnal_attribute":
- return {"lorem": "ipsum {}".format(self.value)}
- case _:
- return {"lorem": "ipsum x"}
+ if name == "additionnal_attribute":
+ return {"lorem": "ipsum {}".format(self.value)}
+ else:
+ return {"lorem": "ipsum x"}
self.assertEqual(
[{"lorem": "ipsum 1"}, {"lorem": "ipsum 2"}],
diff --git a/dsfr/test/test_models.py b/dsfr/test/test_models.py
deleted file mode 100644
index 2fe477841..000000000
--- a/dsfr/test/test_models.py
+++ /dev/null
@@ -1,126 +0,0 @@
-from enum import auto
-
-from django.db.models import TextChoices
-from django.test import SimpleTestCase
-from django.utils.safestring import mark_safe
-
-from dsfr import models
-from dsfr.models import RichRadioButton
-
-
-class ExtendedChoicesTestCase(SimpleTestCase):
- def test_class_must_contain_value(self):
- with self.assertRaises(ValueError):
-
- class TestExtendedChoices(models.ExtendedChoices):
- ITEM_1 = {"label": "Item 1"}
-
- def test_class_must_contain_no_special_value(self):
- with self.subTest("No _sunder_"):
- with self.assertRaises(ValueError):
- class TestExtendedChoices(models.ExtendedChoices):
- ITEM_1 = {"value": "Item_1", "_label_": "Item 1"}
-
- with self.subTest("No __dunder_"):
- with self.assertRaises(ValueError):
- class TestExtendedChoices2(models.ExtendedChoices):
- ITEM_1 = {"value": "Item_1", "__label__": "Item 1"}
-
- def test_class_declaration_no_parent(self):
- class TestExtendedChoices(models.ExtendedChoices):
- ITEM_1 = {
- "value": "Item_1",
- "label": "Item 1",
- "img": "/static/images/item1.png",
- "html_label": mark_safe("Item 1"),
- }
- ITEM_2 = {
- "value": "Item_2",
- "label": "Item 2",
- "img": "/static/images/item1.png",
- }
- ITEM_3 = "ITEM_3", "Item 3"
- ITEM_4 = "ITEM_4"
- ITEM_5 = auto(), "Item 5"
-
- self.assertSetEqual(
- {
- (("Item_1",), "Item 1"),
- (("Item_2",), "Item 2"),
- ("ITEM_4", "Item 4"),
- (("ITEM_3",), "Item 3"),
- ((1,), "Item 5"),
- },
- set(TestExtendedChoices.choices),
- )
-
- def test_class_declaration_TextChoices(self):
- class TestExtendedChoices(models.ExtendedChoices, TextChoices):
- ITEM_1 = {
- "value": "Item_1",
- "label": "Item 1",
- "img": "/static/images/item1.png",
- "html_label": mark_safe("Item 1"),
- }
- ITEM_2 = {
- "value": "Item_2",
- "label": "Item 2",
- "img": "/static/images/item1.png",
- }
- ITEM_3 = "ITEM_3", "Item 3"
- ITEM_4 = "ITEM_4"
- ITEM_5 = auto(), "Item 5"
-
- self.assertSetEqual(
- {
- ("Item_1", "Item 1"),
- ("Item_2", "Item 2"),
- ("ITEM_3", "Item 3"),
- ("ITEM_4", "Item 4"),
- ("ITEM_5", "Item 5"),
- },
- set(TestExtendedChoices.choices),
- )
-
- def test_dynamic_properties(self):
- class TestExtendedChoices(models.ExtendedChoices):
- ITEM_1 = {
- "value": "Item_1",
- "label": "Item 1",
- "img": "/static/images/item1.png",
- "html_label": mark_safe("Item 1"),
- }
- ITEM_2 = {
- "value": "Item_2",
- "label": "Item 2",
- "img": "/static/images/item1.png",
- }
- ITEM_3 = "ITEM_3", "Item 3"
- ITEM_4 = "ITEM_4"
- ITEM_5 = auto(), "Item 5"
-
- self.assertEqual(
- TestExtendedChoices.ITEM_2.img,
- "/static/images/item1.png",
- )
-
- with self.assertRaises(AttributeError):
- TestExtendedChoices.ITEM_3.img
-
- def test_rich_radio_button(self):
- class TestRichRadioButton(RichRadioButton):
- ITEM_1 = {
- "value": "Item_1",
- "label": "Item 1",
- "pictogram": "/static/images/item1.png",
- "html_label": mark_safe("Item 1"),
- }
- ITEM_2 = {
- "value": "Item_2",
- "label": "Item 2",
- "pictogram": "/static/images/item2.png",
- "html_label": mark_safe("Item 2"),
- }
-
- self.assertEqual("/static/images/item1.png", TestRichRadioButton.ITEM_1.pictogram)
- self.assertEqual("/static/images/item2.png", TestRichRadioButton.ITEM_2.pictogram)
\ No newline at end of file
diff --git a/example_app/forms.py b/example_app/forms.py
index 5a120b774..f5dd9fdf0 100644
--- a/example_app/forms.py
+++ b/example_app/forms.py
@@ -1,7 +1,6 @@
from enum import auto
from django import forms
-from django.db.models import TextChoices
from django.forms import (
ModelForm,
inlineformset_factory,
@@ -23,12 +22,7 @@
from example_app.utils import populate_genre_choices
-class Lol(TextChoices):
- WEEE = auto(), "Weee"
- OOOH = auto(), "Oooh"
-
-
-class ExampleRichChoices(RichRadioButtonChoices, IntegerChoices):
+class ExampleRichChoices(IntegerChoices, RichRadioButtonChoices):
ITEM_1 = {
"value": auto(),
"label": "Item 1",