From 9c20344c16d7c1d735da0d9d2273053aa39ad147 Mon Sep 17 00:00:00 2001 From: Christophe Henry Date: Mon, 9 Sep 2024 10:17:10 +0200 Subject: [PATCH] Clean old code --- dsfr/enums.py | 91 ++++++++++++++++++--------- dsfr/models.py | 133 +-------------------------------------- dsfr/test/test_enums.py | 15 ++--- dsfr/test/test_models.py | 126 ------------------------------------- example_app/forms.py | 8 +-- 5 files changed, 70 insertions(+), 303 deletions(-) delete mode 100644 dsfr/test/test_models.py 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 e227c74c1..dbc0880e4 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",