PageRenderTime 57ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/webtest/app.py

https://bitbucket.org/carmelly/webtest
Python | 1774 lines | 1744 code | 14 blank | 16 comment | 24 complexity | 7b734722fdbb3d9450173bd4cb4a3ca1 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. # (c) 2005 Ian Bicking and contributors; written for Paste
  2. # (http://pythonpaste.org)
  3. # Licensed under the MIT license:
  4. # http://www.opensource.org/licenses/mit-license.php
  5. """
  6. Routines for testing WSGI applications.
  7. Most interesting is TestApp
  8. """
  9. import random
  10. import warnings
  11. import mimetypes
  12. import cgi
  13. import os
  14. import re
  15. import fnmatch
  16. from webtest.compat import urlparse
  17. from webtest.compat import print_stderr
  18. from webtest.compat import StringIO
  19. from webtest.compat import BytesIO
  20. from webtest.compat import SimpleCookie, CookieError
  21. from webtest.compat import cookie_quote
  22. from webtest.compat import urlencode
  23. from webtest.compat import splittype
  24. from webtest.compat import splithost
  25. from webtest.compat import string_types
  26. from webtest.compat import binary_type
  27. from webtest.compat import text_type
  28. from webtest.compat import to_string
  29. from webtest.compat import to_bytes
  30. from webtest.compat import join_bytes
  31. from webtest.compat import OrderedDict
  32. from webtest.compat import dumps
  33. from webtest.compat import loads
  34. from webtest.compat import PY3
  35. from webob import Request, Response
  36. if PY3:
  37. from webtest import lint3 as lint
  38. else:
  39. from webtest import lint
  40. __all__ = ['TestApp', 'TestRequest']
  41. class NoDefault(object):
  42. pass
  43. class AppError(Exception):
  44. def __init__(self, message, *args):
  45. message = to_string(message)
  46. str_args = ()
  47. for arg in args:
  48. if isinstance(arg, Response):
  49. body = arg.body
  50. if isinstance(body, binary_type):
  51. if arg.charset:
  52. arg = body.decode(arg.charset)
  53. else:
  54. arg = repr(body)
  55. elif isinstance(arg, binary_type):
  56. try:
  57. arg = to_string(arg)
  58. except UnicodeDecodeError:
  59. arg = repr(arg)
  60. str_args += (arg,)
  61. message = message % str_args
  62. Exception.__init__(self, message)
  63. class TestResponse(Response):
  64. """
  65. Instances of this class are return by ``TestApp``
  66. """
  67. request = None
  68. _forms_indexed = None
  69. def forms__get(self):
  70. """
  71. Returns a dictionary of :class:`~webtest.Form` objects. Indexes are
  72. both in order (from zero) and by form id (if the form is given an id).
  73. """
  74. if self._forms_indexed is None:
  75. self._parse_forms()
  76. return self._forms_indexed
  77. forms = property(forms__get,
  78. doc="""
  79. A list of :class:`~webtest.Form`s found on the page
  80. """)
  81. def form__get(self):
  82. forms = self.forms
  83. if not forms:
  84. raise TypeError(
  85. "You used response.form, but no forms exist")
  86. if 1 in forms:
  87. # There is more than one form
  88. raise TypeError(
  89. "You used response.form, but more than one form exists")
  90. return forms[0]
  91. form = property(form__get,
  92. doc="""
  93. Returns a single :class:`~webtest.Form` instance; it is an
  94. error if there are multiple forms on the page.
  95. """)
  96. @property
  97. def testbody(self):
  98. if getattr(self, '_use_unicode', True) and self.charset:
  99. return self.unicode_body
  100. if PY3:
  101. return to_string(self.body)
  102. return self.body
  103. _tag_re = re.compile(r'<(/?)([:a-z0-9_\-]*)(.*?)>', re.S | re.I)
  104. def _parse_forms(self):
  105. forms = self._forms_indexed = {}
  106. form_texts = []
  107. started = None
  108. for match in self._tag_re.finditer(self.testbody):
  109. end = match.group(1) == '/'
  110. tag = match.group(2).lower()
  111. if tag != 'form':
  112. continue
  113. if end:
  114. assert started, (
  115. "</form> unexpected at %s" % match.start())
  116. form_texts.append(self.testbody[started:match.end()])
  117. started = None
  118. else:
  119. assert not started, (
  120. "Nested form tags at %s" % match.start())
  121. started = match.start()
  122. assert not started, (
  123. "Danging form: %r" % self.testbody[started:])
  124. for i, text in enumerate(form_texts):
  125. form = Form(self, text)
  126. forms[i] = form
  127. if form.id:
  128. forms[form.id] = form
  129. def follow(self, **kw):
  130. """
  131. If this request is a redirect, follow that redirect. It
  132. is an error if this is not a redirect response. Returns
  133. another response object.
  134. """
  135. assert self.status_int >= 300 and self.status_int < 400, (
  136. "You can only follow redirect responses (not %s)"
  137. % self.status)
  138. location = self.headers['location']
  139. type, rest = splittype(location)
  140. host, path = splithost(rest)
  141. # @@: We should test that it's not a remote redirect
  142. return self.test_app.get(location, **kw)
  143. def click(self, description=None, linkid=None, href=None,
  144. anchor=None, index=None, verbose=False,
  145. extra_environ=None):
  146. """
  147. Click the link as described. Each of ``description``,
  148. ``linkid``, and ``url`` are *patterns*, meaning that they are
  149. either strings (regular expressions), compiled regular
  150. expressions (objects with a ``search`` method), or callables
  151. returning true or false.
  152. All the given patterns are ANDed together:
  153. * ``description`` is a pattern that matches the contents of the
  154. anchor (HTML and all -- everything between ``<a...>`` and
  155. ``</a>``)
  156. * ``linkid`` is a pattern that matches the ``id`` attribute of
  157. the anchor. It will receive the empty string if no id is
  158. given.
  159. * ``href`` is a pattern that matches the ``href`` of the anchor;
  160. the literal content of that attribute, not the fully qualified
  161. attribute.
  162. * ``anchor`` is a pattern that matches the entire anchor, with
  163. its contents.
  164. If more than one link matches, then the ``index`` link is
  165. followed. If ``index`` is not given and more than one link
  166. matches, or if no link matches, then ``IndexError`` will be
  167. raised.
  168. If you give ``verbose`` then messages will be printed about
  169. each link, and why it does or doesn't match. If you use
  170. ``app.click(verbose=True)`` you'll see a list of all the
  171. links.
  172. You can use multiple criteria to essentially assert multiple
  173. aspects about the link, e.g., where the link's destination is.
  174. """
  175. __tracebackhide__ = True
  176. found_html, found_desc, found_attrs = self._find_element(
  177. tag='a', href_attr='href',
  178. href_extract=None,
  179. content=description,
  180. id=linkid,
  181. href_pattern=href,
  182. html_pattern=anchor,
  183. index=index, verbose=verbose)
  184. return self.goto(found_attrs['uri'], extra_environ=extra_environ)
  185. def clickbutton(self, description=None, buttonid=None, href=None,
  186. button=None, index=None, verbose=False):
  187. """
  188. Like ``.click()``, except looks for link-like buttons.
  189. This kind of button should look like
  190. ``<button onclick="...location.href='url'...">``.
  191. """
  192. __tracebackhide__ = True
  193. found_html, found_desc, found_attrs = self._find_element(
  194. tag='button', href_attr='onclick',
  195. href_extract=re.compile(r"location\.href='(.*?)'"),
  196. content=description,
  197. id=buttonid,
  198. href_pattern=href,
  199. html_pattern=button,
  200. index=index, verbose=verbose)
  201. return self.goto(found_attrs['uri'])
  202. def _find_element(self, tag, href_attr, href_extract,
  203. content, id,
  204. href_pattern,
  205. html_pattern,
  206. index, verbose):
  207. content_pat = _make_pattern(content)
  208. id_pat = _make_pattern(id)
  209. href_pat = _make_pattern(href_pattern)
  210. html_pat = _make_pattern(html_pattern)
  211. body = self.testbody
  212. _tag_re = re.compile(r'<%s\s+(.*?)>(.*?)</%s>' % (tag, tag),
  213. re.I + re.S)
  214. _script_re = re.compile(r'<script.*?>.*?</script>', re.I | re.S)
  215. bad_spans = []
  216. for match in _script_re.finditer(body):
  217. bad_spans.append((match.start(), match.end()))
  218. def printlog(s):
  219. if verbose:
  220. print(s)
  221. found_links = []
  222. total_links = 0
  223. for match in _tag_re.finditer(body):
  224. found_bad = False
  225. for bad_start, bad_end in bad_spans:
  226. if (match.start() > bad_start
  227. and match.end() < bad_end):
  228. found_bad = True
  229. break
  230. if found_bad:
  231. continue
  232. el_html = match.group(0)
  233. el_attr = match.group(1)
  234. el_content = match.group(2)
  235. attrs = _parse_attrs(el_attr)
  236. if verbose:
  237. printlog('Element: %r' % el_html)
  238. if not attrs.get(href_attr):
  239. printlog(' Skipped: no %s attribute' % href_attr)
  240. continue
  241. el_href = attrs[href_attr]
  242. if href_extract:
  243. m = href_extract.search(el_href)
  244. if not m:
  245. printlog(" Skipped: doesn't match extract pattern")
  246. continue
  247. el_href = m.group(1)
  248. attrs['uri'] = el_href
  249. if el_href.startswith('#'):
  250. printlog(' Skipped: only internal fragment href')
  251. continue
  252. if el_href.startswith('javascript:'):
  253. printlog(' Skipped: cannot follow javascript:')
  254. continue
  255. total_links += 1
  256. if content_pat and not content_pat(el_content):
  257. printlog(" Skipped: doesn't match description")
  258. continue
  259. if id_pat and not id_pat(attrs.get('id', '')):
  260. printlog(" Skipped: doesn't match id")
  261. continue
  262. if href_pat and not href_pat(el_href):
  263. printlog(" Skipped: doesn't match href")
  264. continue
  265. if html_pat and not html_pat(el_html):
  266. printlog(" Skipped: doesn't match html")
  267. continue
  268. printlog(" Accepted")
  269. found_links.append((el_html, el_content, attrs))
  270. if not found_links:
  271. raise IndexError(
  272. "No matching elements found (from %s possible)"
  273. % total_links)
  274. if index is None:
  275. if len(found_links) > 1:
  276. raise IndexError(
  277. "Multiple links match: %s"
  278. % ', '.join([repr(anc) for anc, d, attr in found_links]))
  279. found_link = found_links[0]
  280. else:
  281. try:
  282. found_link = found_links[index]
  283. except IndexError:
  284. raise IndexError(
  285. "Only %s (out of %s) links match; index %s out of range"
  286. % (len(found_links), total_links, index))
  287. return found_link
  288. def goto(self, href, method='get', **args):
  289. """
  290. Go to the (potentially relative) link ``href``, using the
  291. given method (``'get'`` or ``'post'``) and any extra arguments
  292. you want to pass to the ``app.get()`` or ``app.post()``
  293. methods.
  294. All hostnames and schemes will be ignored.
  295. """
  296. scheme, host, path, query, fragment = urlparse.urlsplit(href)
  297. # We
  298. scheme = host = fragment = ''
  299. href = urlparse.urlunsplit((scheme, host, path, query, fragment))
  300. href = urlparse.urljoin(self.request.url, href)
  301. method = method.lower()
  302. assert method in ('get', 'post'), (
  303. 'Only "get" or "post" are allowed for method (you gave %r)'
  304. % method)
  305. # encode unicode strings for the outside world
  306. if not PY3 and getattr(self, '_use_unicode', False):
  307. def to_str(s):
  308. if isinstance(s, text_type):
  309. return s.encode(self.charset)
  310. return s
  311. href = to_str(href)
  312. if 'params' in args:
  313. args['params'] = [tuple(map(to_str, p)) \
  314. for p in args['params']]
  315. if 'upload_files' in args:
  316. args['upload_files'] = [map(to_str, f) \
  317. for f in args['upload_files']]
  318. if 'content_type' in args:
  319. args['content_type'] = to_str(args['content_type'])
  320. if method == 'get':
  321. method = self.test_app.get
  322. else:
  323. method = self.test_app.post
  324. return method(href, **args)
  325. _normal_body_regex = re.compile(to_bytes(r'[ \n\r\t]+'))
  326. _normal_body = None
  327. def normal_body__get(self):
  328. if self._normal_body is None:
  329. self._normal_body = self._normal_body_regex.sub(
  330. to_bytes(' '), self.body)
  331. return self._normal_body
  332. normal_body = property(normal_body__get,
  333. doc="""
  334. Return the whitespace-normalized body
  335. """.strip())
  336. def unicode_normal_body__get(self):
  337. if not self.charset:
  338. raise AttributeError(
  339. ("You cannot access Response.unicode_normal_body "
  340. "unless charset is set"))
  341. return self.normal_body.decode(self.charset)
  342. unicode_normal_body = property(
  343. unicode_normal_body__get, doc="""
  344. Return the whitespace-normalized body, as unicode
  345. """.strip())
  346. def __contains__(self, s):
  347. """
  348. A response 'contains' a string if it is present in the body
  349. of the response. Whitespace is normalized when searching
  350. for a string.
  351. """
  352. if not isinstance(s, string_types):
  353. if hasattr(s, '__unicode__'):
  354. s = s.__unicode__()
  355. else:
  356. s = str(s)
  357. # PY3 Workaround.
  358. # We don't want to search for str when we have no charset
  359. if isinstance(s, text_type) and not self.charset:
  360. s = to_bytes(s)
  361. if isinstance(s, text_type):
  362. body = self.unicode_body
  363. normal_body = self.unicode_normal_body
  364. else:
  365. body = self.body
  366. normal_body = self.normal_body
  367. return s in body or s in normal_body
  368. def mustcontain(self, *strings, **kw):
  369. """
  370. Assert that the response contains all of the strings passed
  371. in as arguments.
  372. Equivalent to::
  373. assert string in res
  374. """
  375. if 'no' in kw:
  376. no = kw['no']
  377. del kw['no']
  378. if isinstance(no, string_types):
  379. no = [no]
  380. else:
  381. no = []
  382. if kw:
  383. raise TypeError(
  384. "The only keyword argument allowed is 'no'")
  385. for s in strings:
  386. if not s in self:
  387. print_stderr("Actual response (no %r):" % s)
  388. print_stderr(str(self))
  389. raise IndexError(
  390. "Body does not contain string %r" % s)
  391. for no_s in no:
  392. if no_s in self:
  393. print_stderr("Actual response (has %r)" % no_s)
  394. print_stderr(str(self))
  395. raise IndexError(
  396. "Body contains bad string %r" % no_s)
  397. def __str__(self):
  398. simple_body = '\n'.join([l for l in self.testbody.splitlines()
  399. if l.strip()])
  400. headers = [(self._normalize_header_name(n), v)
  401. for n, v in self.headerlist
  402. if n.lower() != 'content-length']
  403. headers.sort()
  404. return 'Response: %s\n%s\n%s' % (
  405. to_string(self.status),
  406. '\n'.join(['%s: %s' % (n, v) for n, v in headers]),
  407. simple_body)
  408. def _normalize_header_name(self, name):
  409. name = name.replace('-', ' ').title().replace(' ', '-')
  410. return name
  411. def __repr__(self):
  412. # Specifically intended for doctests
  413. if self.content_type:
  414. ct = ' %s' % self.content_type
  415. else:
  416. ct = ''
  417. if self.body:
  418. br = repr(self.body)
  419. if len(br) > 18:
  420. br = br[:10] + '...' + br[-5:]
  421. br += '/%s' % len(self.body)
  422. body = ' body=%s' % br
  423. else:
  424. body = ' no body'
  425. if self.location:
  426. location = ' location: %s' % self.location
  427. else:
  428. location = ''
  429. return ('<' + to_string(self.status) + ct + location + body + '>')
  430. def html(self):
  431. """
  432. Returns the response as a `BeautifulSoup
  433. <http://www.crummy.com/software/BeautifulSoup/documentation.html>`_
  434. object.
  435. Only works with HTML responses; other content-types raise
  436. AttributeError.
  437. """
  438. if 'html' not in self.content_type:
  439. raise AttributeError(
  440. "Not an HTML response body (content-type: %s)"
  441. % self.content_type)
  442. try:
  443. from BeautifulSoup import BeautifulSoup
  444. except ImportError:
  445. raise ImportError(
  446. "You must have BeautifulSoup installed to use response.html")
  447. soup = BeautifulSoup(self.testbody)
  448. return soup
  449. html = property(html, doc=html.__doc__)
  450. def xml(self):
  451. """
  452. Returns the response as an `ElementTree
  453. <http://python.org/doc/current/lib/module-xml.etree.ElementTree.html>`_
  454. object.
  455. Only works with XML responses; other content-types raise
  456. AttributeError
  457. """
  458. if 'xml' not in self.content_type:
  459. raise AttributeError(
  460. "Not an XML response body (content-type: %s)"
  461. % self.content_type)
  462. try:
  463. from xml.etree import ElementTree
  464. except ImportError:
  465. try:
  466. import ElementTree
  467. except ImportError:
  468. try:
  469. from elementtree import ElementTree
  470. except ImportError:
  471. raise ImportError(
  472. ("You must have ElementTree installed "
  473. "(or use Python 2.5) to use response.xml"))
  474. # ElementTree can't parse unicode => use `body` instead of `testbody`
  475. return ElementTree.XML(self.body)
  476. xml = property(xml, doc=xml.__doc__)
  477. def lxml(self):
  478. """
  479. Returns the response as an `lxml object
  480. <http://codespeak.net/lxml/>`_. You must have lxml installed
  481. to use this.
  482. If this is an HTML response and you have lxml 2.x installed,
  483. then an ``lxml.html.HTML`` object will be returned; if you
  484. have an earlier version of lxml then a ``lxml.HTML`` object
  485. will be returned.
  486. """
  487. if ('html' not in self.content_type
  488. and 'xml' not in self.content_type):
  489. raise AttributeError(
  490. "Not an XML or HTML response body (content-type: %s)"
  491. % self.content_type)
  492. try:
  493. from lxml import etree
  494. except ImportError:
  495. raise ImportError(
  496. "You must have lxml installed to use response.lxml")
  497. try:
  498. from lxml.html import fromstring
  499. except ImportError:
  500. fromstring = etree.HTML
  501. ## FIXME: would be nice to set xml:base, in some fashion
  502. if self.content_type == 'text/html':
  503. return fromstring(self.testbody, base_url=self.request.url)
  504. else:
  505. return etree.XML(self.testbody, base_url=self.request.url)
  506. lxml = property(lxml, doc=lxml.__doc__)
  507. def json(self):
  508. """
  509. Return the response as a JSON response. You must have `simplejson
  510. <http://goo.gl/B9g6s>`_ installed to use this, or be using a Python
  511. version with the json module.
  512. The content type must be application/json to use this.
  513. """
  514. if self.content_type != 'application/json':
  515. raise AttributeError(
  516. "Not a JSON response body (content-type: %s)"
  517. % self.content_type)
  518. if loads is None:
  519. raise ImportError(
  520. "You must have simplejson installed to use response.json")
  521. return loads(self.testbody)
  522. json = property(json, doc=json.__doc__)
  523. def pyquery(self):
  524. """
  525. Returns the response as a `PyQuery <http://pyquery.org/>`_ object.
  526. Only works with HTML and XML responses; other content-types raise
  527. AttributeError.
  528. """
  529. if 'html' not in self.content_type and 'xml' not in self.content_type:
  530. raise AttributeError(
  531. "Not an HTML or XML response body (content-type: %s)"
  532. % self.content_type)
  533. try:
  534. from pyquery import PyQuery
  535. except ImportError:
  536. raise ImportError(
  537. "You must have PyQuery installed to use response.pyquery")
  538. d = PyQuery(self.testbody)
  539. return d
  540. pyquery = property(pyquery, doc=pyquery.__doc__)
  541. def showbrowser(self):
  542. """
  543. Show this response in a browser window (for debugging purposes,
  544. when it's hard to read the HTML).
  545. """
  546. import webbrowser
  547. import tempfile
  548. f = tempfile.NamedTemporaryFile(prefix='webtest-page',
  549. suffix='.html')
  550. name = f.name
  551. f.close()
  552. f = open(name, 'w')
  553. f.write(to_string(self.body))
  554. f.close()
  555. if name[0] != '/':
  556. # windows ...
  557. url = 'file:///' + name
  558. else:
  559. url = 'file://' + name
  560. webbrowser.open_new(url)
  561. class TestRequest(Request):
  562. # for py.test
  563. disabled = True
  564. ResponseClass = TestResponse
  565. class TestApp(object):
  566. """
  567. Wraps a WSGI application in a more convenient interface for
  568. testing.
  569. ``app`` may be an application, or a Paste Deploy app
  570. URI, like ``'config:filename.ini#test'``.
  571. ``extra_environ`` is a dictionary of values that should go
  572. into the environment for each request. These can provide a
  573. communication channel with the application.
  574. ``relative_to`` is a directory, and filenames used for file
  575. uploads are calculated relative to this. Also ``config:``
  576. URIs that aren't absolute.
  577. """
  578. # for py.test
  579. disabled = True
  580. RequestClass = TestRequest
  581. def __init__(self, app, extra_environ=None, relative_to=None,
  582. use_unicode=True):
  583. if isinstance(app, string_types):
  584. from paste.deploy import loadapp
  585. # @@: Should pick up relative_to from calling module's
  586. # __file__
  587. app = loadapp(app, relative_to=relative_to)
  588. self.app = app
  589. self.relative_to = relative_to
  590. if extra_environ is None:
  591. extra_environ = {}
  592. self.extra_environ = extra_environ
  593. self.use_unicode = use_unicode
  594. self.reset()
  595. def reset(self):
  596. """
  597. Resets the state of the application; currently just clears
  598. saved cookies.
  599. """
  600. self.cookies = {}
  601. def _make_environ(self, extra_environ=None):
  602. environ = self.extra_environ.copy()
  603. environ['paste.throw_errors'] = True
  604. if extra_environ:
  605. environ.update(extra_environ)
  606. return environ
  607. def _remove_fragment(self, url):
  608. scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
  609. return urlparse.urlunsplit((scheme, netloc, path, query, ""))
  610. def get(self, url, params=None, headers=None, extra_environ=None,
  611. status=None, expect_errors=False):
  612. """
  613. Get the given url (well, actually a path like
  614. ``'/page.html'``).
  615. ``params``:
  616. A query string, or a dictionary that will be encoded
  617. into a query string. You may also include a query
  618. string on the ``url``.
  619. ``headers``:
  620. A dictionary of extra headers to send.
  621. ``extra_environ``:
  622. A dictionary of environmental variables that should
  623. be added to the request.
  624. ``status``:
  625. The integer status code you expect (if not 200 or 3xx).
  626. If you expect a 404 response, for instance, you must give
  627. ``status=404`` or it will be an error. You can also give
  628. a wildcard, like ``'3*'`` or ``'*'``.
  629. ``expect_errors``:
  630. If this is not true, then if anything is written to
  631. ``wsgi.errors`` it will be an error. If it is true, then
  632. non-200/3xx responses are also okay.
  633. Returns a :class:`webtest.TestResponse` object.
  634. """
  635. environ = self._make_environ(extra_environ)
  636. # Hide from py.test:
  637. __tracebackhide__ = True
  638. url = str(url)
  639. url = self._remove_fragment(url)
  640. if params:
  641. if not isinstance(params, string_types):
  642. params = urlencode(params, doseq=True)
  643. if '?' in url:
  644. url += '&'
  645. else:
  646. url += '?'
  647. url += params
  648. if '?' in url:
  649. url, environ['QUERY_STRING'] = url.split('?', 1)
  650. else:
  651. environ['QUERY_STRING'] = ''
  652. req = self.RequestClass.blank(url, environ)
  653. if headers:
  654. req.headers.update(headers)
  655. return self.do_request(req, status=status,
  656. expect_errors=expect_errors)
  657. def _gen_request(self, method, url, params='', headers=None,
  658. extra_environ=None, status=None, upload_files=None,
  659. expect_errors=False, content_type=None):
  660. """
  661. Do a generic request.
  662. """
  663. environ = self._make_environ(extra_environ)
  664. # @@: Should this be all non-strings?
  665. params = encode_params(params, content_type)
  666. if upload_files or \
  667. (content_type and to_string(content_type).startswith('multipart')):
  668. params = cgi.parse_qsl(params, keep_blank_values=True)
  669. content_type, params = self.encode_multipart(
  670. params, upload_files or ())
  671. environ['CONTENT_TYPE'] = content_type
  672. elif params:
  673. environ.setdefault('CONTENT_TYPE',
  674. 'application/x-www-form-urlencoded')
  675. if '?' in url:
  676. url, environ['QUERY_STRING'] = url.split('?', 1)
  677. else:
  678. environ['QUERY_STRING'] = ''
  679. if content_type is not None:
  680. environ['CONTENT_TYPE'] = content_type
  681. environ['CONTENT_LENGTH'] = str(len(params))
  682. environ['REQUEST_METHOD'] = method
  683. environ['wsgi.input'] = BytesIO(to_bytes(params))
  684. url = self._remove_fragment(url)
  685. req = self.RequestClass.blank(url, environ)
  686. if headers:
  687. req.headers.update(headers)
  688. return self.do_request(req, status=status,
  689. expect_errors=expect_errors)
  690. def post(self, url, params='', headers=None, extra_environ=None,
  691. status=None, upload_files=None, expect_errors=False,
  692. content_type=None):
  693. """
  694. Do a POST request. Very like the ``.get()`` method.
  695. ``params`` are put in the body of the request.
  696. ``upload_files`` is for file uploads. It should be a list of
  697. ``[(fieldname, filename, file_content)]``. You can also use
  698. just ``[(fieldname, filename)]`` and the file content will be
  699. read from disk.
  700. Returns a ``webob.Response`` object.
  701. """
  702. return self._gen_request('POST', url, params=params, headers=headers,
  703. extra_environ=extra_environ, status=status,
  704. upload_files=upload_files,
  705. expect_errors=expect_errors,
  706. content_type=content_type)
  707. def post_json(self, url, params='', headers=None, extra_environ=None,
  708. status=None, expect_errors=False):
  709. """
  710. Do a POST request. Very like the ``.get()`` method.
  711. ``params`` are dumps to json and put in the body of the request.
  712. Content-Type is set to ``application/json``.
  713. Returns a ``webob.Response`` object.
  714. """
  715. content_type = 'application/json'
  716. if params:
  717. params = dumps(params)
  718. return self._gen_request('POST', url, params=params, headers=headers,
  719. extra_environ=extra_environ, status=status,
  720. upload_files=None,
  721. expect_errors=expect_errors,
  722. content_type=content_type)
  723. def put(self, url, params='', headers=None, extra_environ=None,
  724. status=None, upload_files=None, expect_errors=False,
  725. content_type=None):
  726. """
  727. Do a PUT request. Very like the ``.post()`` method.
  728. ``params`` are put in the body of the request, if params is a
  729. tuple, dictionary, list, or iterator it will be urlencoded and
  730. placed in the body as with a POST, if it is string it will not
  731. be encoded, but placed in the body directly.
  732. Returns a ``webob.Response`` object.
  733. """
  734. return self._gen_request('PUT', url, params=params, headers=headers,
  735. extra_environ=extra_environ, status=status,
  736. upload_files=upload_files,
  737. expect_errors=expect_errors,
  738. content_type=content_type)
  739. def put_json(self, url, params='', headers=None, extra_environ=None,
  740. status=None, expect_errors=False):
  741. """
  742. Do a PUT request. Very like the ``.post()`` method.
  743. ``params`` are dumps to json and put in the body of the request.
  744. Content-Type is set to ``application/json``.
  745. Returns a ``webob.Response`` object.
  746. """
  747. content_type = 'application/json'
  748. if params:
  749. params = dumps(params)
  750. return self._gen_request('PUT', url, params=params, headers=headers,
  751. extra_environ=extra_environ, status=status,
  752. upload_files=None,
  753. expect_errors=expect_errors,
  754. content_type=content_type)
  755. def delete(self, url, params='', headers=None, extra_environ=None,
  756. status=None, expect_errors=False, content_type=None):
  757. """
  758. Do a DELETE request. Very like the ``.get()`` method.
  759. Returns a ``webob.Response`` object.
  760. """
  761. if params:
  762. warnings.warn(('You are not supposed to send a body in a '
  763. 'DELETE request. Most web servers will ignore it'),
  764. lint.WSGIWarning)
  765. return self._gen_request('DELETE', url, params=params, headers=headers,
  766. extra_environ=extra_environ, status=status,
  767. upload_files=None,
  768. expect_errors=expect_errors,
  769. content_type=content_type)
  770. def delete_json(self, url, params='', headers=None, extra_environ=None,
  771. status=None, expect_errors=False):
  772. """
  773. Do a DELETE request. Very like the ``.get()`` method.
  774. Content-Type is set to ``application/json``.
  775. Returns a ``webob.Response`` object.
  776. """
  777. if params:
  778. warnings.warn(('You are not supposed to send a body in a '
  779. 'DELETE request. Most web servers will ignore it'),
  780. lint.WSGIWarning)
  781. content_type = 'application/json'
  782. if params:
  783. params = dumps(params)
  784. return self._gen_request('DELETE', url, params=params, headers=headers,
  785. extra_environ=extra_environ, status=status,
  786. upload_files=None,
  787. expect_errors=expect_errors,
  788. content_type=content_type)
  789. def options(self, url, headers=None, extra_environ=None,
  790. status=None, expect_errors=False):
  791. """
  792. Do a OPTIONS request. Very like the ``.get()`` method.
  793. Returns a ``webob.Response`` object.
  794. """
  795. return self._gen_request('OPTIONS', url, headers=headers,
  796. extra_environ=extra_environ, status=status,
  797. upload_files=None,
  798. expect_errors=expect_errors)
  799. def head(self, url, headers=None, extra_environ=None,
  800. status=None, expect_errors=False):
  801. """
  802. Do a HEAD request. Very like the ``.get()`` method.
  803. Returns a ``webob.Response`` object.
  804. """
  805. return self._gen_request('HEAD', url, headers=headers,
  806. extra_environ=extra_environ, status=status,
  807. upload_files=None,
  808. expect_errors=expect_errors)
  809. def encode_multipart(self, params, files):
  810. """
  811. Encodes a set of parameters (typically a name/value list) and
  812. a set of files (a list of (name, filename, file_body)) into a
  813. typical POST body, returning the (content_type, body).
  814. """
  815. boundary = '----------a_BoUnDaRy%s$' % random.random()
  816. lines = []
  817. for key, value in params:
  818. lines.append('--' + boundary)
  819. lines.append('Content-Disposition: form-data; name="%s"' % key)
  820. lines.append('')
  821. lines.append(value)
  822. for file_info in files:
  823. key, filename, value = self._get_file_info(file_info)
  824. lines.append('--' + boundary)
  825. lines.append(
  826. 'Content-Disposition: form-data; name="%s"; filename="%s"'
  827. % (key, filename))
  828. fcontent = mimetypes.guess_type(filename)[0]
  829. lines.append('Content-Type: %s' %
  830. (fcontent or 'application/octet-stream'))
  831. lines.append('')
  832. lines.append(value)
  833. lines.append('--' + boundary + '--')
  834. lines.append('')
  835. body = join_bytes('\r\n', lines)
  836. content_type = 'multipart/form-data; boundary=%s' % boundary
  837. return content_type, body
  838. def _get_file_info(self, file_info):
  839. if len(file_info) == 2:
  840. # It only has a filename
  841. filename = file_info[1]
  842. if self.relative_to:
  843. filename = os.path.join(self.relative_to, filename)
  844. f = open(filename, 'rb')
  845. content = f.read()
  846. if PY3 and isinstance(content, text_type):
  847. # we want bytes
  848. content = content.encode(f.encoding)
  849. f.close()
  850. return (file_info[0], filename, content)
  851. elif len(file_info) == 3:
  852. content = file_info[2]
  853. if not isinstance(content, binary_type):
  854. raise ValueError('File content must be %s not %s'
  855. % (binary_type, type(content)))
  856. return file_info
  857. else:
  858. raise ValueError(
  859. "upload_files need to be a list of tuples of (fieldname, "
  860. "filename, filecontent) or (fieldname, filename); "
  861. "you gave: %r"
  862. % repr(file_info)[:100])
  863. def request(self, url_or_req, status=None, expect_errors=False,
  864. **req_params):
  865. """
  866. Creates and executes a request. You may either pass in an
  867. instantiated :class:`TestRequest` object, or you may pass in a
  868. URL and keyword arguments to be passed to
  869. :meth:`TestRequest.blank`.
  870. You can use this to run a request without the intermediary
  871. functioning of :meth:`TestApp.get` etc. For instance, to
  872. test a WebDAV method::
  873. resp = app.request('/new-col', method='MKCOL')
  874. Note that the request won't have a body unless you specify it,
  875. like::
  876. resp = app.request('/test.txt', method='PUT', body='test')
  877. You can use ``POST={args}`` to set the request body to the
  878. serialized arguments, and simultaneously set the request
  879. method to ``POST``
  880. """
  881. if isinstance(url_or_req, string_types):
  882. req = self.RequestClass.blank(url_or_req, **req_params)
  883. else:
  884. req = url_or_req.copy()
  885. for name, value in req_params.items():
  886. setattr(req, name, value)
  887. if req.content_length == -1:
  888. req.content_length = len(req.body)
  889. req.environ['paste.throw_errors'] = True
  890. for name, value in self.extra_environ.items():
  891. req.environ.setdefault(name, value)
  892. return self.do_request(req, status=status, expect_errors=expect_errors)
  893. def do_request(self, req, status, expect_errors):
  894. """
  895. Executes the given request (``req``), with the expected
  896. ``status``. Generally ``.get()`` and ``.post()`` are used
  897. instead.
  898. To use this::
  899. resp = app.do_request(webtest.TestRequest.blank(
  900. 'url', ...args...))
  901. Note you can pass any keyword arguments to
  902. ``TestRequest.blank()``, which will be set on the request.
  903. These can be arguments like ``content_type``, ``accept``, etc.
  904. """
  905. __tracebackhide__ = True
  906. errors = StringIO()
  907. req.environ['wsgi.errors'] = errors
  908. script_name = req.environ.get('SCRIPT_NAME', '')
  909. if script_name and req.path_info.startswith(script_name):
  910. req.path_info = req.path_info[len(script_name):]
  911. if self.cookies:
  912. cookie_header = ''.join([
  913. '%s=%s; ' % (name, cookie_quote(value))
  914. for name, value in self.cookies.items()])
  915. req.environ['HTTP_COOKIE'] = cookie_header
  916. req.environ['paste.testing'] = True
  917. req.environ['paste.testing_variables'] = {}
  918. app = lint.middleware(self.app)
  919. ## FIXME: should it be an option to not catch exc_info?
  920. res = req.get_response(app, catch_exc_info=True)
  921. res._use_unicode = self.use_unicode
  922. res.request = req
  923. res.app = app
  924. res.test_app = self
  925. # We do this to make sure the app_iter is exausted:
  926. try:
  927. res.body
  928. except TypeError:
  929. pass
  930. res.errors = errors.getvalue()
  931. for name, value in req.environ['paste.testing_variables'].items():
  932. if hasattr(res, name):
  933. raise ValueError(
  934. "paste.testing_variables contains the variable %r, but "
  935. "the response object already has an attribute by that "
  936. "name" % name)
  937. setattr(res, name, value)
  938. if not expect_errors:
  939. self._check_status(status, res)
  940. self._check_errors(res)
  941. res.cookies_set = {}
  942. for header in res.headers.getall('set-cookie'):
  943. try:
  944. c = SimpleCookie(header)
  945. except CookieError:
  946. raise CookieError(
  947. "Could not parse cookie header %r" % (header,))
  948. for key, morsel in c.items():
  949. self.cookies[key] = morsel.value
  950. res.cookies_set[key] = morsel.value
  951. return res
  952. def _check_status(self, status, res):
  953. __tracebackhide__ = True
  954. if status == '*':
  955. return
  956. res_status = to_string(res.status)
  957. if (isinstance(status, string_types)
  958. and '*' in status):
  959. if re.match(fnmatch.translate(status), res_status, re.I):
  960. return
  961. if isinstance(status, (list, tuple)):
  962. if res.status_int not in status:
  963. raise AppError(
  964. "Bad response: %s (not one of %s for %s)\n%s",
  965. res_status, ', '.join(map(str, status)),
  966. res.request.url, res)
  967. return
  968. if status is None:
  969. if res.status_int >= 200 and res.status_int < 400:
  970. return
  971. raise AppError(
  972. "Bad response: %s (not 200 OK or 3xx redirect for %s)\n%s",
  973. res_status, res.request.url,
  974. res)
  975. if status != res.status_int:
  976. raise AppError(
  977. "Bad response: %s (not %s)", res_status, status)
  978. def _check_errors(self, res):
  979. errors = res.errors
  980. if errors:
  981. raise AppError(
  982. "Application had errors logged:\n%s", errors)
  983. ########################################
  984. ## Form objects
  985. ########################################
  986. _attr_re = re.compile(
  987. (r'([^= \n\r\t]+)[ \n\r\t]*(?:=[ \n\r\t]*(?:"([^"]*)"|\'([^\']*)'
  988. r'\'|([^"\'][^ \n\r\t>]*)))?'), re.S)
  989. def _parse_attrs(text):
  990. attrs = {}
  991. for match in _attr_re.finditer(text):
  992. attr_name = match.group(1).lower()
  993. attr_body = match.group(2) or match.group(3)
  994. attr_body = html_unquote(attr_body or '')
  995. # python <= 2.5 doesn't like **dict when the keys are unicode
  996. # so cast str on them. Unicode field attributes are not
  997. # supported now (actually they have never been supported).
  998. attrs[str(attr_name)] = attr_body
  999. return attrs
  1000. class Field(object):
  1001. """
  1002. Field object.
  1003. """
  1004. # Dictionary of field types (select, radio, etc) to classes
  1005. classes = {}
  1006. settable = True
  1007. def __init__(self, form, tag, name, pos,
  1008. value=None, id=None, **attrs):
  1009. self.form = form
  1010. self.tag = tag
  1011. self.name = name
  1012. self.pos = pos
  1013. self._value = value
  1014. self.id = id
  1015. self.attrs = attrs
  1016. def value__set(self, value):
  1017. if not self.settable:
  1018. raise AttributeError(
  1019. "You cannot set the value of the <%s> field %r"
  1020. % (self.tag, self.name))
  1021. self._value = value
  1022. def force_value(self, value):
  1023. """
  1024. Like setting a value, except forces it even for, say, hidden
  1025. fields.
  1026. """
  1027. self._value = value
  1028. def value__get(self):
  1029. return self._value
  1030. value = property(value__get, value__set)
  1031. def __repr__(self):
  1032. value = '<%s name="%s"' % (self.__class__.__name__, self.name)
  1033. if self.id:
  1034. value += ' id="%s"' % self.id
  1035. return value + '>'
  1036. class NoValue(object):
  1037. pass
  1038. class Select(Field):
  1039. """
  1040. Field representing ``<select>``
  1041. """
  1042. def __init__(self, *args, **attrs):
  1043. super(Select, self).__init__(*args, **attrs)
  1044. self.options = []
  1045. # Undetermined yet:
  1046. self.selectedIndex = None
  1047. # we have no forced value
  1048. self._forced_value = NoValue
  1049. def force_value(self, value):
  1050. self._forced_value = value
  1051. def value__set(self, value):
  1052. if self._forced_value is not NoValue:
  1053. self._forced_value = NoValue
  1054. for i, (option, checked) in enumerate(self.options):
  1055. if option == _stringify(value):
  1056. self.selectedIndex = i
  1057. break
  1058. else:
  1059. raise ValueError(
  1060. "Option %r not found (from %s)"
  1061. % (value, ', '.join(
  1062. [repr(o) for o, c in self.options])))
  1063. def value__get(self):
  1064. if self._forced_value is not NoValue:
  1065. return self._forced_value
  1066. elif self.selectedIndex is not None:
  1067. return self.options[self.selectedIndex][0]
  1068. else:
  1069. for option, checked in self.options:
  1070. if checked:
  1071. return option
  1072. else:
  1073. if self.options:
  1074. return self.options[0][0]
  1075. else:
  1076. return None
  1077. value = property(value__get, value__set)
  1078. Field.classes['select'] = Select
  1079. class MultipleSelect(Field):
  1080. """
  1081. Field representing ``<select multiple="multiple">``
  1082. """
  1083. def __init__(self, *args, **attrs):
  1084. super(MultipleSelect, self).__init__(*args, **attrs)
  1085. self.options = []
  1086. # Undetermined yet:
  1087. self.selectedIndices = []
  1088. self._forced_values = []
  1089. def force_value(self, values):
  1090. self._forced_values = values
  1091. self.selectedIndices = []
  1092. def value__set(self, values):
  1093. str_values = [_stringify(value) for value in values]
  1094. self.selectedIndicies = []
  1095. for i, (option, checked) in enumerate(self.options):
  1096. if option in str_values:
  1097. self.selectedIndices.append(i)
  1098. str_values.remove(option)
  1099. if str_values:
  1100. raise ValueError(
  1101. "Option(s) %r not found (from %s)"
  1102. % (', '.join(str_values),
  1103. ', '.join(
  1104. [repr(o) for o, c in self.options])))
  1105. def value__get(self):
  1106. selected_values = []
  1107. if self.selectedIndices:
  1108. selected_values = [self.options[i][0] \
  1109. for i in self.selectedIndices]
  1110. elif not self._forced_values:
  1111. selected_values = []
  1112. for option, checked in self.options:
  1113. if checked:
  1114. selected_values.append(option)
  1115. if self._forced_values:
  1116. selected_values += self._forced_values
  1117. if self.options and (not selected_values):
  1118. selected_values = None
  1119. return selected_values
  1120. value = property(value__get, value__set)
  1121. Field.classes['multiple_select'] = MultipleSelect
  1122. class Radio(Select):
  1123. """
  1124. Field representing ``<input type="radio">``
  1125. """
  1126. def value__get(self):
  1127. if self.selectedIndex is not None:
  1128. return self.options[self.selectedIndex][0]
  1129. else:
  1130. for option, checked in self.options:
  1131. if checked:
  1132. return option
  1133. else:
  1134. return None
  1135. value = property(value__get, Select.value__set)
  1136. Field.classes['radio'] = Radio
  1137. class Checkbox(Field):
  1138. """
  1139. Field representing ``<input type="checkbox">``
  1140. """
  1141. def __init__(self, *args, **attrs):
  1142. super(Checkbox, self).__init__(*args, **attrs)
  1143. self.checked = 'checked' in attrs
  1144. def value__set(self, value):
  1145. self.checked = not not value
  1146. def value__get(self):
  1147. if self.checked:
  1148. if self._value is None:
  1149. return 'on'
  1150. else:
  1151. return self._value
  1152. else:
  1153. return None
  1154. value = property(value__get, value__set)
  1155. Field.classes['checkbox'] = Checkbox
  1156. class Text(Field):
  1157. """
  1158. Field representing ``<input type="text">``
  1159. """
  1160. def value__get(self):
  1161. if self._value is None:
  1162. return ''
  1163. else:
  1164. return self._value
  1165. value = property(value__get, Field.value__set)
  1166. Field.classes['text'] = Text
  1167. class File(Field):
  1168. """
  1169. Field representing ``<input type="file">``
  1170. """
  1171. ## FIXME: This doesn't actually handle file uploads and enctype
  1172. def value__get(self):
  1173. if self._value is None:
  1174. return ''
  1175. else:
  1176. return self._value
  1177. value = property(value__get, Field.value__set)
  1178. Field.classes['file'] = File
  1179. class Textarea(Text):
  1180. """
  1181. Field representing ``<textarea>``
  1182. """
  1183. Field.classes['textarea'] = Textarea
  1184. class Hidden(Text):
  1185. """
  1186. Field representing ``<input type="hidden">``
  1187. """
  1188. Field.classes['hidden'] = Hidden
  1189. class Submit(Field):
  1190. """
  1191. Field representing ``<input type="submit">`` and ``<button>``
  1192. """
  1193. settable = False
  1194. def value__get(self):
  1195. return None
  1196. value = property(value__get)
  1197. def value_if_submitted(self):
  1198. return self._value
  1199. Field.classes['submit'] = Submit
  1200. Field.classes['button'] = Submit
  1201. Field.classes['image'] = Submit
  1202. class Form(object):
  1203. """
  1204. This object represents a form that has been found in a page.
  1205. This has a couple useful attributes:
  1206. ``text``:
  1207. the full HTML of the form.
  1208. ``action``:
  1209. the relative URI of the action.
  1210. ``method``:
  1211. the method (e.g., ``'GET'``).
  1212. ``id``:
  1213. the id, or None if not given.
  1214. ``fields``:
  1215. a dictionary of fields, each value is a list of fields by
  1216. that name. ``<input type=\"radio\">`` and ``<select>`` are
  1217. both represented as single fields with multiple options.
  1218. """
  1219. # @@: This really should be using Mechanize/ClientForm or
  1220. # something...
  1221. _tag_re = re.compile(r'<(/?)([a-z0-9_\-]*)([^>]*?)>', re.I)
  1222. _label_re = re.compile(
  1223. '''<label\s+(?:[^>]*)for=(?:"|')([a-z0-9_\-]+)(?:"|')(?:[^>]*)>''',
  1224. re.I)
  1225. FieldClass = Field
  1226. def __init__(self, response, text):
  1227. self.response = response
  1228. self.text = text
  1229. self._parse_fields()
  1230. self._parse_action()
  1231. def _parse_fields(self):
  1232. in_select = None
  1233. in_textarea = None
  1234. fields = OrderedDict()
  1235. for match in self._tag_re.finditer(self.text):
  1236. end = match.group(1) == '/'
  1237. tag = match.group(2).lower()
  1238. if tag not in ('input', 'select…

Large files files are truncated, but you can click here to view the full file