Fork me on GitHub

Source code for webtest.forms

# -*- coding: utf-8 -*-
"""Helpers to fill and submit forms."""

import re

from bs4 import BeautifulSoup
from webtest.compat import OrderedDict
from webtest import utils


class NoValue(object):
    pass


[docs]class Upload(object): """ A file to upload:: >>> Upload('filename.txt', 'data') <Upload "filename.txt"> >>> Upload("README.txt") <Upload "README.txt"> :param filename: Name of the file to upload. :param content: Contents of the file. """ def __init__(self, filename, content=None): self.filename = filename self.content = content def __iter__(self): yield self.filename if self.content: yield self.content # TODO: do we handle the case when we need to get # contents ourselves? def __repr__(self): return '<Upload "%s">' % self.filename
[docs]class Field(object): """Base class for all Field objects. .. attribute:: classes Dictionary of field types (select, radio, etc) .. attribute:: value Set/get value of the field. """ classes = {} def __init__(self, form, tag, name, pos, value=None, id=None, **attrs): self.form = form self.tag = tag self.name = name self.pos = pos self._value = value self.id = id self.attrs = attrs def value__get(self): if self._value is None: return '' else: return self._value def value__set(self, value): self._value = value value = property(value__get, value__set)
[docs] def force_value(self, value): """Like setting a value, except forces it (even for, say, hidden fields). """ self._value = value
def __repr__(self): value = '<%s name="%s"' % (self.__class__.__name__, self.name) if self.id: value += ' id="%s"' % self.id return value + '>'
[docs]class Select(Field): """Field representing ``<select />`` form element.""" def __init__(self, *args, **attrs): super(Select, self).__init__(*args, **attrs) self.options = [] # Undetermined yet: self.selectedIndex = None # we have no forced value self._forced_value = NoValue
[docs] def force_value(self, value): """Like setting a value, except forces it (even for, say, hidden fields). """ self._forced_value = value
def value__set(self, value): if self._forced_value is not NoValue: self._forced_value = NoValue for i, (option, checked) in enumerate(self.options): if option == utils.stringify(value): self.selectedIndex = i break else: raise ValueError( "Option %r not found (from %s)" % (value, ', '.join( [repr(o) for o, c in self.options]))) def value__get(self): if self._forced_value is not NoValue: return self._forced_value elif self.selectedIndex is not None: return self.options[self.selectedIndex][0] else: for option, checked in self.options: if checked: return option else: if self.options: return self.options[0][0] value = property(value__get, value__set)
[docs]class MultipleSelect(Field): """Field representing ``<select multiple="multiple">``""" def __init__(self, *args, **attrs): super(MultipleSelect, self).__init__(*args, **attrs) self.options = [] # Undetermined yet: self.selectedIndices = [] self._forced_values = []
[docs] def force_value(self, values): """Like setting a value, except forces it (even for, say, hidden fields). """ self._forced_values = values self.selectedIndices = []
def value__set(self, values): str_values = [utils.stringify(value) for value in values] self.selectedIndices = [] for i, (option, checked) in enumerate(self.options): if option in str_values: self.selectedIndices.append(i) str_values.remove(option) if str_values: raise ValueError( "Option(s) %r not found (from %s)" % (', '.join(str_values), ', '.join([repr(o) for o, c in self.options]))) def value__get(self): selected_values = [] if self.selectedIndices: selected_values = [self.options[i][0] for i in self.selectedIndices] elif not self._forced_values: selected_values = [] for option, checked in self.options: if checked: selected_values.append(option) if self._forced_values: selected_values += self._forced_values if self.options and (not selected_values): selected_values = None return selected_values value = property(value__get, value__set)
[docs]class Radio(Select): """Field representing ``<input type="radio">``""" def value__get(self): if self.selectedIndex is not None: return self.options[self.selectedIndex][0] else: for option, checked in self.options: if checked: return option else: return None value = property(value__get, Select.value__set)
[docs]class Checkbox(Field): """Field representing ``<input type="checkbox">`` .. attribute:: checked Returns True if checkbox is checked. """ def __init__(self, *args, **attrs): super(Checkbox, self).__init__(*args, **attrs) self._checked = 'checked' in attrs def value__set(self, value): self._checked = not not value def value__get(self): if self._checked: if self._value is None: return 'on' else: return self._value else: return None value = property(value__get, value__set) def checked__get(self): return bool(self._checked) def checked__set(self, value): self._checked = not not value checked = property(checked__get, checked__set)
[docs]class Text(Field): """Field representing ``<input type="text">``"""
[docs]class File(Field): """Field representing ``<input type="file">``""" # TODO: This doesn't actually handle file uploads and enctype def value__get(self): if self._value is None: return '' else: return self._value value = property(value__get, Field.value__set)
[docs]class Textarea(Text): """Field representing ``<textarea>``"""
[docs]class Hidden(Text): """Field representing ``<input type="hidden">``"""
[docs]class Submit(Field): """Field representing ``<input type="submit">`` and ``<button>``""" def value__get(self): return None def value__set(self, value): raise AttributeError( "You cannot set the value of the <%s> field %r" % (self.tag, self.name)) value = property(value__get, value__set) def value_if_submitted(self): # TODO: does this ever get set? return self._value
Field.classes['submit'] = Submit Field.classes['button'] = Submit Field.classes['image'] = Submit Field.classes['multiple_select'] = MultipleSelect Field.classes['select'] = Select Field.classes['hidden'] = Hidden Field.classes['file'] = File Field.classes['text'] = Text Field.classes['password'] = Text Field.classes['checkbox'] = Checkbox Field.classes['textarea'] = Textarea Field.classes['radio'] = Radio
[docs]class Form(object): """This object represents a form that has been found in a page. :param response: `webob.response.TestResponse` instance :param text: Unparsed html of the form .. attribute:: text the full HTML of the form. .. attribute:: action the relative URI of the action. .. attribute:: method the HTTP method (e.g., ``'GET'``). .. attribute:: id the id, or None if not given. .. attribute:: enctype encoding of the form submission .. attribute:: fields a dictionary of fields, each value is a list of fields by that name. ``<input type=\"radio\">`` and ``<select>`` are both represented as single fields with multiple options. .. attribute:: field_order Ordered list of field names as found in the html. """ # TODO: use BeautifulSoup4 for this _tag_re = re.compile(r'<(/?)([a-z0-9_\-]*)([^>]*?)>', re.I) _label_re = re.compile( '''<label\s+(?:[^>]*)for=(?:"|')([a-z0-9_\-]+)(?:"|')(?:[^>]*)>''', re.I) FieldClass = Field def __init__(self, response, text): self.response = response self.text = text self.html = BeautifulSoup(self.text, "html.parser") attrs = self.html('form')[0].attrs self.action = attrs.get('action', '') self.method = attrs.get('method', 'GET') self.id = attrs.get('id') self.enctype = attrs.get('enctype', 'application/x-www-form-urlencoded') self._parse_fields() def _parse_fields(self): fields = OrderedDict() field_order = [] tags = ('input', 'select', 'textarea', 'button') for pos, node in enumerate(self.html.findAll(tags)): attrs = node.attrs tag = node.name name = None if 'name' in attrs: name = attrs.pop('name') if tag == 'textarea': attrs['value'] = node.text tag_type = attrs.get('type', 'text').lower() if tag == 'select': tag_type = 'select' if tag_type == "select" and attrs.get("multiple"): tag_type = "multiple_select" FieldClass = self.FieldClass.classes.get(tag_type, self.FieldClass) if tag == 'input': if tag_type == 'radio': field = fields.get(name) if not field: field = FieldClass(self, tag, name, pos, **attrs) fields.setdefault(name, []).append(field) field_order.append((name, field)) else: field = field[0] assert isinstance(field, self.FieldClass.classes['radio']) field.options.append((attrs.get('value'), 'checked' in attrs)) continue field = FieldClass(self, tag, name, pos, **attrs) fields.setdefault(name, []).append(field) field_order.append((name, field)) if tag == 'select': for option in node('option'): field.options.append((option.attrs.get('value'), 'selected' in option.attrs)) self.field_order = field_order self.fields = fields def __setitem__(self, name, value): """Set the value of the named field. If there is 0 or multiple fields by that name, it is an error. Setting the value of a ``<select>`` selects the given option (and confirms it is an option). Setting radio fields does the same. Checkboxes get boolean values. You cannot set hidden fields or buttons. Use ``.set()`` if there is any ambiguity and you must provide an index. """ fields = self.fields.get(name) assert fields is not None, ( "No field by the name %r found (fields: %s)" % (name, ', '.join(map(repr, self.fields.keys())))) assert len(fields) == 1, ( "Multiple fields match %r: %s" % (name, ', '.join(map(repr, fields)))) fields[0].value = value def __getitem__(self, name): """Get the named field object (ambiguity is an error).""" fields = self.fields.get(name) assert fields is not None, ( "No field by the name %r found" % name) assert len(fields) == 1, ( "Multiple fields match %r: %s" % (name, ', '.join(map(repr, fields)))) return fields[0]
[docs] def lint(self): """ Check that the html is valid: - each field must have an id - each field must have a label """ labels = self._label_re.findall(self.text) for name, fields in self.fields.items(): for field in fields: if not isinstance(field, (Submit, Hidden)): if not field.id: raise AttributeError("%r as no id attribute" % field) elif field.id not in labels: raise AttributeError( "%r as no associated label" % field)
[docs] def set(self, name, value, index=None): """Set the given name, using ``index`` to disambiguate.""" if index is None: self[name] = value else: fields = self.fields.get(name) assert fields is not None, ( "No fields found matching %r" % name) field = fields[index] field.value = value
[docs] def get(self, name, index=None, default=utils.NoDefault): """ Get the named/indexed field object, or ``default`` if no field is found. Throws an AssertionError if no field is found and no ``default`` was given. """ fields = self.fields.get(name) if fields is None: if default is utils.NoDefault: raise AssertionError( "No fields found matching %r (and no default given)" % name) return default if index is None: return self[name] return fields[index]
[docs] def select(self, name, value, index=None): """Like ``.set()``, except also confirms the target is a ``<select>``. """ field = self.get(name, index=index) assert isinstance(field, Select) field.value = value
[docs] def submit(self, name=None, index=None, **args): """Submits the form. If ``name`` is given, then also select that button (using ``index`` to disambiguate)``. Any extra keyword arguments are passed to the :meth:`webtest.TestResponse.get` or :meth:`webtest.TestResponse.post` method. Returns a :class:`webtest.TestResponse` object. """ fields = self.submit_fields(name, index=index) if self.method.upper() != "GET": args.setdefault("content_type", self.enctype) return self.response.goto(self.action, method=self.method, params=fields, **args)
[docs] def upload_fields(self): """Return a list of file field tuples of the form:: (field name, file name) or:: (field name, file name, file contents). """ uploads = [] for name, fields in self.fields.items(): for field in fields: if isinstance(field, File) and field.value: uploads.append([name] + list(field.value)) return uploads
[docs] def submit_fields(self, name=None, index=None): """Return a list of ``[(name, value), ...]`` for the current state of the form. :param name: Same as for :meth:`submit` :param index: Same as for :meth:`submit` """ submit = [] # Use another name here so we can keep function param the same for BWC. submit_name = name if index is None: index = 0 # This counts all fields with the submit name not just submit fields. current_index = 0 for name, field in self.field_order: if name is None: # pragma: no cover continue if submit_name is not None and name == submit_name: if current_index == index: submit.append((name, field.value_if_submitted())) current_index += 1 else: value = field.value if value is None: continue if isinstance(field, File): submit.append((name, field)) continue if isinstance(value, list): for item in value: submit.append((name, item)) else: submit.append((name, value)) return submit
def __repr__(self): value = '<Form' if self.id: value += ' id=%r' % str(self.id) return value + ' />'

This Page