codekingpro/portable-devtools
114k
1import itertools2from collections import OrderedDict3 4from wtforms.meta import DefaultMeta5from wtforms.utils import unset_value6 7__all__ = ("BaseForm", "Form")8 9_default_meta = DefaultMeta()10 11 12class BaseForm:13 """14 Base Form Class. Provides core behaviour like field construction,15 validation, and data and error proxying.16 """17 18 def __init__(self, fields, prefix="", meta=_default_meta):19 """20 :param fields:21 A dict or sequence of 2-tuples of partially-constructed fields.22 :param prefix:23 If provided, all fields will have their name prefixed with the24 value.25 :param meta:26 A meta instance which is used for configuration and customization27 of WTForms behaviors.28 """29 if prefix and prefix[-1] not in "-_;:/.":30 prefix += "-"31 32 self.meta = meta33 self._prefix = prefix34 self._fields = OrderedDict()35 36 if hasattr(fields, "items"):37 fields = fields.items()38 39 translations = self.meta.get_translations(self)40 extra_fields = []41 if meta.csrf:42 self._csrf = meta.build_csrf(self)43 extra_fields.extend(self._csrf.setup_form(self))44 45 for name, unbound_field in itertools.chain(fields, extra_fields):46 field_name = unbound_field.name or name47 options = dict(name=field_name, prefix=prefix, translations=translations)48 field = meta.bind_field(self, unbound_field, options)49 self._fields[name] = field50 51 self.form_errors = []52 53 def __iter__(self):54 """Iterate form fields in creation order."""55 return iter(self._fields.values())56 57 def __contains__(self, name):58 """Returns `True` if the named field is a member of this form."""59 return name in self._fields60 61 def __getitem__(self, name):62 """Dict-style access to this form's fields."""63 return self._fields[name]64 65 def __setitem__(self, name, value):66 """Bind a field to this form."""67 self._fields[name] = value.bind(form=self, name=name, prefix=self._prefix)68 69 def __delitem__(self, name):70 """Remove a field from this form."""71 del self._fields[name]72 73 def populate_obj(self, obj):74 """75 Populates the attributes of the passed `obj` with data from the form's76 fields.77 78 :note: This is a destructive operation; Any attribute with the same name79 as a field will be overridden. Use with caution.80 """81 for name, field in self._fields.items():82 field.populate_obj(obj, name)83 84 def process(self, formdata=None, obj=None, data=None, extra_filters=None, **kwargs):85 """Process default and input data with each field.86 87 :param formdata: Input data coming from the client, usually88 ``request.form`` or equivalent. Should provide a "multi89 dict" interface to get a list of values for a given key,90 such as what Werkzeug, Django, and WebOb provide.91 :param obj: Take existing data from attributes on this object92 matching form field attributes. Only used if ``formdata`` is93 not passed.94 :param data: Take existing data from keys in this dict matching95 form field attributes. ``obj`` takes precedence if it also96 has a matching attribute. Only used if ``formdata`` is not97 passed.98 :param extra_filters: A dict mapping field attribute names to99 lists of extra filter functions to run. Extra filters run100 after filters passed when creating the field. If the form101 has ``filter_<fieldname>``, it is the last extra filter.102 :param kwargs: Merged with ``data`` to allow passing existing103 data as parameters. Overwrites any duplicate keys in104 ``data``. Only used if ``formdata`` is not passed.105 """106 formdata = self.meta.wrap_formdata(self, formdata)107 108 if data is not None:109 kwargs = dict(data, **kwargs)110 111 filters = extra_filters.copy() if extra_filters is not None else {}112 113 for name, field in self._fields.items():114 field_extra_filters = filters.get(name, [])115 116 inline_filter = getattr(self, "filter_%s" % name, None)117 if inline_filter is not None:118 field_extra_filters.append(inline_filter)119 120 if obj is not None and hasattr(obj, name):121 data = getattr(obj, name)122 elif name in kwargs:123 data = kwargs[name]124 else:125 data = unset_value126 127 field.process(formdata, data, extra_filters=field_extra_filters)128 129 def validate(self, extra_validators=None):130 """131 Validates the form by calling `validate` on each field.132 133 :param extra_validators:134 If provided, is a dict mapping field names to a sequence of135 callables which will be passed as extra validators to the field's136 `validate` method.137 138 Returns `True` if no errors occur.139 """140 success = True141 for name, field in self._fields.items():142 if extra_validators is not None and name in extra_validators:143 extra = extra_validators[name]144 else:145 extra = tuple()146 if not field.validate(self, extra):147 success = False148 return success149 150 @property151 def data(self):152 return {name: f.data for name, f in self._fields.items()}153 154 @property155 def errors(self):156 errors = {name: f.errors for name, f in self._fields.items() if f.errors}157 if self.form_errors:158 errors[None] = self.form_errors159 return errors160 161 162class FormMeta(type):163 """164 The metaclass for `Form` and any subclasses of `Form`.165 166 `FormMeta`'s responsibility is to create the `_unbound_fields` list, which167 is a list of `UnboundField` instances sorted by their order of168 instantiation. The list is created at the first instantiation of the form.169 If any fields are added/removed from the form, the list is cleared to be170 re-generated on the next instantiation.171 172 Any properties which begin with an underscore or are not `UnboundField`173 instances are ignored by the metaclass.174 """175 176 def __init__(cls, name, bases, attrs):177 type.__init__(cls, name, bases, attrs)178 cls._unbound_fields = None179 cls._wtforms_meta = None180 181 def __call__(cls, *args, **kwargs):182 """183 Construct a new `Form` instance.184 185 Creates the `_unbound_fields` list and the internal `_wtforms_meta`186 subclass of the class Meta in order to allow a proper inheritance187 hierarchy.188 """189 if cls._unbound_fields is None:190 fields = []191 for name in dir(cls):192 if not name.startswith("_"):193 unbound_field = getattr(cls, name)194 if hasattr(unbound_field, "_formfield"):195 fields.append((name, unbound_field))196 # We keep the name as the second element of the sort197 # to ensure a stable sort.198 fields.sort(key=lambda x: (x[1].creation_counter, x[0]))199 cls._unbound_fields = fields200 201 # Create a subclass of the 'class Meta' using all the ancestors.202 if cls._wtforms_meta is None:203 bases = []204 for mro_class in cls.__mro__:205 if "Meta" in mro_class.__dict__:206 bases.append(mro_class.Meta)207 cls._wtforms_meta = type("Meta", tuple(bases), {})208 return type.__call__(cls, *args, **kwargs)209 210 def __setattr__(cls, name, value):211 """212 Add an attribute to the class, clearing `_unbound_fields` if needed.213 """214 if name == "Meta":215 cls._wtforms_meta = None216 elif not name.startswith("_") and hasattr(value, "_formfield"):217 cls._unbound_fields = None218 type.__setattr__(cls, name, value)219 220 def __delattr__(cls, name):221 """222 Remove an attribute from the class, clearing `_unbound_fields` if223 needed.224 """225 if not name.startswith("_"):226 cls._unbound_fields = None227 type.__delattr__(cls, name)228 229 230class Form(BaseForm, metaclass=FormMeta):231 """232 Declarative Form base class. Extends BaseForm's core behaviour allowing233 fields to be defined on Form subclasses as class attributes.234 235 In addition, form and instance input data are taken at construction time236 and passed to `process()`.237 """238 239 Meta = DefaultMeta240 241 def __init__(242 self,243 formdata=None,244 obj=None,245 prefix="",246 data=None,247 meta=None,248 **kwargs,249 ):250 """251 :param formdata: Input data coming from the client, usually252 ``request.form`` or equivalent. Should provide a "multi253 dict" interface to get a list of values for a given key,254 such as what Werkzeug, Django, and WebOb provide.255 :param obj: Take existing data from attributes on this object256 matching form field attributes. Only used if ``formdata`` is257 not passed.258 :param prefix: If provided, all fields will have their name259 prefixed with the value. This is for distinguishing multiple260 forms on a single page. This only affects the HTML name for261 matching input data, not the Python name for matching262 existing data.263 :param data: Take existing data from keys in this dict matching264 form field attributes. ``obj`` takes precedence if it also265 has a matching attribute. Only used if ``formdata`` is not266 passed.267 :param meta: A dict of attributes to override on this form's268 :attr:`meta` instance.269 :param extra_filters: A dict mapping field attribute names to270 lists of extra filter functions to run. Extra filters run271 after filters passed when creating the field. If the form272 has ``filter_<fieldname>``, it is the last extra filter.273 :param kwargs: Merged with ``data`` to allow passing existing274 data as parameters. Overwrites any duplicate keys in275 ``data``. Only used if ``formdata`` is not passed.276 """277 meta_obj = self._wtforms_meta()278 if meta is not None and isinstance(meta, dict):279 meta_obj.update_values(meta)280 super().__init__(self._unbound_fields, meta=meta_obj, prefix=prefix)281 282 for name, field in self._fields.items():283 # Set all the fields to attributes so that they obscure the class284 # attributes with the same names.285 setattr(self, name, field)286 self.process(formdata, obj, data=data, **kwargs)287 288 def __setitem__(self, name, value):289 raise TypeError("Fields may not be added to Form instances, only classes.")290 291 def __delitem__(self, name):292 del self._fields[name]293 setattr(self, name, None)294 295 def __delattr__(self, name):296 if name in self._fields:297 self.__delitem__(name)298 else:299 # This is done for idempotency, if we have a name which is a field,300 # we want to mask it by setting the value to None.301 unbound_field = getattr(self.__class__, name, None)302 if unbound_field is not None and hasattr(unbound_field, "_formfield"):303 setattr(self, name, None)304 else:305 super().__delattr__(name)306 307 def validate(self, extra_validators=None):308 """Validate the form by calling ``validate`` on each field.309 Returns ``True`` if validation passes.310 311 If the form defines a ``validate_<fieldname>`` method, it is312 appended as an extra validator for the field's ``validate``.313 314 :param extra_validators: A dict mapping field names to lists of315 extra validator methods to run. Extra validators run after316 validators passed when creating the field. If the form has317 ``validate_<fieldname>``, it is the last extra validator.318 """319 if extra_validators is not None:320 extra = extra_validators.copy()321 else:322 extra = {}323 324 for name in self._fields:325 inline = getattr(self.__class__, f"validate_{name}", None)326 if inline is not None:327 extra.setdefault(name, []).append(inline)328 329 return super().validate(extra)330 