From 36a8ef82959e64b463588b0d56733efd1fa46460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Wed, 29 Apr 2020 16:18:58 +0200 Subject: [PATCH 1/5] Field '_name' argument has been renamed 'name' --- src/wtforms/fields/core.py | 22 +++++++++++----------- tests/test_fields.py | 10 +++++----- tests/test_locale_babel.py | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/wtforms/fields/core.py b/src/wtforms/fields/core.py index 2b19ea610..fddfff497 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 and "name" in kwargs: return super().__new__(cls) else: return UnboundField(cls, *args, **kwargs) @@ -60,8 +60,8 @@ def __init__( default=None, widget=None, render_kw=None, + name=None, _form=None, - _name=None, _prefix="", _translations=None, _meta=None, @@ -88,12 +88,12 @@ def __init__( :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 name of this field, passed by the enclosing form during its + construction. You should never pass this value yourself. :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 @@ def __init__( 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` and `name` 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 @@ def __init__( 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 @@ def __init__( 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: @@ -391,9 +391,9 @@ def __init__(self, field_class, *args, **kwargs): 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 +479,7 @@ def iter_choices(self): 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/tests/test_fields.py b/tests/test_fields.py index c101be3e0..0b7f0eab6 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -52,8 +52,8 @@ def __init__(self, *args, **kw): 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 @@ def test_meta_attribute(self): # 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() @@ -1142,7 +1142,7 @@ def validator(form, field): 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 ea80ebe6a..e752c998c 100644 --- a/tests/test_locale_babel.py +++ b/tests/test_locale_babel.py @@ -28,8 +28,8 @@ def build(**kw): form = self.F() DecimalField( use_locale=True, + name="a", _form=form, - _name="a", _translations=form.meta.get_translations(form), **kw ) From c58db6af22fc9e4cf18ec59c7f644227937a5264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Thu, 30 Apr 2020 12:49:19 +0200 Subject: [PATCH 2/5] Field metaclass only return an Unbound field if '_form' is not set --- src/wtforms/fields/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wtforms/fields/core.py b/src/wtforms/fields/core.py index fddfff497..d0bb6d0e0 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) @@ -105,7 +105,7 @@ def __init__( 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. """ From 1da23cb43b4e60a0498595bd1645570c07c71eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89loi=20Rivard?= Date: Thu, 30 Apr 2020 12:50:45 +0200 Subject: [PATCH 3/5] Field name are customizable --- CHANGES.rst | 2 ++ src/wtforms/fields/core.py | 7 ++--- src/wtforms/form.py | 11 +++++--- tests/test_fields.py | 55 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5a16f312c..3d73946fc 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 have 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 d0bb6d0e0..d4d3368b7 100644 --- a/src/wtforms/fields/core.py +++ b/src/wtforms/fields/core.py @@ -89,8 +89,8 @@ def __init__( If provided, a dictionary which provides default keywords that will be given to the widget at render time. :param name: - The name of this field, passed by the enclosing form during its - construction. You should never pass this value yourself. + The HTML name of this field. The default value is the python + field name. :param _form: The form holding this field. It is passed by the form itself during construction. You should never pass this value yourself. @@ -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") diff --git a/src/wtforms/form.py b/src/wtforms/form.py index b9fe171da..f3721ea77 100644 --- a/src/wtforms/form.py +++ b/src/wtforms/form.py @@ -42,7 +42,8 @@ def __init__(self, fields, prefix="", meta=_default_meta): 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 @@ -124,8 +125,12 @@ def process(self, formdata=None, obj=None, data=None, extra_filters=None, **kwar field.process( formdata, getattr(obj, name), extra_filters=field_extra_filters ) - elif name in kwargs: - field.process(formdata, kwargs[name], extra_filters=field_extra_filters) + elif field.short_name in kwargs: + field.process( + formdata, + kwargs[field.short_name], + extra_filters=field_extra_filters, + ) else: field.process(formdata, extra_filters=field_extra_filters) diff --git a/tests/test_fields.py b/tests/test_fields.py index 0b7f0eab6..e5127f207 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -309,6 +309,43 @@ def test_check_validators(self): ): Field(validators=[v2]) + def test_custom_name(self): + class F(Form): + foo = StringField(name="bar", default="defaultvalue") + + class Objfoo: + foo = "Objfoo" + + class Objbar: + bar = "Objbar" + + f = F() + assert "defaultvalue" == f.foo.data + assert ( + """""" + == f.foo() + ) + + f = F(bar="formvalue") + assert "formvalue" == f.foo.data + assert ( + """""" == f.foo() + ) + + f = F(foo="formvalue") + assert "defaultvalue" == f.foo.data + assert ( + """""" + == f.foo() + ) + + f = F(None, Objbar()) + assert "defaultvalue" == f.foo.data + assert ( + """""" + == f.foo() + ) + class PrePostTestField(StringField): def pre_validate(self, form): @@ -1087,6 +1124,24 @@ def make_inner(): with pytest.raises(TypeError): form.populate_obj(obj2) + def test_enclosed_subform_custom_name(self): + class Inside(Form): + foo = StringField(name="bar", default="defaultvalue") + + class Outside(Form): + subforms = FieldList(FormField(Inside), min_entries=1) + + o = Outside() + assert "defaultvalue" == o.subforms[0].foo.data + + pdata = DummyPostData({"subforms-0-bar": "formvalue"}) + o = Outside(pdata) + assert "formvalue" == o.subforms[0].foo.data + + pdata = DummyPostData({"subforms-0-foo": "formvalue"}) + o = Outside(pdata) + assert "defaultvalue" == o.subforms[0].foo.data + def test_entry_management(self): F = make_form(a=FieldList(self.t)) a = F(a=["hello", "bye"]).a From 2b07c72ff917b462590865ebf843f0d8ed0fe6be Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 30 May 2020 23:30:36 -0700 Subject: [PATCH 4/5] match Python name, not HTML name, from data --- CHANGES.rst | 4 +-- src/wtforms/fields/core.py | 4 +-- src/wtforms/form.py | 6 ++-- tests/test_fields.py | 66 ++++++++++++++++++-------------------- 4 files changed, 38 insertions(+), 42 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3d73946fc..a33ed92b9 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -29,8 +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 have a HTML name different than their python name. - :issue:`205` :pr:`601` +- 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 d4d3368b7..d83509238 100644 --- a/src/wtforms/fields/core.py +++ b/src/wtforms/fields/core.py @@ -89,8 +89,8 @@ def __init__( 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 - field 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. diff --git a/src/wtforms/form.py b/src/wtforms/form.py index f3721ea77..4f597e4d4 100644 --- a/src/wtforms/form.py +++ b/src/wtforms/form.py @@ -125,11 +125,9 @@ def process(self, formdata=None, obj=None, data=None, extra_filters=None, **kwar field.process( formdata, getattr(obj, name), extra_filters=field_extra_filters ) - elif field.short_name in kwargs: + elif name in kwargs: field.process( - formdata, - kwargs[field.short_name], - extra_filters=field_extra_filters, + formdata, kwargs[name], extra_filters=field_extra_filters, ) else: field.process(formdata, extra_filters=field_extra_filters) diff --git a/tests/test_fields.py b/tests/test_fields.py index e5127f207..a82a3f9b7 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -311,40 +311,38 @@ def test_check_validators(self): def test_custom_name(self): class F(Form): - foo = StringField(name="bar", default="defaultvalue") + foo = StringField(name="bar", default="default") + x = StringField() - class Objfoo: - foo = "Objfoo" + class ObjFoo: + foo = "obj" - class Objbar: - bar = "Objbar" + class ObjBar: + bar = "obj" - f = F() - assert "defaultvalue" == f.foo.data - assert ( - """""" - == f.foo() - ) + f = F(DummyPostData(foo="data")) + assert f.foo.data == "default" + assert 'value="default"' in f.foo() - f = F(bar="formvalue") - assert "formvalue" == f.foo.data - assert ( - """""" == f.foo() - ) + f = F(DummyPostData(bar="data")) + assert f.foo.data == "data" + assert 'value="data"' in f.foo() - f = F(foo="formvalue") - assert "defaultvalue" == f.foo.data - assert ( - """""" - == f.foo() - ) + f = F(foo="kwarg") + assert f.foo.data == "kwarg" + assert 'value="kwarg"' in f.foo() - f = F(None, Objbar()) - assert "defaultvalue" == f.foo.data - assert ( - """""" - == 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): @@ -1126,21 +1124,21 @@ def make_inner(): def test_enclosed_subform_custom_name(self): class Inside(Form): - foo = StringField(name="bar", default="defaultvalue") + foo = StringField(name="bar", default="default") class Outside(Form): subforms = FieldList(FormField(Inside), min_entries=1) o = Outside() - assert "defaultvalue" == o.subforms[0].foo.data + assert o.subforms[0].foo.data == "default" - pdata = DummyPostData({"subforms-0-bar": "formvalue"}) + pdata = DummyPostData({"subforms-0-bar": "form"}) o = Outside(pdata) - assert "formvalue" == o.subforms[0].foo.data + assert o.subforms[0].foo.data == "form" - pdata = DummyPostData({"subforms-0-foo": "formvalue"}) + pdata = DummyPostData({"subforms-0-foo": "form"}) o = Outside(pdata) - assert "defaultvalue" == o.subforms[0].foo.data + assert o.subforms[0].foo.data == "default" def test_entry_management(self): F = make_form(a=FieldList(self.t)) From cdc4e6bcd3304f862a16c2a4e290921525f3756f Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 30 May 2020 23:36:45 -0700 Subject: [PATCH 5/5] update docs about data clarify form vs existing data, and Python vs HTML name --- src/wtforms/form.py | 92 ++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 47 deletions(-) diff --git a/src/wtforms/form.py b/src/wtforms/form.py index 4f597e4d4..b686473e4 100644 --- a/src/wtforms/form.py +++ b/src/wtforms/form.py @@ -81,35 +81,30 @@ def populate_obj(self, obj): 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_``, 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_``, 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 {} @@ -248,28 +243,31 @@ def __init__( 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_``, 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):