Repository: wtforms/wtforms (python)

Issue:
Override field's name attribute?
I need compatibility with some existing HTML that uses dashes in the `name` attribute, but I have yet to find a way to override it with wtforms. [The documentation for `wtforms.fields.core.Field`](https://wtforms.readthedocs.org/en/latest/fields.html?highlight=selectmultiplefield#wtforms.fields.Field) mentions the following about `_name`:

> The name of this field, passed by the enclosing form during its construction. You should never pass this value yourself.

This makes me think that I should be able to override the name in the enclosing form. But in that case - where? Here's a failing test that shows what I'm expecting to be able to do:

``` python
import wtforms
from werkzeug.datastructures import ImmutableMultiDict


class MyForm(wtforms.Form):
    my_field_id = wtforms.IntegerField('My field', id='my-field',
                                       _name='my-field')


my_data = ImmutableMultiDict([('my-field', '2')])
my_form = MyForm(my_data)
assert my_form.my_field_id.name == 'my-field'  # Instead `my_field_id`
assert my_form.my_field_id.data == 2  # Instead `None`
```

Instead I have to use this custom field to get what I'm looking for:

``` python
import wtforms
class MyCustomIntegerField(wtforms.IntegerField):

    def __init__(self, *args, **kwargs):
        name = kwargs.pop('name', None)
        super().__init__(*args, **kwargs)
        if name:
            self.name = name
```

Am I missing something here?


Pull request description:
Allow overriding a field's HTML name
Fixes #205 
Brings the ability to customize a field HTML `name` attribute.

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 5a16f31..a33ed92 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -29,6 +29,8 @@ Unreleased
 -   Implemented :class:`~wtforms.fields.core.MonthField`. :pr:`530` :pr:`593`
 -   Filters can be inline. :meth:`form.BaseForm.process` takes a
     *extra_filters* parameter. :issue:`128` :pr:`592`
+-   Fields can be passed the ``name`` argument to use a HTML name
+    different than their Python name. :issue:`205`, :pr:`601`
 
 
 Version 2.3.1
diff --git a/src/wtforms/fields/core.py b/src/wtforms/fields/core.py
index 2b19ea6..d835092 100644
--- a/src/wtforms/fields/core.py
+++ b/src/wtforms/fields/core.py
@@ -45,7 +45,7 @@ class Field:
     do_not_call_in_templates = True  # Allow Django 1.4 traversal
 
     def __new__(cls, *args, **kwargs):
-        if "_form" in kwargs and "_name" in kwargs:
+        if "_form" in kwargs:
             return super().__new__(cls)
         else:
             return UnboundField(cls, *args, **kwargs)
@@ -60,8 +60,8 @@ class Field:
         default=None,
         widget=None,
         render_kw=None,
+        name=None,
         _form=None,
-        _name=None,
         _prefix="",
         _translations=None,
         _meta=None,
@@ -88,12 +88,12 @@ class Field:
         :param dict render_kw:
             If provided, a dictionary which provides default keywords that
             will be given to the widget at render time.
+        :param name:
+            The HTML name of this field. The default value is the Python
+            attribute name.
         :param _form:
             The form holding this field. It is passed by the form itself during
             construction. You should never pass this value yourself.
-        :param _name:
-            The name of this field, passed by the enclosing form during its
-            construction. You should never pass this value yourself.
         :param _prefix:
             The prefix to prepend to the form name of this field, passed by
             the enclosing form during construction.
@@ -105,7 +105,7 @@ class Field:
             If provided, this is the 'meta' instance from the form. You usually
             don't pass this yourself.
 
-        If `_form` and `_name` isn't provided, an :class:`UnboundField` will be
+        If `_form` isn't provided, an :class:`UnboundField` will be
         returned instead. Call its :func:`bind` method with a form instance and
         a name to construct the field.
         """
@@ -124,8 +124,8 @@ class Field:
         self.render_kw = render_kw
         self.filters = filters
         self.flags = Flags()
-        self.name = _prefix + _name
-        self.short_name = _name
+        self.name = _prefix + name
+        self.short_name = name
         self.type = type(self).__name__
 
         self.check_validators(validators)
@@ -136,7 +136,7 @@ class Field:
             self.id,
             label
             if label is not None
-            else self.gettext(_name.replace("_", " ").title()),
+            else self.gettext(name.replace("_", " ").title()),
         )
 
         if widget is not None:
@@ -378,10 +378,11 @@ class UnboundField:
     _formfield = True
     creation_counter = 0
 
-    def __init__(self, field_class, *args, **kwargs):
+    def __init__(self, field_class, *args, name=None, **kwargs):
         UnboundField.creation_counter += 1
         self.field_class = field_class
         self.args = args
+        self.name = name
         self.kwargs = kwargs
         self.creation_counter = UnboundField.creation_counter
         validators = kwargs.get("validators")
@@ -391,9 +392,9 @@ class UnboundField:
     def bind(self, form, name, prefix="", translations=None, **kwargs):
         kw = dict(
             self.kwargs,
+            name=name,
             _form=form,
             _prefix=prefix,
-            _name=name,
             _translations=translations,
             **kwargs,
         )
@@ -479,7 +480,7 @@ class SelectFieldBase(Field):
 
     def __iter__(self):
         opts = dict(
-            widget=self.option_widget, _name=self.name, _form=None, _meta=self.meta
+            widget=self.option_widget, name=self.name, _form=None, _meta=self.meta
         )
         for i, (value, label, checked) in enumerate(self.iter_choices()):
             opt = self._Option(label=label, id="%s-%d" % (self.id, i), **opts)
diff --git a/src/wtforms/form.py b/src/wtforms/form.py
index b9fe171..b686473 100644
--- a/src/wtforms/form.py
+++ b/src/wtforms/form.py
@@ -42,7 +42,8 @@ class BaseForm:
             extra_fields.extend(self._csrf.setup_form(self))
 
         for name, unbound_field in itertools.chain(fields, extra_fields):
-            options = dict(name=name, prefix=prefix, translations=translations)
+            field_name = unbound_field.name or name
+            options = dict(name=field_name, prefix=prefix, translations=translations)
             field = meta.bind_field(self, unbound_field, options)
             self._fields[name] = field
 
@@ -80,35 +81,30 @@ class BaseForm:
             field.populate_obj(obj, name)
 
     def process(self, formdata=None, obj=None, data=None, extra_filters=None, **kwargs):
-        """
-        Take form, object data, and keyword arg input and have the fields
-        process them.
-
-        :param formdata:
-            Used to pass data coming from the enduser, usually `request.POST` or
-            equivalent.
-        :param obj:
-            If `formdata` is empty or not provided, this object is checked for
-            attributes matching form field names, which will be used for field
-            values.
-        :param data:
-            If provided, must be a dictionary of data. This is only used if
-            `formdata` is empty or not provided and `obj` does not contain
-            an attribute named the same as the field.
-        :param extra_filters: A dict mapping field names to lists of
-            extra filters functions to run. Extra filters run after
-            filters passed when creating the field. If the form has
-            ``filter_<fieldname>``, it is the last extra filter.
-        :param `**kwargs`:
-            If `formdata` is empty or not provided and `obj` does not contain
-            an attribute named the same as a field, form will assign the value
-            of a matching keyword argument to the field, if one exists.
+        """Process default and input data with each field.
+
+        :param formdata: Input data coming from the client, usually
+            ``request.form`` or equivalent. Should provide a "multi
+            dict" interface to get a list of values for a given key,
+            such as what Werkzeug, Django, and WebOb provide.
+        :param obj: Take existing data from attributes on this object
+            matching form field attributes. Only used if ``formdata`` is
+            not passed.
+        :param data: Take existing data from keys in this dict matching
+            form field attributes. ``obj`` takes precedence if it also
+            has a matching attribute. Only used if ``formdata`` is not
+            passed.
+        :param extra_filters: A dict mapping field attribute names to
+            lists of extra filter functions to run. Extra filters run
+            after filters passed when creating the field. If the form
+            has ``filter_<fieldname>``, it is the last extra filter.
+        :param kwargs: Merged with ``data`` to allow passing existing
+            data as parameters. Overwrites any duplicate keys in
+            ``data``. Only used if ``formdata`` is not passed.
         """
         formdata = self.meta.wrap_formdata(self, formdata)
 
         if data is not None:
-            # XXX we want to eventually process 'data' as a new entity.
-            #     Temporarily, this can simply be merged with kwargs.
             kwargs = dict(data, **kwargs)
 
         filters = extra_filters.copy() if extra_filters is not None else {}
@@ -125,7 +121,9 @@ class BaseForm:
                     formdata, getattr(obj, name), extra_filters=field_extra_filters
                 )
             elif name in kwargs:
-                field.process(formdata, kwargs[name], extra_filters=field_extra_filters)
+                field.process(
+                    formdata, kwargs[name], extra_filters=field_extra_filters,
+                )
             else:
                 field.process(formdata, extra_filters=field_extra_filters)
 
@@ -245,28 +243,31 @@ class Form(BaseForm, metaclass=FormMeta):
         self, formdata=None, obj=None, prefix="", data=None, meta=None, **kwargs,
     ):
         """
-        :param formdata:
-            Used to pass data coming from the enduser, usually `request.POST` or
-            equivalent. formdata should be some sort of request-data wrapper which
-            can get multiple parameters from the form input, and values are unicode
-            strings, e.g. a Werkzeug/Django/WebOb MultiDict
-        :param obj:
-            If `formdata` is empty or not provided, this object is checked for
-            attributes matching form field names, which will be used for field
-            values.
-        :param prefix:
-            If provided, all fields will have their name prefixed with the
-            value.
-        :param data:
-            Accept a dictionary of data. This is only used if `formdata` and
-            `obj` are not present.
-        :param meta:
-            If provided, this is a dictionary of values to override attributes
-            on this form's meta instance.
-        :param `**kwargs`:
-            If `formdata` is empty or not provided and `obj` does not contain
-            an attribute named the same as a field, form will assign the value
-            of a matching keyword argument to the field, if one exists.
+        :param formdata: Input data coming from the client, usually
+            ``request.form`` or equivalent. Should provide a "multi
+            dict" interface to get a list of values for a given key,
+            such as what Werkzeug, Django, and WebOb provide.
+        :param obj: Take existing data from attributes on this object
+            matching form field attributes. Only used if ``formdata`` is
+            not passed.
+        :param prefix: If provided, all fields will have their name
+            prefixed with the value. This is for distinguishing multiple
+            forms on a single page. This only affects the HTML name for
+            matching input data, not the Python name for matching
+            existing data.
+        :param data: Take existing data from keys in this dict matching
+            form field attributes. ``obj`` takes precedence if it also
+            has a matching attribute. Only used if ``formdata`` is not
+            passed.
+        :param meta: A dict of attributes to override on this form's
+            :attr:`meta` instance.
+        :param extra_filters: A dict mapping field attribute names to
+            lists of extra filter functions to run. Extra filters run
+            after filters passed when creating the field. If the form
+            has ``filter_<fieldname>``, it is the last extra filter.
+        :param kwargs: Merged with ``data`` to allow passing existing
+            data as parameters. Overwrites any duplicate keys in
+            ``data``. Only used if ``formdata`` is not passed.
         """
         meta_obj = self._wtforms_meta()
         if meta is not None and isinstance(meta, dict):


Test patch:
diff --git a/tests/test_fields.py b/tests/test_fields.py
index c101be3..a82a3f9 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -52,8 +52,8 @@ class AttrDict:
         self.__dict__.update(*args, **kw)
 
 
-def make_form(_name="F", **fields):
-    return type(str(_name), (Form,), fields)
+def make_form(name="F", **fields):
+    return type(str(name), (Form,), fields)
 
 
 class TestDefaults:
@@ -244,12 +244,12 @@ class TestField:
 
         # Can we pass in meta via _meta?
         form_meta = meta.DefaultMeta()
-        field = StringField(_name="Foo", _form=None, _meta=form_meta)
+        field = StringField(name="Foo", _form=None, _meta=form_meta)
         assert field.meta is form_meta
 
         # Do we fail if both _meta and _form are None?
         with pytest.raises(TypeError):
-            StringField(_name="foo", _form=None)
+            StringField(name="foo", _form=None)
 
     def test_render_kw(self):
         form = self.F()
@@ -309,6 +309,41 @@ class TestField:
         ):
             Field(validators=[v2])
 
+    def test_custom_name(self):
+        class F(Form):
+            foo = StringField(name="bar", default="default")
+            x = StringField()
+
+        class ObjFoo:
+            foo = "obj"
+
+        class ObjBar:
+            bar = "obj"
+
+        f = F(DummyPostData(foo="data"))
+        assert f.foo.data == "default"
+        assert 'value="default"' in f.foo()
+
+        f = F(DummyPostData(bar="data"))
+        assert f.foo.data == "data"
+        assert 'value="data"' in f.foo()
+
+        f = F(foo="kwarg")
+        assert f.foo.data == "kwarg"
+        assert 'value="kwarg"' in f.foo()
+
+        f = F(bar="kwarg")
+        assert f.foo.data == "default"
+        assert 'value="default"' in f.foo()
+
+        f = F(obj=ObjFoo())
+        assert f.foo.data == "obj"
+        assert 'value="obj"' in f.foo()
+
+        f = F(obj=ObjBar())
+        assert f.foo.data == "default"
+        assert 'value="default"' in f.foo()
+
 
 class PrePostTestField(StringField):
     def pre_validate(self, form):
@@ -1087,6 +1122,24 @@ class TestFieldList:
         with pytest.raises(TypeError):
             form.populate_obj(obj2)
 
+    def test_enclosed_subform_custom_name(self):
+        class Inside(Form):
+            foo = StringField(name="bar", default="default")
+
+        class Outside(Form):
+            subforms = FieldList(FormField(Inside), min_entries=1)
+
+        o = Outside()
+        assert o.subforms[0].foo.data == "default"
+
+        pdata = DummyPostData({"subforms-0-bar": "form"})
+        o = Outside(pdata)
+        assert o.subforms[0].foo.data == "form"
+
+        pdata = DummyPostData({"subforms-0-foo": "form"})
+        o = Outside(pdata)
+        assert o.subforms[0].foo.data == "default"
+
     def test_entry_management(self):
         F = make_form(a=FieldList(self.t))
         a = F(a=["hello", "bye"]).a
@@ -1142,7 +1195,7 @@ class TestFieldList:
 
     def test_no_filters(self):
         with pytest.raises(TypeError):
-            FieldList(self.t, filters=[lambda x: x], _form=Form(), _name="foo")
+            FieldList(self.t, filters=[lambda x: x], _form=Form(), name="foo")
 
     def test_process_prefilled(self):
         data = ["foo", "hi", "rawr"]
diff --git a/tests/test_locale_babel.py b/tests/test_locale_babel.py
index ea80ebe..e752c99 100644
--- a/tests/test_locale_babel.py
+++ b/tests/test_locale_babel.py
@@ -28,8 +28,8 @@ class TestLocaleDecimal:
             form = self.F()
             DecimalField(
                 use_locale=True,
+                name="a",
                 _form=form,
-                _name="a",
                 _translations=form.meta.get_translations(form),
                 **kw
             )