00001
00002 """Helpers to fill and submit forms."""
00003
00004 import re
00005 import sys
00006
00007 from bs4 import BeautifulSoup
00008 from webtest.compat import OrderedDict
00009 from webtest import utils
00010
00011
00012 class NoValue(object):
00013 pass
00014
00015
00016 class Upload(object):
00017 """
00018 A file to upload::
00019
00020 >>> Upload('filename.txt', 'data', 'application/octet-stream')
00021 <Upload "filename.txt">
00022 >>> Upload('filename.txt', 'data')
00023 <Upload "filename.txt">
00024 >>> Upload("README.txt")
00025 <Upload "README.txt">
00026
00027 :param filename: Name of the file to upload.
00028 :param content: Contents of the file.
00029 :param content_type: MIME type of the file.
00030
00031 """
00032
00033 def __init__(self, filename, content=None, content_type=None):
00034 self.filename = filename
00035 self.content = content
00036 self.content_type = content_type
00037
00038 def __iter__(self):
00039 yield self.filename
00040 if self.content:
00041 yield self.content
00042 yield self.content_type
00043
00044
00045
00046 def __repr__(self):
00047 return '<Upload "%s">' % self.filename
00048
00049
00050 class Field(object):
00051 """Base class for all Field objects.
00052
00053 .. attribute:: classes
00054
00055 Dictionary of field types (select, radio, etc)
00056
00057 .. attribute:: value
00058
00059 Set/get value of the field.
00060
00061 """
00062
00063 classes = {}
00064
00065 def __init__(self, form, tag, name, pos,
00066 value=None, id=None, **attrs):
00067 self.form = form
00068 self.tag = tag
00069 self.name = name
00070 self.pos = pos
00071 self._value = value
00072 self.id = id
00073 self.attrs = attrs
00074
00075 def value__get(self):
00076 if self._value is None:
00077 return ''
00078 else:
00079 return self._value
00080
00081 def value__set(self, value):
00082 self._value = value
00083
00084 value = property(value__get, value__set)
00085
00086 def force_value(self, value):
00087 """Like setting a value, except forces it (even for, say, hidden
00088 fields).
00089 """
00090 self._value = value
00091
00092 def __repr__(self):
00093 value = '<%s name="%s"' % (self.__class__.__name__, self.name)
00094 if self.id:
00095 value += ' id="%s"' % self.id
00096 return value + '>'
00097
00098
00099 class Select(Field):
00100 """Field representing ``<select />`` form element."""
00101
00102 def __init__(self, *args, **attrs):
00103 super(Select, self).__init__(*args, **attrs)
00104 self.options = []
00105
00106 self.selectedIndex = None
00107
00108 self._forced_value = NoValue
00109
00110 def force_value(self, value):
00111 """Like setting a value, except forces it (even for, say, hidden
00112 fields).
00113 """
00114 self._forced_value = value
00115
00116 def select(self, value=None, text=None):
00117 if value is not None and text is not None:
00118 raise ValueError("Specify only one of value and text.")
00119
00120 if text is not None:
00121 value = self._get_value_for_text(text)
00122
00123 self.value = value
00124
00125 def _get_value_for_text(self, text):
00126 for i, (option_value, checked, option_text) in enumerate(self.options):
00127 if option_text == utils.stringify(text):
00128 return option_value
00129
00130 raise ValueError("Option with text %r not found (from %s)"
00131 % (text, ', '.join(
00132 [repr(t) for o, c, t in self.options])))
00133
00134 def value__set(self, value):
00135 if self._forced_value is not NoValue:
00136 self._forced_value = NoValue
00137 for i, (option, checked, text) in enumerate(self.options):
00138 if option == utils.stringify(value):
00139 self.selectedIndex = i
00140 break
00141 else:
00142 raise ValueError(
00143 "Option %r not found (from %s)"
00144 % (value, ', '.join([repr(o) for o, c, t in self.options])))
00145
00146 def value__get(self):
00147 if self._forced_value is not NoValue:
00148 return self._forced_value
00149 elif self.selectedIndex is not None:
00150 return self.options[self.selectedIndex][0]
00151 else:
00152 for option, checked, text in self.options:
00153 if checked:
00154 return option
00155 else:
00156 if self.options:
00157 return self.options[0][0]
00158
00159 value = property(value__get, value__set)
00160
00161
00162 class MultipleSelect(Field):
00163 """Field representing ``<select multiple="multiple">``"""
00164
00165 def __init__(self, *args, **attrs):
00166 super(MultipleSelect, self).__init__(*args, **attrs)
00167 self.options = []
00168
00169 self.selectedIndices = []
00170 self._forced_values = []
00171
00172 def force_value(self, values):
00173 """Like setting a value, except forces it (even for, say, hidden
00174 fields).
00175 """
00176 self._forced_values = values
00177 self.selectedIndices = []
00178
00179 def select_multiple(self, value=None, texts=None):
00180 if value is not None and texts is not None:
00181 raise ValueError("Specify only one of value and texts.")
00182
00183 if texts is not None:
00184 value = self._get_value_for_texts(texts)
00185
00186 self.value = value
00187
00188 def _get_value_for_texts(self, texts):
00189 str_texts = [utils.stringify(text) for text in texts]
00190 value = []
00191 for i, (option, checked, text) in enumerate(self.options):
00192 if text in str_texts:
00193 value.append(option)
00194 str_texts.remove(text)
00195
00196 if str_texts:
00197 raise ValueError(
00198 "Option(s) %r not found (from %s)"
00199 % (', '.join(str_texts),
00200 ', '.join([repr(t) for o, c, t in self.options])))
00201
00202 return value
00203
00204 def value__set(self, values):
00205 str_values = [utils.stringify(value) for value in values]
00206 self.selectedIndices = []
00207 for i, (option, checked, text) in enumerate(self.options):
00208 if option in str_values:
00209 self.selectedIndices.append(i)
00210 str_values.remove(option)
00211 if str_values:
00212 raise ValueError(
00213 "Option(s) %r not found (from %s)"
00214 % (', '.join(str_values),
00215 ', '.join([repr(o) for o, c, t in self.options])))
00216
00217 def value__get(self):
00218 selected_values = []
00219 if self.selectedIndices:
00220 selected_values = [self.options[i][0]
00221 for i in self.selectedIndices]
00222 elif not self._forced_values:
00223 selected_values = []
00224 for option, checked, text in self.options:
00225 if checked:
00226 selected_values.append(option)
00227 if self._forced_values:
00228 selected_values += self._forced_values
00229
00230 if self.options and (not selected_values):
00231 selected_values = None
00232 return selected_values
00233 value = property(value__get, value__set)
00234
00235
00236 class Radio(Select):
00237 """Field representing ``<input type="radio">``"""
00238
00239 def value__get(self):
00240 if self._forced_value is not NoValue:
00241 return self._forced_value
00242 elif self.selectedIndex is not None:
00243 return self.options[self.selectedIndex][0]
00244 else:
00245 for option, checked, text in self.options:
00246 if checked:
00247 return option
00248 else:
00249 return None
00250
00251 value = property(value__get, Select.value__set)
00252
00253
00254 class Checkbox(Field):
00255 """Field representing ``<input type="checkbox">``
00256
00257 .. attribute:: checked
00258
00259 Returns True if checkbox is checked.
00260
00261 """
00262
00263 def __init__(self, *args, **attrs):
00264 super(Checkbox, self).__init__(*args, **attrs)
00265 self._checked = 'checked' in attrs
00266
00267 def value__set(self, value):
00268 self._checked = not not value
00269
00270 def value__get(self):
00271 if self._checked:
00272 if self._value is None:
00273 return 'on'
00274 else:
00275 return self._value
00276 else:
00277 return None
00278
00279 value = property(value__get, value__set)
00280
00281 def checked__get(self):
00282 return bool(self._checked)
00283
00284 def checked__set(self, value):
00285 self._checked = not not value
00286
00287 checked = property(checked__get, checked__set)
00288
00289
00290 class Text(Field):
00291 """Field representing ``<input type="text">``"""
00292
00293
00294 class File(Field):
00295 """Field representing ``<input type="file">``"""
00296
00297
00298 def value__get(self):
00299 if self._value is None:
00300 return ''
00301 else:
00302 return self._value
00303
00304 value = property(value__get, Field.value__set)
00305
00306
00307 class Textarea(Text):
00308 """Field representing ``<textarea>``"""
00309
00310
00311 class Hidden(Text):
00312 """Field representing ``<input type="hidden">``"""
00313
00314
00315 class Submit(Field):
00316 """Field representing ``<input type="submit">`` and ``<button>``"""
00317
00318 def value__get(self):
00319 return None
00320
00321 def value__set(self, value):
00322 raise AttributeError(
00323 "You cannot set the value of the <%s> field %r"
00324 % (self.tag, self.name))
00325
00326 value = property(value__get, value__set)
00327
00328 def value_if_submitted(self):
00329
00330 return self._value
00331
00332
00333 Field.classes['submit'] = Submit
00334
00335 Field.classes['button'] = Submit
00336
00337 Field.classes['image'] = Submit
00338
00339 Field.classes['multiple_select'] = MultipleSelect
00340
00341 Field.classes['select'] = Select
00342
00343 Field.classes['hidden'] = Hidden
00344
00345 Field.classes['file'] = File
00346
00347 Field.classes['text'] = Text
00348
00349 Field.classes['password'] = Text
00350
00351 Field.classes['checkbox'] = Checkbox
00352
00353 Field.classes['textarea'] = Textarea
00354
00355 Field.classes['radio'] = Radio
00356
00357
00358 class Form(object):
00359 """This object represents a form that has been found in a page.
00360
00361 :param response: `webob.response.TestResponse` instance
00362 :param text: Unparsed html of the form
00363
00364 .. attribute:: text
00365
00366 the full HTML of the form.
00367
00368 .. attribute:: action
00369
00370 the relative URI of the action.
00371
00372 .. attribute:: method
00373
00374 the HTTP method (e.g., ``'GET'``).
00375
00376 .. attribute:: id
00377
00378 the id, or None if not given.
00379
00380 .. attribute:: enctype
00381
00382 encoding of the form submission
00383
00384 .. attribute:: fields
00385
00386 a dictionary of fields, each value is a list of fields by
00387 that name. ``<input type=\"radio\">`` and ``<select>`` are
00388 both represented as single fields with multiple options.
00389
00390 .. attribute:: field_order
00391
00392 Ordered list of field names as found in the html.
00393
00394 """
00395
00396
00397
00398 _tag_re = re.compile(r'<(/?)([a-z0-9_\-]*)([^>]*?)>', re.I)
00399 _label_re = re.compile(
00400 '''<label\s+(?:[^>]*)for=(?:"|')([a-z0-9_\-]+)(?:"|')(?:[^>]*)>''',
00401 re.I)
00402
00403 FieldClass = Field
00404
00405 def __init__(self, response, text, parser_features='html.parser'):
00406 self.response = response
00407 self.text = text
00408 self.html = BeautifulSoup(self.text, parser_features)
00409
00410 attrs = self.html('form')[0].attrs
00411 self.action = attrs.get('action', '')
00412 self.method = attrs.get('method', 'GET')
00413 self.id = attrs.get('id')
00414 self.enctype = attrs.get('enctype',
00415 'application/x-www-form-urlencoded')
00416
00417 self._parse_fields()
00418
00419 def _parse_fields(self):
00420 fields = OrderedDict()
00421 field_order = []
00422 tags = ('input', 'select', 'textarea', 'button')
00423 for pos, node in enumerate(self.html.findAll(tags)):
00424 attrs = dict(node.attrs)
00425 tag = node.name
00426 name = None
00427 if 'name' in attrs:
00428 name = attrs.pop('name')
00429
00430 if tag == 'textarea':
00431 if node.text.startswith('\r\n'):
00432 text = node.text[2:]
00433 elif node.text.startswith('\n'):
00434 text = node.text[1:]
00435 else:
00436 text = node.text
00437 attrs['value'] = text
00438
00439 tag_type = attrs.get('type', 'text').lower()
00440 if tag == 'select':
00441 tag_type = 'select'
00442 if tag_type == "select" and "multiple" in attrs:
00443 tag_type = "multiple_select"
00444 if tag == 'button':
00445 tag_type = 'submit'
00446
00447 FieldClass = self.FieldClass.classes.get(tag_type,
00448 self.FieldClass)
00449
00450
00451 if sys.version_info[:2] <= (2, 6):
00452 attrs = dict((k.encode('utf-8') if isinstance(k, unicode)
00453 else k, v) for k, v in attrs.items())
00454
00455 if tag == 'input':
00456 if tag_type == 'radio':
00457 field = fields.get(name)
00458 if not field:
00459 field = FieldClass(self, tag, name, pos, **attrs)
00460 fields.setdefault(name, []).append(field)
00461 field_order.append((name, field))
00462 else:
00463 field = field[0]
00464 assert isinstance(field,
00465 self.FieldClass.classes['radio'])
00466 field.options.append((attrs.get('value'),
00467 'checked' in attrs,
00468 None))
00469 continue
00470 elif tag_type == 'file':
00471 if 'value' in attrs:
00472 del attrs['value']
00473
00474 field = FieldClass(self, tag, name, pos, **attrs)
00475 fields.setdefault(name, []).append(field)
00476 field_order.append((name, field))
00477
00478 if tag == 'select':
00479 for option in node('option'):
00480 field.options.append(
00481 (option.attrs.get('value', option.text),
00482 'selected' in option.attrs,
00483 option.text))
00484
00485 self.field_order = field_order
00486 self.fields = fields
00487
00488 def __setitem__(self, name, value):
00489 """Set the value of the named field. If there is 0 or multiple fields
00490 by that name, it is an error.
00491
00492 Multiple checkboxes of the same name are special-cased; a list may be
00493 assigned to them to check the checkboxes whose value is present in the
00494 list (and uncheck all others).
00495
00496 Setting the value of a ``<select>`` selects the given option (and
00497 confirms it is an option). Setting radio fields does the same.
00498 Checkboxes get boolean values. You cannot set hidden fields or buttons.
00499
00500 Use ``.set()`` if there is any ambiguity and you must provide an index.
00501 """
00502 fields = self.fields.get(name)
00503 assert fields is not None, (
00504 "No field by the name %r found (fields: %s)"
00505 % (name, ', '.join(map(repr, self.fields.keys()))))
00506 all_checkboxes = all(isinstance(f, Checkbox) for f in fields)
00507 if all_checkboxes and isinstance(value, list):
00508 values = set(utils.stringify(v) for v in value)
00509 for f in fields:
00510 f.checked = f._value in values
00511 else:
00512 assert len(fields) == 1, (
00513 "Multiple fields match %r: %s"
00514 % (name, ', '.join(map(repr, fields))))
00515 fields[0].value = value
00516
00517 def __getitem__(self, name):
00518 """Get the named field object (ambiguity is an error)."""
00519 fields = self.fields.get(name)
00520 assert fields is not None, (
00521 "No field by the name %r found" % name)
00522 assert len(fields) == 1, (
00523 "Multiple fields match %r: %s"
00524 % (name, ', '.join(map(repr, fields))))
00525 return fields[0]
00526
00527 def lint(self):
00528 """
00529 Check that the html is valid:
00530
00531 - each field must have an id
00532 - each field must have a label
00533
00534 """
00535 labels = self._label_re.findall(self.text)
00536 for name, fields in self.fields.items():
00537 for field in fields:
00538 if not isinstance(field, (Submit, Hidden)):
00539 if not field.id:
00540 raise AttributeError("%r as no id attribute" % field)
00541 elif field.id not in labels:
00542 raise AttributeError(
00543 "%r as no associated label" % field)
00544
00545 def set(self, name, value, index=None):
00546 """Set the given name, using ``index`` to disambiguate."""
00547 if index is None:
00548 self[name] = value
00549 else:
00550 fields = self.fields.get(name)
00551 assert fields is not None, (
00552 "No fields found matching %r" % name)
00553 field = fields[index]
00554 field.value = value
00555
00556 def get(self, name, index=None, default=utils.NoDefault):
00557 """
00558 Get the named/indexed field object, or ``default`` if no field is
00559 found. Throws an AssertionError if no field is found and no ``default``
00560 was given.
00561 """
00562 fields = self.fields.get(name)
00563 if fields is None:
00564 if default is utils.NoDefault:
00565 raise AssertionError(
00566 "No fields found matching %r (and no default given)"
00567 % name)
00568 return default
00569 if index is None:
00570 return self[name]
00571 return fields[index]
00572
00573 def select(self, name, value=None, text=None, index=None):
00574 """Like ``.set()``, except also confirms the target is a ``<select>``
00575 and allows selecting options by text.
00576 """
00577 field = self.get(name, index=index)
00578 assert isinstance(field, Select)
00579
00580 field.select(value, text)
00581
00582 def select_multiple(self, name, value=None, texts=None, index=None):
00583 """Like ``.set()``, except also confirms the target is a
00584 ``<select multiple>`` and allows selecting options by text.
00585 """
00586 field = self.get(name, index=index)
00587 assert isinstance(field, MultipleSelect)
00588
00589 field.select_multiple(value, texts)
00590
00591 def submit(self, name=None, index=None, value=None, **args):
00592 """Submits the form. If ``name`` is given, then also select that
00593 button (using ``index`` or ``value`` to disambiguate)``.
00594
00595 Any extra keyword arguments are passed to the
00596 :meth:`webtest.TestResponse.get` or
00597 :meth:`webtest.TestResponse.post` method.
00598
00599 Returns a :class:`webtest.TestResponse` object.
00600
00601 """
00602 fields = self.submit_fields(name, index=index, submit_value=value)
00603 if self.method.upper() != "GET":
00604 args.setdefault("content_type", self.enctype)
00605 return self.response.goto(self.action, method=self.method,
00606 params=fields, **args)
00607
00608 def upload_fields(self):
00609 """Return a list of file field tuples of the form::
00610
00611 (field name, file name)
00612
00613 or::
00614
00615 (field name, file name, file contents).
00616
00617 """
00618 uploads = []
00619 for name, fields in self.fields.items():
00620 for field in fields:
00621 if isinstance(field, File) and field.value:
00622 uploads.append([name] + list(field.value))
00623 return uploads
00624
00625 def submit_fields(self, name=None, index=None, submit_value=None):
00626 """Return a list of ``[(name, value), ...]`` for the current state of
00627 the form.
00628
00629 :param name: Same as for :meth:`submit`
00630 :param index: Same as for :meth:`submit`
00631
00632 """
00633 submit = []
00634
00635 submit_name = name
00636 if index is not None and submit_value is not None:
00637 raise ValueError("Can't specify both submit_value and index.")
00638
00639
00640 if index is None and submit_value is None:
00641 index = 0
00642
00643
00644 current_index = 0
00645 for name, field in self.field_order:
00646 if name is None:
00647 continue
00648 if submit_name is not None and name == submit_name:
00649 if index is not None and current_index == index:
00650 submit.append((name, field.value_if_submitted()))
00651 if submit_value is not None and \
00652 field.value_if_submitted() == submit_value:
00653 submit.append((name, field.value_if_submitted()))
00654 current_index += 1
00655 else:
00656 value = field.value
00657 if value is None:
00658 continue
00659 if isinstance(field, File):
00660 submit.append((name, field))
00661 continue
00662 if isinstance(value, list):
00663 for item in value:
00664 submit.append((name, item))
00665 else:
00666 submit.append((name, value))
00667 return submit
00668
00669 def __repr__(self):
00670 value = '<Form'
00671 if self.id:
00672 value += ' id=%r' % str(self.id)
00673 return value + ' />'