11"""Enumeration support for django model forms"""
2- from typing import Any , Iterable , List , Tuple , Type , Union
2+ from typing import Any , Iterable , List , Optional , Tuple , Type , Union
33
44from django .core .exceptions import ValidationError
55from django .db .models import Choices
6- from django .forms .fields import ChoiceField
6+ from django .forms .fields import TypedChoiceField
77from django .forms .widgets import Select
88
99__all__ = ['NonStrictSelect' , 'EnumChoiceField' ]
@@ -41,7 +41,7 @@ def render(self, *args, **kwargs):
4141 return super ().render (* args , ** kwargs )
4242
4343
44- class EnumChoiceField (ChoiceField ):
44+ class EnumChoiceField (TypedChoiceField ):
4545 """
4646 The default ``ChoiceField`` will only accept the base enumeration values.
4747 Use this field on forms to accept any value mappable to an enumeration
@@ -60,68 +60,105 @@ class EnumChoiceField(ChoiceField):
6060 :param kwargs: Any additional parameters to pass to ChoiceField base class.
6161 """
6262
63- enum : Type [Choices ]
64- strict : bool = True
63+ enum_ : Optional [ Type [Choices ]] = None
64+ strict_ : bool = True
6565 empty_value : Any = ''
6666 empty_values : List [Any ]
67+ choices : Iterable [Tuple [Any , Any ]]
68+
69+ @property
70+ def strict (self ):
71+ """strict fields allow non-enumeration values"""
72+ return self .strict_
73+
74+ @strict .setter
75+ def strict (self , strict ):
76+ self .strict_ = strict
77+
78+ @property
79+ def enum (self ):
80+ """the class of the enumeration"""
81+ return self .enum_
82+
83+ @enum .setter
84+ def enum (self , enum ):
85+ self .enum_ = enum
86+ self .choices = self .choices or getattr (
87+ self .enum ,
88+ 'choices' ,
89+ self .choices
90+ )
91+ # remove any of our valid enumeration values or symmetric properties
92+ # from our empty value list if there exists an equivalency
93+ for empty in self .empty_values :
94+ for enum_val in self .enum :
95+ if empty == enum_val :
96+ # copy the list instead of modifying the class's
97+ self .empty_values = [
98+ empty for empty in self .empty_values
99+ if empty != enum_val
100+ ]
101+ if empty == self .empty_value :
102+ if self .empty_values :
103+ self .empty_value = self .empty_values [0 ]
104+ else :
105+ raise ValueError (
106+ f'Enumeration value { repr (enum_val )} is'
107+ f'equivalent to { self .empty_value } , you must '
108+ f'specify a non-conflicting empty_value.'
109+ )
67110
68111 def __init__ (
69112 self ,
70- enum : Type [Choices ],
113+ enum : Optional [ Type [Choices ]] = None ,
71114 * ,
72115 empty_value : Any = _Unspecified ,
73- strict : bool = strict ,
116+ strict : bool = strict_ ,
74117 choices : Iterable [Tuple [Any , str ]] = (),
75118 ** kwargs
76119 ):
77- self .enum = enum
78120 self .strict = strict
79121 if not self .strict :
80122 kwargs .setdefault ('widget' , NonStrictSelect )
81123
124+ self .empty_values = kwargs .pop ('empty_values' , self .empty_values )
125+
82126 super ().__init__ (
83- choices = choices or getattr (self .enum , 'choices' , ()),
127+ choices = choices or getattr (self .enum , 'choices' , choices ),
128+ coerce = kwargs .pop ('coerce' , self .coerce ),
84129 ** kwargs
85130 )
86131
87132 if empty_value is not _Unspecified :
88- self .empty_values .insert (0 , empty_value )
133+ if empty_value not in self .empty_values :
134+ self .empty_values .insert (0 , empty_value )
89135 self .empty_value = empty_value
90136
91- # remove any of our valid enumeration values or symmetric properties
92- # from our empty value list if there exists an equivalency
93- for empty in self .empty_values :
94- for enum_val in self .enum :
95- if empty == enum_val :
96- # copy the list instead of modifying the class's
97- self .empty_values = [
98- empty for empty in self .empty_values
99- if empty != enum_val
100- ]
101- if empty == self .empty_value :
102- raise ValueError (
103- f'Enumeration value { repr (enum_val )} is equivalent'
104- f' to { self .empty_value } , you must specify a '
105- f'non-conflicting empty_value.'
106- )
137+ if enum :
138+ self .enum = enum
107139
108140 def _coerce_to_value_type (self , value : Any ) -> Any :
109141 """Coerce the value to the enumerations value type"""
110142 return type (self .enum .values [0 ])(value )
111143
112- def _coerce (self , value : Any ) -> Union [Choices , Any ]:
144+ def coerce ( # pylint: disable=E0202
145+ self , value : Any
146+ ) -> Union [Choices , Any ]:
113147 """
114148 Attempt conversion of value to an enumeration value and return it
115149 if successful.
116150
151+ .. note::
152+
153+ When used to represent a model field, by default the model field's
154+ to_python method will be substituted for this method.
155+
117156 :param value: The value to convert
118157 :raises ValidationError: if a valid return value cannot be determined.
119158 :return: An enumeration value or the canonical empty value if value is
120159 one of our empty_values, or the value itself if this is a
121160 non-strict field and the value is of a matching primitive type
122161 """
123- if value in self .empty_values :
124- return self .empty_value
125162 if (
126163 self .enum is not None and
127164 not isinstance (value , self .enum ) # pylint: disable=R0801
@@ -134,8 +171,8 @@ def _coerce(self, value: Any) -> Union[Choices, Any]:
134171 value = self .enum (value )
135172 except (TypeError , ValueError ) as err :
136173 if self .strict or not isinstance (
137- value ,
138- type (self .enum .values [0 ])
174+ value ,
175+ type (self .enum .values [0 ])
139176 ):
140177 raise ValidationError (
141178 f'{ value } is not a valid { self .enum } .' ,
0 commit comments