Repository: carltongibson/django-filter

Issue:
`CSVWidget` does not handle attrs parameters
It seems to me that `CSVWidget` does not handle `attrs` parameters. 

I would use it to give CSS class to the rendered form.

Is it an issue ?


Discussion:
I found that too, I hope it can be solved.
Actually, I noticed that it passes the `attrs` when there is only one value present. With multiple values it does not pass the `attrs`. The relevant code is this: https://github.com/carltongibson/django-filter/blob/400469f03f7ebac6e0a7d8cbe6a7706f71e14c96/django_filters/widgets.py#L208-L219

It seems to render both options with `attrs`, but somehow it doesn't work. Maybe someone more competent can help?
So, first thing is a minimal reproduce case, so we can look at the behaviour in question. 

Resolving patch:
diff --git a/django_filters/widgets.py b/django_filters/widgets.py
--- a/django_filters/widgets.py
+++ b/django_filters/widgets.py
@@ -1,4 +1,5 @@
 from collections.abc import Iterable
+from copy import deepcopy
 from itertools import chain
 from re import search, sub
 
@@ -189,6 +190,17 @@ def value_from_datadict(self, data, files, name):
 
 
 class BaseCSVWidget(forms.Widget):
+    # Surrogate widget for rendering multiple values
+    surrogate = forms.TextInput
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        if isinstance(self.surrogate, type):
+            self.surrogate = self.surrogate()
+        else:
+            self.surrogate = deepcopy(self.surrogate)
+
     def _isiterable(self, value):
         return isinstance(value, Iterable) and not isinstance(value, str)
 
@@ -212,15 +224,18 @@ def render(self, name, value, attrs=None, renderer=None):
 
         # if we have multiple values, we need to force render as a text input
         # (otherwise, the additional values are lost)
-        surrogate = forms.TextInput()
-        value = [force_str(surrogate.format_value(v)) for v in value]
+        value = [force_str(self.surrogate.format_value(v)) for v in value]
         value = ','.join(list(value))
 
-        return surrogate.render(name, value, attrs, renderer=renderer)
+        return self.surrogate.render(name, value, attrs, renderer=renderer)
 
 
 class CSVWidget(BaseCSVWidget, forms.TextInput):
-    pass
+    def __init__(self, *args, attrs=None, **kwargs):
+        super().__init__(*args, attrs, **kwargs)
+
+        if attrs is not None:
+            self.surrogate.attrs.update(attrs)
 
 
 class QueryArrayWidget(BaseCSVWidget, forms.TextInput):


Test patch:
diff --git a/tests/test_widgets.py b/tests/test_widgets.py
--- a/tests/test_widgets.py
+++ b/tests/test_widgets.py
@@ -1,4 +1,4 @@
-from django.forms import Select, TextInput
+from django.forms import NumberInput, Select, TextInput
 from django.test import TestCase
 
 from django_filters.widgets import (
@@ -266,23 +266,26 @@ def test_widget_value_from_datadict(self):
         self.assertEqual(result, None)
 
 
-class CSVWidgetTests(TestCase):
-    def test_widget(self):
-        w = CSVWidget()
+class BaseCSVWidgetTests(TestCase):
+    def test_widget_render(self):
+        class NumberCSVWidget(BaseCSVWidget, NumberInput):
+            pass
+
+        w = NumberCSVWidget(attrs={'test': 'attr'})
         self.assertHTMLEqual(w.render('price', None), """
-            <input type="text" name="price" />""")
+            <input type="number" test="attr" name="price" />""")
 
         self.assertHTMLEqual(w.render('price', ''), """
-            <input type="text" name="price" />""")
+            <input type="number" test="attr" name="price" />""")
 
         self.assertHTMLEqual(w.render('price', []), """
-            <input type="text" name="price" />""")
+            <input type="number" test="attr" name="price" />""")
 
         self.assertHTMLEqual(w.render('price', '1'), """
-            <input type="text" name="price" value="1" />""")
+            <input type="number" test="attr" name="price" value="1" />""")
 
         self.assertHTMLEqual(w.render('price', '1,2'), """
-            <input type="text" name="price" value="1,2" />""")
+            <input type="number" test="attr" name="price" value="1,2" />""")
 
         self.assertHTMLEqual(w.render('price', ['1', '2']), """
             <input type="text" name="price" value="1,2" />""")
@@ -291,8 +294,10 @@ def test_widget(self):
             <input type="text" name="price" value="1,2" />""")
 
     def test_widget_value_from_datadict(self):
-        w = CSVWidget()
+        class NumberCSVWidget(BaseCSVWidget, NumberInput):
+            pass
 
+        w = NumberCSVWidget()
         data = {'price': None}
         result = w.value_from_datadict(data, {}, 'price')
         self.assertEqual(result, None)
@@ -324,6 +329,46 @@ def test_widget_value_from_datadict(self):
         result = w.value_from_datadict({}, {}, 'price')
         self.assertEqual(result, None)
 
+    def test_surrogate_class(self):
+        class ClassSurrogate(BaseCSVWidget, NumberInput):
+            surrogate = NumberInput
+
+        w = ClassSurrogate()
+        self.assertIsInstance(w.surrogate, NumberInput)
+
+    def test_surrogate_instance(self):
+        class InstanceSurrogate(BaseCSVWidget, NumberInput):
+            surrogate = NumberInput()
+
+        w = InstanceSurrogate()
+        self.assertIsInstance(w.surrogate, NumberInput)
+        self.assertIsNot(InstanceSurrogate.surrogate, w.surrogate)  # deepcopied
+
+
+class CSVWidgetTests(TestCase):
+    def test_widget_render(self):
+        w = CSVWidget(attrs={'test': 'attr'})
+        self.assertHTMLEqual(w.render('price', None), """
+            <input type="text" test="attr" name="price" />""")
+
+        self.assertHTMLEqual(w.render('price', ''), """
+            <input type="text" test="attr" name="price" />""")
+
+        self.assertHTMLEqual(w.render('price', []), """
+            <input type="text" test="attr" name="price" />""")
+
+        self.assertHTMLEqual(w.render('price', '1'), """
+            <input type="text" test="attr" name="price" value="1" />""")
+
+        self.assertHTMLEqual(w.render('price', '1,2'), """
+            <input type="text" test="attr" name="price" value="1,2" />""")
+
+        self.assertHTMLEqual(w.render('price', ['1', '2']), """
+            <input type="text" test="attr" name="price" value="1,2" />""")
+
+        self.assertHTMLEqual(w.render('price', [1, 2]), """
+            <input type="text" test="attr" name="price" value="1,2" />""")
+
 
 class CSVSelectTests(TestCase):
     class CSVSelect(BaseCSVWidget, Select):