source: publico/il.spdo/trunk/Paste-1.7.5.1-py2.6.egg/paste/fixture.py @ 5327

Última Alteração nesse arquivo desde 5327 foi 5327, incluída por fabianosantos, 8 anos atrás

Import inicial.

File size: 56.7 KB
Linha 
1# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
2# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
3"""
4Routines for testing WSGI applications.
5
6Most interesting is the `TestApp <class-paste.fixture.TestApp.html>`_
7for testing WSGI applications, and the `TestFileEnvironment
8<class-paste.fixture.TestFileEnvironment.html>`_ class for testing the
9effects of command-line scripts.
10"""
11
12import sys
13import random
14import urllib
15import urlparse
16import mimetypes
17import time
18import cgi
19import os
20import shutil
21import smtplib
22import shlex
23from Cookie import BaseCookie
24try:
25    from cStringIO import StringIO
26except ImportError:
27    from StringIO import StringIO
28import re
29try:
30    import subprocess
31except ImportError:
32    from paste.util import subprocess24 as subprocess
33
34from paste import wsgilib
35from paste import lint
36from paste.response import HeaderDict
37
38def tempnam_no_warning(*args):
39    """
40    An os.tempnam with the warning turned off, because sometimes
41    you just need to use this and don't care about the stupid
42    security warning.
43    """
44    return os.tempnam(*args)
45
46class NoDefault(object):
47    pass
48
49def sorted(l):
50    l = list(l)
51    l.sort()
52    return l
53
54class Dummy_smtplib(object):
55
56    existing = None
57
58    def __init__(self, server):
59        import warnings
60        warnings.warn(
61            'Dummy_smtplib is not maintained and is deprecated',
62            DeprecationWarning, 2)
63        assert not self.existing, (
64            "smtplib.SMTP() called again before Dummy_smtplib.existing.reset() "
65            "called.")
66        self.server = server
67        self.open = True
68        self.__class__.existing = self
69
70    def quit(self):
71        assert self.open, (
72            "Called %s.quit() twice" % self)
73        self.open = False
74
75    def sendmail(self, from_address, to_addresses, msg):
76        self.from_address = from_address
77        self.to_addresses = to_addresses
78        self.message = msg
79
80    def install(cls):
81        smtplib.SMTP = cls
82
83    install = classmethod(install)
84
85    def reset(self):
86        assert not self.open, (
87            "SMTP connection not quit")
88        self.__class__.existing = None
89
90class AppError(Exception):
91    pass
92
93class TestApp(object):
94
95    # for py.test
96    disabled = True
97
98    def __init__(self, app, namespace=None, relative_to=None,
99                 extra_environ=None, pre_request_hook=None,
100                 post_request_hook=None):
101        """
102        Wraps a WSGI application in a more convenient interface for
103        testing.
104
105        ``app`` may be an application, or a Paste Deploy app
106        URI, like ``'config:filename.ini#test'``.
107
108        ``namespace`` is a dictionary that will be written to (if
109        provided).  This can be used with doctest or some other
110        system, and the variable ``res`` will be assigned everytime
111        you make a request (instead of returning the request).
112
113        ``relative_to`` is a directory, and filenames used for file
114        uploads are calculated relative to this.  Also ``config:``
115        URIs that aren't absolute.
116
117        ``extra_environ`` is a dictionary of values that should go
118        into the environment for each request.  These can provide a
119        communication channel with the application.
120
121        ``pre_request_hook`` is a function to be called prior to
122        making requests (such as ``post`` or ``get``). This function
123        must take one argument (the instance of the TestApp).
124
125        ``post_request_hook`` is a function, similar to
126        ``pre_request_hook``, to be called after requests are made.
127        """
128        if isinstance(app, (str, unicode)):
129            from paste.deploy import loadapp
130            # @@: Should pick up relative_to from calling module's
131            # __file__
132            app = loadapp(app, relative_to=relative_to)
133        self.app = app
134        self.namespace = namespace
135        self.relative_to = relative_to
136        if extra_environ is None:
137            extra_environ = {}
138        self.extra_environ = extra_environ
139        self.pre_request_hook = pre_request_hook
140        self.post_request_hook = post_request_hook
141        self.reset()
142
143    def reset(self):
144        """
145        Resets the state of the application; currently just clears
146        saved cookies.
147        """
148        self.cookies = {}
149
150    def _make_environ(self):
151        environ = self.extra_environ.copy()
152        environ['paste.throw_errors'] = True
153        return environ
154
155    def get(self, url, params=None, headers=None, extra_environ=None,
156            status=None, expect_errors=False):
157        """
158        Get the given url (well, actually a path like
159        ``'/page.html'``).
160
161        ``params``:
162            A query string, or a dictionary that will be encoded
163            into a query string.  You may also include a query
164            string on the ``url``.
165
166        ``headers``:
167            A dictionary of extra headers to send.
168
169        ``extra_environ``:
170            A dictionary of environmental variables that should
171            be added to the request.
172
173        ``status``:
174            The integer status code you expect (if not 200 or 3xx).
175            If you expect a 404 response, for instance, you must give
176            ``status=404`` or it will be an error.  You can also give
177            a wildcard, like ``'3*'`` or ``'*'``.
178
179        ``expect_errors``:
180            If this is not true, then if anything is written to
181            ``wsgi.errors`` it will be an error.  If it is true, then
182            non-200/3xx responses are also okay.
183
184        Returns a `response object
185        <class-paste.fixture.TestResponse.html>`_
186        """
187        if extra_environ is None:
188            extra_environ = {}
189        # Hide from py.test:
190        __tracebackhide__ = True
191        if params:
192            if not isinstance(params, (str, unicode)):
193                params = urllib.urlencode(params, doseq=True)
194            if '?' in url:
195                url += '&'
196            else:
197                url += '?'
198            url += params
199        environ = self._make_environ()
200        url = str(url)
201        if '?' in url:
202            url, environ['QUERY_STRING'] = url.split('?', 1)
203        else:
204            environ['QUERY_STRING'] = ''
205        self._set_headers(headers, environ)
206        environ.update(extra_environ)
207        req = TestRequest(url, environ, expect_errors)
208        return self.do_request(req, status=status)
209
210    def _gen_request(self, method, url, params='', headers=None, extra_environ=None,
211             status=None, upload_files=None, expect_errors=False):
212        """
213        Do a generic request.
214        """
215        if headers is None:
216            headers = {}
217        if extra_environ is None:
218            extra_environ = {}
219        environ = self._make_environ()
220        # @@: Should this be all non-strings?
221        if isinstance(params, (list, tuple, dict)):
222            params = urllib.urlencode(params)
223        if hasattr(params, 'items'):
224            # Some other multi-dict like format
225            params = urllib.urlencode(params.items())
226        if upload_files:
227            params = cgi.parse_qsl(params, keep_blank_values=True)
228            content_type, params = self.encode_multipart(
229                params, upload_files)
230            environ['CONTENT_TYPE'] = content_type
231        elif params:
232            environ.setdefault('CONTENT_TYPE', 'application/x-www-form-urlencoded')
233        if '?' in url:
234            url, environ['QUERY_STRING'] = url.split('?', 1)
235        else:
236            environ['QUERY_STRING'] = ''
237        environ['CONTENT_LENGTH'] = str(len(params))
238        environ['REQUEST_METHOD'] = method
239        environ['wsgi.input'] = StringIO(params)
240        self._set_headers(headers, environ)
241        environ.update(extra_environ)
242        req = TestRequest(url, environ, expect_errors)
243        return self.do_request(req, status=status)
244
245    def post(self, url, params='', headers=None, extra_environ=None,
246             status=None, upload_files=None, expect_errors=False):
247        """
248        Do a POST request.  Very like the ``.get()`` method.
249        ``params`` are put in the body of the request.
250
251        ``upload_files`` is for file uploads.  It should be a list of
252        ``[(fieldname, filename, file_content)]``.  You can also use
253        just ``[(fieldname, filename)]`` and the file content will be
254        read from disk.
255
256        Returns a `response object
257        <class-paste.fixture.TestResponse.html>`_
258        """
259        return self._gen_request('POST', url, params=params, headers=headers,
260                                 extra_environ=extra_environ,status=status,
261                                 upload_files=upload_files,
262                                 expect_errors=expect_errors)
263
264    def put(self, url, params='', headers=None, extra_environ=None,
265             status=None, upload_files=None, expect_errors=False):
266        """
267        Do a PUT request.  Very like the ``.get()`` method.
268        ``params`` are put in the body of the request.
269
270        ``upload_files`` is for file uploads.  It should be a list of
271        ``[(fieldname, filename, file_content)]``.  You can also use
272        just ``[(fieldname, filename)]`` and the file content will be
273        read from disk.
274
275        Returns a `response object
276        <class-paste.fixture.TestResponse.html>`_
277        """
278        return self._gen_request('PUT', url, params=params, headers=headers,
279                                 extra_environ=extra_environ,status=status,
280                                 upload_files=upload_files,
281                                 expect_errors=expect_errors)
282
283    def delete(self, url, params='', headers=None, extra_environ=None,
284               status=None, expect_errors=False):
285        """
286        Do a DELETE request.  Very like the ``.get()`` method.
287        ``params`` are put in the body of the request.
288
289        Returns a `response object
290        <class-paste.fixture.TestResponse.html>`_
291        """
292        return self._gen_request('DELETE', url, params=params, headers=headers,
293                                 extra_environ=extra_environ,status=status,
294                                 upload_files=None, expect_errors=expect_errors)
295
296
297
298
299    def _set_headers(self, headers, environ):
300        """
301        Turn any headers into environ variables
302        """
303        if not headers:
304            return
305        for header, value in headers.items():
306            if header.lower() == 'content-type':
307                var = 'CONTENT_TYPE'
308            elif header.lower() == 'content-length':
309                var = 'CONTENT_LENGTH'
310            else:
311                var = 'HTTP_%s' % header.replace('-', '_').upper()
312            environ[var] = value
313
314    def encode_multipart(self, params, files):
315        """
316        Encodes a set of parameters (typically a name/value list) and
317        a set of files (a list of (name, filename, file_body)) into a
318        typical POST body, returning the (content_type, body).
319        """
320        boundary = '----------a_BoUnDaRy%s$' % random.random()
321        lines = []
322        for key, value in params:
323            lines.append('--'+boundary)
324            lines.append('Content-Disposition: form-data; name="%s"' % key)
325            lines.append('')
326            lines.append(value)
327        for file_info in files:
328            key, filename, value = self._get_file_info(file_info)
329            lines.append('--'+boundary)
330            lines.append('Content-Disposition: form-data; name="%s"; filename="%s"'
331                         % (key, filename))
332            fcontent = mimetypes.guess_type(filename)[0]
333            lines.append('Content-Type: %s' %
334                         fcontent or 'application/octet-stream')
335            lines.append('')
336            lines.append(value)
337        lines.append('--' + boundary + '--')
338        lines.append('')
339        body = '\r\n'.join(lines)
340        content_type = 'multipart/form-data; boundary=%s' % boundary
341        return content_type, body
342
343    def _get_file_info(self, file_info):
344        if len(file_info) == 2:
345            # It only has a filename
346            filename = file_info[1]
347            if self.relative_to:
348                filename = os.path.join(self.relative_to, filename)
349            f = open(filename, 'rb')
350            content = f.read()
351            f.close()
352            return (file_info[0], filename, content)
353        elif len(file_info) == 3:
354            return file_info
355        else:
356            raise ValueError(
357                "upload_files need to be a list of tuples of (fieldname, "
358                "filename, filecontent) or (fieldname, filename); "
359                "you gave: %r"
360                % repr(file_info)[:100])
361
362    def do_request(self, req, status):
363        """
364        Executes the given request (``req``), with the expected
365        ``status``.  Generally ``.get()`` and ``.post()`` are used
366        instead.
367        """
368        if self.pre_request_hook:
369            self.pre_request_hook(self)
370        __tracebackhide__ = True
371        if self.cookies:
372            c = BaseCookie()
373            for name, value in self.cookies.items():
374                c[name] = value
375            hc = '; '.join(['='.join([m.key, m.value]) for m in c.values()])
376            req.environ['HTTP_COOKIE'] = hc
377        req.environ['paste.testing'] = True
378        req.environ['paste.testing_variables'] = {}
379        app = lint.middleware(self.app)
380        old_stdout = sys.stdout
381        out = CaptureStdout(old_stdout)
382        try:
383            sys.stdout = out
384            start_time = time.time()
385            raise_on_wsgi_error = not req.expect_errors
386            raw_res = wsgilib.raw_interactive(
387                app, req.url,
388                raise_on_wsgi_error=raise_on_wsgi_error,
389                **req.environ)
390            end_time = time.time()
391        finally:
392            sys.stdout = old_stdout
393            sys.stderr.write(out.getvalue())
394        res = self._make_response(raw_res, end_time - start_time)
395        res.request = req
396        for name, value in req.environ['paste.testing_variables'].items():
397            if hasattr(res, name):
398                raise ValueError(
399                    "paste.testing_variables contains the variable %r, but "
400                    "the response object already has an attribute by that "
401                    "name" % name)
402            setattr(res, name, value)
403        if self.namespace is not None:
404            self.namespace['res'] = res
405        if not req.expect_errors:
406            self._check_status(status, res)
407            self._check_errors(res)
408        res.cookies_set = {}
409        for header in res.all_headers('set-cookie'):
410            c = BaseCookie(header)
411            for key, morsel in c.items():
412                self.cookies[key] = morsel.value
413                res.cookies_set[key] = morsel.value
414        if self.post_request_hook:
415            self.post_request_hook(self)
416        if self.namespace is None:
417            # It's annoying to return the response in doctests, as it'll
418            # be printed, so we only return it is we couldn't assign
419            # it anywhere
420            return res
421
422    def _check_status(self, status, res):
423        __tracebackhide__ = True
424        if status == '*':
425            return
426        if isinstance(status, (list, tuple)):
427            if res.status not in status:
428                raise AppError(
429                    "Bad response: %s (not one of %s for %s)\n%s"
430                    % (res.full_status, ', '.join(map(str, status)),
431                       res.request.url, res.body))
432            return
433        if status is None:
434            if res.status >= 200 and res.status < 400:
435                return
436            raise AppError(
437                "Bad response: %s (not 200 OK or 3xx redirect for %s)\n%s"
438                % (res.full_status, res.request.url,
439                   res.body))
440        if status != res.status:
441            raise AppError(
442                "Bad response: %s (not %s)" % (res.full_status, status))
443
444    def _check_errors(self, res):
445        if res.errors:
446            raise AppError(
447                "Application had errors logged:\n%s" % res.errors)
448
449    def _make_response(self, (status, headers, body, errors), total_time):
450        return TestResponse(self, status, headers, body, errors,
451                            total_time)
452
453class CaptureStdout(object):
454
455    def __init__(self, actual):
456        self.captured = StringIO()
457        self.actual = actual
458
459    def write(self, s):
460        self.captured.write(s)
461        self.actual.write(s)
462
463    def flush(self):
464        self.actual.flush()
465
466    def writelines(self, lines):
467        for item in lines:
468            self.write(item)
469
470    def getvalue(self):
471        return self.captured.getvalue()
472
473class TestResponse(object):
474
475    # for py.test
476    disabled = True
477
478    """
479    Instances of this class are return by `TestApp
480    <class-paste.fixture.TestApp.html>`_
481    """
482
483    def __init__(self, test_app, status, headers, body, errors,
484                 total_time):
485        self.test_app = test_app
486        self.status = int(status.split()[0])
487        self.full_status = status
488        self.headers = headers
489        self.header_dict = HeaderDict.fromlist(self.headers)
490        self.body = body
491        self.errors = errors
492        self._normal_body = None
493        self.time = total_time
494        self._forms_indexed = None
495
496    def forms__get(self):
497        """
498        Returns a dictionary of ``Form`` objects.  Indexes are both in
499        order (from zero) and by form id (if the form is given an id).
500        """
501        if self._forms_indexed is None:
502            self._parse_forms()
503        return self._forms_indexed
504
505    forms = property(forms__get,
506                     doc="""
507                     A list of <form>s found on the page (instances of
508                     `Form <class-paste.fixture.Form.html>`_)
509                     """)
510
511    def form__get(self):
512        forms = self.forms
513        if not forms:
514            raise TypeError(
515                "You used response.form, but no forms exist")
516        if 1 in forms:
517            # There is more than one form
518            raise TypeError(
519                "You used response.form, but more than one form exists")
520        return forms[0]
521
522    form = property(form__get,
523                    doc="""
524                    Returns a single `Form
525                    <class-paste.fixture.Form.html>`_ instance; it
526                    is an error if there are multiple forms on the
527                    page.
528                    """)
529
530    _tag_re = re.compile(r'<(/?)([:a-z0-9_\-]*)(.*?)>', re.S|re.I)
531
532    def _parse_forms(self):
533        forms = self._forms_indexed = {}
534        form_texts = []
535        started = None
536        for match in self._tag_re.finditer(self.body):
537            end = match.group(1) == '/'
538            tag = match.group(2).lower()
539            if tag != 'form':
540                continue
541            if end:
542                assert started, (
543                    "</form> unexpected at %s" % match.start())
544                form_texts.append(self.body[started:match.end()])
545                started = None
546            else:
547                assert not started, (
548                    "Nested form tags at %s" % match.start())
549                started = match.start()
550        assert not started, (
551            "Danging form: %r" % self.body[started:])
552        for i, text in enumerate(form_texts):
553            form = Form(self, text)
554            forms[i] = form
555            if form.id:
556                forms[form.id] = form
557
558    def header(self, name, default=NoDefault):
559        """
560        Returns the named header; an error if there is not exactly one
561        matching header (unless you give a default -- always an error
562        if there is more than one header)
563        """
564        found = None
565        for cur_name, value in self.headers:
566            if cur_name.lower() == name.lower():
567                assert not found, (
568                    "Ambiguous header: %s matches %r and %r"
569                    % (name, found, value))
570                found = value
571        if found is None:
572            if default is NoDefault:
573                raise KeyError(
574                    "No header found: %r (from %s)"
575                    % (name, ', '.join([n for n, v in self.headers])))
576            else:
577                return default
578        return found
579
580    def all_headers(self, name):
581        """
582        Gets all headers by the ``name``, returns as a list
583        """
584        found = []
585        for cur_name, value in self.headers:
586            if cur_name.lower() == name.lower():
587                found.append(value)
588        return found
589
590    def follow(self, **kw):
591        """
592        If this request is a redirect, follow that redirect.  It
593        is an error if this is not a redirect response.  Returns
594        another response object.
595        """
596        assert self.status >= 300 and self.status < 400, (
597            "You can only follow redirect responses (not %s)"
598            % self.full_status)
599        location = self.header('location')
600        type, rest = urllib.splittype(location)
601        host, path = urllib.splithost(rest)
602        # @@: We should test that it's not a remote redirect
603        return self.test_app.get(location, **kw)
604
605    def click(self, description=None, linkid=None, href=None,
606              anchor=None, index=None, verbose=False):
607        """
608        Click the link as described.  Each of ``description``,
609        ``linkid``, and ``url`` are *patterns*, meaning that they are
610        either strings (regular expressions), compiled regular
611        expressions (objects with a ``search`` method), or callables
612        returning true or false.
613
614        All the given patterns are ANDed together:
615
616        * ``description`` is a pattern that matches the contents of the
617          anchor (HTML and all -- everything between ``<a...>`` and
618          ``</a>``)
619
620        * ``linkid`` is a pattern that matches the ``id`` attribute of
621          the anchor.  It will receive the empty string if no id is
622          given.
623
624        * ``href`` is a pattern that matches the ``href`` of the anchor;
625          the literal content of that attribute, not the fully qualified
626          attribute.
627
628        * ``anchor`` is a pattern that matches the entire anchor, with
629          its contents.
630
631        If more than one link matches, then the ``index`` link is
632        followed.  If ``index`` is not given and more than one link
633        matches, or if no link matches, then ``IndexError`` will be
634        raised.
635
636        If you give ``verbose`` then messages will be printed about
637        each link, and why it does or doesn't match.  If you use
638        ``app.click(verbose=True)`` you'll see a list of all the
639        links.
640
641        You can use multiple criteria to essentially assert multiple
642        aspects about the link, e.g., where the link's destination is.
643        """
644        __tracebackhide__ = True
645        found_html, found_desc, found_attrs = self._find_element(
646            tag='a', href_attr='href',
647            href_extract=None,
648            content=description,
649            id=linkid,
650            href_pattern=href,
651            html_pattern=anchor,
652            index=index, verbose=verbose)
653        return self.goto(found_attrs['uri'])
654
655    def clickbutton(self, description=None, buttonid=None, href=None,
656                    button=None, index=None, verbose=False):
657        """
658        Like ``.click()``, except looks for link-like buttons.
659        This kind of button should look like
660        ``<button onclick="...location.href='url'...">``.
661        """
662        __tracebackhide__ = True
663        found_html, found_desc, found_attrs = self._find_element(
664            tag='button', href_attr='onclick',
665            href_extract=re.compile(r"location\.href='(.*?)'"),
666            content=description,
667            id=buttonid,
668            href_pattern=href,
669            html_pattern=button,
670            index=index, verbose=verbose)
671        return self.goto(found_attrs['uri'])
672
673    def _find_element(self, tag, href_attr, href_extract,
674                      content, id,
675                      href_pattern,
676                      html_pattern,
677                      index, verbose):
678        content_pat = _make_pattern(content)
679        id_pat = _make_pattern(id)
680        href_pat = _make_pattern(href_pattern)
681        html_pat = _make_pattern(html_pattern)
682
683        _tag_re = re.compile(r'<%s\s+(.*?)>(.*?)</%s>' % (tag, tag),
684                             re.I+re.S)
685
686        def printlog(s):
687            if verbose:
688                print s
689
690        found_links = []
691        total_links = 0
692        for match in _tag_re.finditer(self.body):
693            el_html = match.group(0)
694            el_attr = match.group(1)
695            el_content = match.group(2)
696            attrs = _parse_attrs(el_attr)
697            if verbose:
698                printlog('Element: %r' % el_html)
699            if not attrs.get(href_attr):
700                printlog('  Skipped: no %s attribute' % href_attr)
701                continue
702            el_href = attrs[href_attr]
703            if href_extract:
704                m = href_extract.search(el_href)
705                if not m:
706                    printlog("  Skipped: doesn't match extract pattern")
707                    continue
708                el_href = m.group(1)
709            attrs['uri'] = el_href
710            if el_href.startswith('#'):
711                printlog('  Skipped: only internal fragment href')
712                continue
713            if el_href.startswith('javascript:'):
714                printlog('  Skipped: cannot follow javascript:')
715                continue
716            total_links += 1
717            if content_pat and not content_pat(el_content):
718                printlog("  Skipped: doesn't match description")
719                continue
720            if id_pat and not id_pat(attrs.get('id', '')):
721                printlog("  Skipped: doesn't match id")
722                continue
723            if href_pat and not href_pat(el_href):
724                printlog("  Skipped: doesn't match href")
725                continue
726            if html_pat and not html_pat(el_html):
727                printlog("  Skipped: doesn't match html")
728                continue
729            printlog("  Accepted")
730            found_links.append((el_html, el_content, attrs))
731        if not found_links:
732            raise IndexError(
733                "No matching elements found (from %s possible)"
734                % total_links)
735        if index is None:
736            if len(found_links) > 1:
737                raise IndexError(
738                    "Multiple links match: %s"
739                    % ', '.join([repr(anc) for anc, d, attr in found_links]))
740            found_link = found_links[0]
741        else:
742            try:
743                found_link = found_links[index]
744            except IndexError:
745                raise IndexError(
746                    "Only %s (out of %s) links match; index %s out of range"
747                    % (len(found_links), total_links, index))
748        return found_link
749
750    def goto(self, href, method='get', **args):
751        """
752        Go to the (potentially relative) link ``href``, using the
753        given method (``'get'`` or ``'post'``) and any extra arguments
754        you want to pass to the ``app.get()`` or ``app.post()``
755        methods.
756
757        All hostnames and schemes will be ignored.
758        """
759        scheme, host, path, query, fragment = urlparse.urlsplit(href)
760        # We
761        scheme = host = fragment = ''
762        href = urlparse.urlunsplit((scheme, host, path, query, fragment))
763        href = urlparse.urljoin(self.request.full_url, href)
764        method = method.lower()
765        assert method in ('get', 'post'), (
766            'Only "get" or "post" are allowed for method (you gave %r)'
767            % method)
768        if method == 'get':
769            method = self.test_app.get
770        else:
771            method = self.test_app.post
772        return method(href, **args)
773
774    _normal_body_regex = re.compile(r'[ \n\r\t]+')
775
776    def normal_body__get(self):
777        if self._normal_body is None:
778            self._normal_body = self._normal_body_regex.sub(
779                ' ', self.body)
780        return self._normal_body
781
782    normal_body = property(normal_body__get,
783                           doc="""
784                           Return the whitespace-normalized body
785                           """)
786
787    def __contains__(self, s):
788        """
789        A response 'contains' a string if it is present in the body
790        of the response.  Whitespace is normalized when searching
791        for a string.
792        """
793        if not isinstance(s, (str, unicode)):
794            s = str(s)
795        if isinstance(s, unicode):
796            ## FIXME: we don't know that this response uses utf8:
797            s = s.encode('utf8')
798        return (self.body.find(s) != -1
799                or self.normal_body.find(s) != -1)
800
801    def mustcontain(self, *strings, **kw):
802        """
803        Assert that the response contains all of the strings passed
804        in as arguments.
805
806        Equivalent to::
807
808            assert string in res
809        """
810        if 'no' in kw:
811            no = kw['no']
812            del kw['no']
813            if isinstance(no, basestring):
814                no = [no]
815        else:
816            no = []
817        if kw:
818            raise TypeError(
819                "The only keyword argument allowed is 'no'")
820        for s in strings:
821            if not s in self:
822                print >> sys.stderr, "Actual response (no %r):" % s
823                print >> sys.stderr, self
824                raise IndexError(
825                    "Body does not contain string %r" % s)
826        for no_s in no:
827            if no_s in self:
828                print >> sys.stderr, "Actual response (has %r)" % no_s
829                print >> sys.stderr, self
830                raise IndexError(
831                    "Body contains string %r" % s)
832
833    def __repr__(self):
834        return '<Response %s %r>' % (self.full_status, self.body[:20])
835
836    def __str__(self):
837        simple_body = '\n'.join([l for l in self.body.splitlines()
838                                 if l.strip()])
839        return 'Response: %s\n%s\n%s' % (
840            self.status,
841            '\n'.join(['%s: %s' % (n, v) for n, v in self.headers]),
842            simple_body)
843
844    def showbrowser(self):
845        """
846        Show this response in a browser window (for debugging purposes,
847        when it's hard to read the HTML).
848        """
849        import webbrowser
850        fn = tempnam_no_warning(None, 'paste-fixture') + '.html'
851        f = open(fn, 'wb')
852        f.write(self.body)
853        f.close()
854        url = 'file:' + fn.replace(os.sep, '/')
855        webbrowser.open_new(url)
856
857class TestRequest(object):
858
859    # for py.test
860    disabled = True
861
862    """
863    Instances of this class are created by `TestApp
864    <class-paste.fixture.TestApp.html>`_ with the ``.get()`` and
865    ``.post()`` methods, and are consumed there by ``.do_request()``.
866
867    Instances are also available as a ``.req`` attribute on
868    `TestResponse <class-paste.fixture.TestResponse.html>`_ instances.
869
870    Useful attributes:
871
872    ``url``:
873        The url (actually usually the path) of the request, without
874        query string.
875
876    ``environ``:
877        The environment dictionary used for the request.
878
879    ``full_url``:
880        The url/path, with query string.
881    """
882
883    def __init__(self, url, environ, expect_errors=False):
884        if url.startswith('http://localhost'):
885            url = url[len('http://localhost'):]
886        self.url = url
887        self.environ = environ
888        if environ.get('QUERY_STRING'):
889            self.full_url = url + '?' + environ['QUERY_STRING']
890        else:
891            self.full_url = url
892        self.expect_errors = expect_errors
893
894
895class Form(object):
896
897    """
898    This object represents a form that has been found in a page.
899    This has a couple useful attributes:
900
901    ``text``:
902        the full HTML of the form.
903
904    ``action``:
905        the relative URI of the action.
906
907    ``method``:
908        the method (e.g., ``'GET'``).
909
910    ``id``:
911        the id, or None if not given.
912
913    ``fields``:
914        a dictionary of fields, each value is a list of fields by
915        that name.  ``<input type=\"radio\">`` and ``<select>`` are
916        both represented as single fields with multiple options.
917    """
918
919    # @@: This really should be using Mechanize/ClientForm or
920    # something...
921
922    _tag_re = re.compile(r'<(/?)([:a-z0-9_\-]*)([^>]*?)>', re.I)
923
924    def __init__(self, response, text):
925        self.response = response
926        self.text = text
927        self._parse_fields()
928        self._parse_action()
929
930    def _parse_fields(self):
931        in_select = None
932        in_textarea = None
933        fields = {}
934        for match in self._tag_re.finditer(self.text):
935            end = match.group(1) == '/'
936            tag = match.group(2).lower()
937            if tag not in ('input', 'select', 'option', 'textarea',
938                           'button'):
939                continue
940            if tag == 'select' and end:
941                assert in_select, (
942                    '%r without starting select' % match.group(0))
943                in_select = None
944                continue
945            if tag == 'textarea' and end:
946                assert in_textarea, (
947                    "</textarea> with no <textarea> at %s" % match.start())
948                in_textarea[0].value = html_unquote(self.text[in_textarea[1]:match.start()])
949                in_textarea = None
950                continue
951            if end:
952                continue
953            attrs = _parse_attrs(match.group(3))
954            if 'name' in attrs:
955                name = attrs.pop('name')
956            else:
957                name = None
958            if tag == 'option':
959                in_select.options.append((attrs.get('value'),
960                                          'selected' in attrs))
961                continue
962            if tag == 'input' and attrs.get('type') == 'radio':
963                field = fields.get(name)
964                if not field:
965                    field = Radio(self, tag, name, match.start(), **attrs)
966                    fields.setdefault(name, []).append(field)
967                else:
968                    field = field[0]
969                    assert isinstance(field, Radio)
970                field.options.append((attrs.get('value'),
971                                      'checked' in attrs))
972                continue
973            tag_type = tag
974            if tag == 'input':
975                tag_type = attrs.get('type', 'text').lower()
976            FieldClass = Field.classes.get(tag_type, Field)
977            field = FieldClass(self, tag, name, match.start(), **attrs)
978            if tag == 'textarea':
979                assert not in_textarea, (
980                    "Nested textareas: %r and %r"
981                    % (in_textarea, match.group(0)))
982                in_textarea = field, match.end()
983            elif tag == 'select':
984                assert not in_select, (
985                    "Nested selects: %r and %r"
986                    % (in_select, match.group(0)))
987                in_select = field
988            fields.setdefault(name, []).append(field)
989        self.fields = fields
990
991    def _parse_action(self):
992        self.action = None
993        for match in self._tag_re.finditer(self.text):
994            end = match.group(1) == '/'
995            tag = match.group(2).lower()
996            if tag != 'form':
997                continue
998            if end:
999                break
1000            attrs = _parse_attrs(match.group(3))
1001            self.action = attrs.get('action', '')
1002            self.method = attrs.get('method', 'GET')
1003            self.id = attrs.get('id')
1004            # @@: enctype?
1005        else:
1006            assert 0, "No </form> tag found"
1007        assert self.action is not None, (
1008            "No <form> tag found")
1009
1010    def __setitem__(self, name, value):
1011        """
1012        Set the value of the named field.  If there is 0 or multiple
1013        fields by that name, it is an error.
1014
1015        Setting the value of a ``<select>`` selects the given option
1016        (and confirms it is an option).  Setting radio fields does the
1017        same.  Checkboxes get boolean values.  You cannot set hidden
1018        fields or buttons.
1019
1020        Use ``.set()`` if there is any ambiguity and you must provide
1021        an index.
1022        """
1023        fields = self.fields.get(name)
1024        assert fields is not None, (
1025            "No field by the name %r found (fields: %s)"
1026            % (name, ', '.join(map(repr, self.fields.keys()))))
1027        assert len(fields) == 1, (
1028            "Multiple fields match %r: %s"
1029            % (name, ', '.join(map(repr, fields))))
1030        fields[0].value = value
1031
1032    def __getitem__(self, name):
1033        """
1034        Get the named field object (ambiguity is an error).
1035        """
1036        fields = self.fields.get(name)
1037        assert fields is not None, (
1038            "No field by the name %r found" % name)
1039        assert len(fields) == 1, (
1040            "Multiple fields match %r: %s"
1041            % (name, ', '.join(map(repr, fields))))
1042        return fields[0]
1043
1044    def set(self, name, value, index=None):
1045        """
1046        Set the given name, using ``index`` to disambiguate.
1047        """
1048        if index is None:
1049            self[name] = value
1050        else:
1051            fields = self.fields.get(name)
1052            assert fields is not None, (
1053                "No fields found matching %r" % name)
1054            field = fields[index]
1055            field.value = value
1056
1057    def get(self, name, index=None, default=NoDefault):
1058        """
1059        Get the named/indexed field object, or ``default`` if no field
1060        is found.
1061        """
1062        fields = self.fields.get(name)
1063        if fields is None and default is not NoDefault:
1064            return default
1065        if index is None:
1066            return self[name]
1067        else:
1068            fields = self.fields.get(name)
1069            assert fields is not None, (
1070                "No fields found matching %r" % name)
1071            field = fields[index]
1072            return field
1073
1074    def select(self, name, value, index=None):
1075        """
1076        Like ``.set()``, except also confirms the target is a
1077        ``<select>``.
1078        """
1079        field = self.get(name, index=index)
1080        assert isinstance(field, Select)
1081        field.value = value
1082
1083    def submit(self, name=None, index=None, **args):
1084        """
1085        Submits the form.  If ``name`` is given, then also select that
1086        button (using ``index`` to disambiguate)``.
1087
1088        Any extra keyword arguments are passed to the ``.get()`` or
1089        ``.post()`` method.
1090
1091        Returns a response object.
1092        """
1093        fields = self.submit_fields(name, index=index)
1094        return self.response.goto(self.action, method=self.method,
1095                                  params=fields, **args)
1096
1097    def submit_fields(self, name=None, index=None):
1098        """
1099        Return a list of ``[(name, value), ...]`` for the current
1100        state of the form.
1101        """
1102        submit = []
1103        if name is not None:
1104            field = self.get(name, index=index)
1105            submit.append((field.name, field.value_if_submitted()))
1106        for name, fields in self.fields.items():
1107            if name is None:
1108                continue
1109            for field in fields:
1110                value = field.value
1111                if value is None:
1112                    continue
1113                submit.append((name, value))
1114        return submit
1115
1116
1117_attr_re = re.compile(r'([^= \n\r\t]+)[ \n\r\t]*(?:=[ \n\r\t]*(?:"([^"]*)"|([^"][^ \n\r\t>]*)))?', re.S)
1118
1119def _parse_attrs(text):
1120    attrs = {}
1121    for match in _attr_re.finditer(text):
1122        attr_name = match.group(1).lower()
1123        attr_body = match.group(2) or match.group(3)
1124        attr_body = html_unquote(attr_body or '')
1125        attrs[attr_name] = attr_body
1126    return attrs
1127
1128class Field(object):
1129
1130    """
1131    Field object.
1132    """
1133
1134    # Dictionary of field types (select, radio, etc) to classes
1135    classes = {}
1136
1137    settable = True
1138
1139    def __init__(self, form, tag, name, pos,
1140                 value=None, id=None, **attrs):
1141        self.form = form
1142        self.tag = tag
1143        self.name = name
1144        self.pos = pos
1145        self._value = value
1146        self.id = id
1147        self.attrs = attrs
1148
1149    def value__set(self, value):
1150        if not self.settable:
1151            raise AttributeError(
1152                "You cannot set the value of the <%s> field %r"
1153                % (self.tag, self.name))
1154        self._value = value
1155
1156    def force_value(self, value):
1157        """
1158        Like setting a value, except forces it even for, say, hidden
1159        fields.
1160        """
1161        self._value = value
1162
1163    def value__get(self):
1164        return self._value
1165
1166    value = property(value__get, value__set)
1167
1168class Select(Field):
1169
1170    """
1171    Field representing ``<select>``
1172    """
1173
1174    def __init__(self, *args, **attrs):
1175        super(Select, self).__init__(*args, **attrs)
1176        self.options = []
1177        self.multiple = attrs.get('multiple')
1178        assert not self.multiple, (
1179            "<select multiple> not yet supported")
1180        # Undetermined yet:
1181        self.selectedIndex = None
1182
1183    def value__set(self, value):
1184        for i, (option, checked) in enumerate(self.options):
1185            if option == str(value):
1186                self.selectedIndex = i
1187                break
1188        else:
1189            raise ValueError(
1190                "Option %r not found (from %s)"
1191                % (value, ', '.join(
1192                [repr(o) for o, c in self.options])))
1193
1194    def value__get(self):
1195        if self.selectedIndex is not None:
1196            return self.options[self.selectedIndex][0]
1197        else:
1198            for option, checked in self.options:
1199                if checked:
1200                    return option
1201            else:
1202                if self.options:
1203                    return self.options[0][0]
1204                else:
1205                    return None
1206
1207    value = property(value__get, value__set)
1208
1209Field.classes['select'] = Select
1210
1211class Radio(Select):
1212
1213    """
1214    Field representing ``<input type="radio">``
1215    """
1216
1217Field.classes['radio'] = Radio
1218
1219class Checkbox(Field):
1220
1221    """
1222    Field representing ``<input type="checkbox">``
1223    """
1224
1225    def __init__(self, *args, **attrs):
1226        super(Checkbox, self).__init__(*args, **attrs)
1227        self.checked = 'checked' in attrs
1228
1229    def value__set(self, value):
1230        self.checked = not not value
1231
1232    def value__get(self):
1233        if self.checked:
1234            if self._value is None:
1235                return 'on'
1236            else:
1237                return self._value
1238        else:
1239            return None
1240
1241    value = property(value__get, value__set)
1242
1243Field.classes['checkbox'] = Checkbox
1244
1245class Text(Field):
1246    """
1247    Field representing ``<input type="text">``
1248    """
1249    def __init__(self, form, tag, name, pos,
1250                 value='', id=None, **attrs):
1251        #text fields default to empty string
1252        Field.__init__(self, form, tag, name, pos,
1253                       value=value, id=id, **attrs)
1254
1255Field.classes['text'] = Text
1256
1257class Textarea(Text):
1258    """
1259    Field representing ``<textarea>``
1260    """
1261
1262Field.classes['textarea'] = Textarea
1263
1264class Hidden(Text):
1265    """
1266    Field representing ``<input type="hidden">``
1267    """
1268
1269Field.classes['hidden'] = Hidden
1270
1271class Submit(Field):
1272    """
1273    Field representing ``<input type="submit">`` and ``<button>``
1274    """
1275
1276    settable = False
1277
1278    def value__get(self):
1279        return None
1280
1281    value = property(value__get)
1282
1283    def value_if_submitted(self):
1284        return self._value
1285
1286Field.classes['submit'] = Submit
1287
1288Field.classes['button'] = Submit
1289
1290Field.classes['image'] = Submit
1291
1292############################################################
1293## Command-line testing
1294############################################################
1295
1296
1297class TestFileEnvironment(object):
1298
1299    """
1300    This represents an environment in which files will be written, and
1301    scripts will be run.
1302    """
1303
1304    # for py.test
1305    disabled = True
1306
1307    def __init__(self, base_path, template_path=None,
1308                 script_path=None,
1309                 environ=None, cwd=None, start_clear=True,
1310                 ignore_paths=None, ignore_hidden=True):
1311        """
1312        Creates an environment.  ``base_path`` is used as the current
1313        working directory, and generally where changes are looked for.
1314
1315        ``template_path`` is the directory to look for *template*
1316        files, which are files you'll explicitly add to the
1317        environment.  This is done with ``.writefile()``.
1318
1319        ``script_path`` is the PATH for finding executables.  Usually
1320        grabbed from ``$PATH``.
1321
1322        ``environ`` is the operating system environment,
1323        ``os.environ`` if not given.
1324
1325        ``cwd`` is the working directory, ``base_path`` by default.
1326
1327        If ``start_clear`` is true (default) then the ``base_path``
1328        will be cleared (all files deleted) when an instance is
1329        created.  You can also use ``.clear()`` to clear the files.
1330
1331        ``ignore_paths`` is a set of specific filenames that should be
1332        ignored when created in the environment.  ``ignore_hidden``
1333        means, if true (default) that filenames and directories
1334        starting with ``'.'`` will be ignored.
1335        """
1336        self.base_path = base_path
1337        self.template_path = template_path
1338        if environ is None:
1339            environ = os.environ.copy()
1340        self.environ = environ
1341        if script_path is None:
1342            if sys.platform == 'win32':
1343                script_path = environ.get('PATH', '').split(';')
1344            else:
1345                script_path = environ.get('PATH', '').split(':')
1346        self.script_path = script_path
1347        if cwd is None:
1348            cwd = base_path
1349        self.cwd = cwd
1350        if start_clear:
1351            self.clear()
1352        elif not os.path.exists(base_path):
1353            os.makedirs(base_path)
1354        self.ignore_paths = ignore_paths or []
1355        self.ignore_hidden = ignore_hidden
1356
1357    def run(self, script, *args, **kw):
1358        """
1359        Run the command, with the given arguments.  The ``script``
1360        argument can have space-separated arguments, or you can use
1361        the positional arguments.
1362
1363        Keywords allowed are:
1364
1365        ``expect_error``: (default False)
1366            Don't raise an exception in case of errors
1367        ``expect_stderr``: (default ``expect_error``)
1368            Don't raise an exception if anything is printed to stderr
1369        ``stdin``: (default ``""``)
1370            Input to the script
1371        ``printresult``: (default True)
1372            Print the result after running
1373        ``cwd``: (default ``self.cwd``)
1374            The working directory to run in
1375
1376        Returns a `ProcResponse
1377        <class-paste.fixture.ProcResponse.html>`_ object.
1378        """
1379        __tracebackhide__ = True
1380        expect_error = _popget(kw, 'expect_error', False)
1381        expect_stderr = _popget(kw, 'expect_stderr', expect_error)
1382        cwd = _popget(kw, 'cwd', self.cwd)
1383        stdin = _popget(kw, 'stdin', None)
1384        printresult = _popget(kw, 'printresult', True)
1385        args = map(str, args)
1386        assert not kw, (
1387            "Arguments not expected: %s" % ', '.join(kw.keys()))
1388        if ' ' in script:
1389            assert not args, (
1390                "You cannot give a multi-argument script (%r) "
1391                "and arguments (%s)" % (script, args))
1392            script, args = script.split(None, 1)
1393            args = shlex.split(args)
1394        script = self._find_exe(script)
1395        all = [script] + args
1396        files_before = self._find_files()
1397        proc = subprocess.Popen(all, stdin=subprocess.PIPE,
1398                                stderr=subprocess.PIPE,
1399                                stdout=subprocess.PIPE,
1400                                cwd=cwd,
1401                                env=self.environ)
1402        stdout, stderr = proc.communicate(stdin)
1403        files_after = self._find_files()
1404        result = ProcResult(
1405            self, all, stdin, stdout, stderr,
1406            returncode=proc.returncode,
1407            files_before=files_before,
1408            files_after=files_after)
1409        if printresult:
1410            print result
1411            print '-'*40
1412        if not expect_error:
1413            result.assert_no_error()
1414        if not expect_stderr:
1415            result.assert_no_stderr()
1416        return result
1417
1418    def _find_exe(self, script_name):
1419        if self.script_path is None:
1420            script_name = os.path.join(self.cwd, script_name)
1421            if not os.path.exists(script_name):
1422                raise OSError(
1423                    "Script %s does not exist" % script_name)
1424            return script_name
1425        for path in self.script_path:
1426            fn = os.path.join(path, script_name)
1427            if os.path.exists(fn):
1428                return fn
1429        raise OSError(
1430            "Script %s could not be found in %s"
1431            % (script_name, ':'.join(self.script_path)))
1432
1433    def _find_files(self):
1434        result = {}
1435        for fn in os.listdir(self.base_path):
1436            if self._ignore_file(fn):
1437                continue
1438            self._find_traverse(fn, result)
1439        return result
1440
1441    def _ignore_file(self, fn):
1442        if fn in self.ignore_paths:
1443            return True
1444        if self.ignore_hidden and os.path.basename(fn).startswith('.'):
1445            return True
1446        return False
1447
1448    def _find_traverse(self, path, result):
1449        full = os.path.join(self.base_path, path)
1450        if os.path.isdir(full):
1451            result[path] = FoundDir(self.base_path, path)
1452            for fn in os.listdir(full):
1453                fn = os.path.join(path, fn)
1454                if self._ignore_file(fn):
1455                    continue
1456                self._find_traverse(fn, result)
1457        else:
1458            result[path] = FoundFile(self.base_path, path)
1459
1460    def clear(self):
1461        """
1462        Delete all the files in the base directory.
1463        """
1464        if os.path.exists(self.base_path):
1465            shutil.rmtree(self.base_path)
1466        os.mkdir(self.base_path)
1467
1468    def writefile(self, path, content=None,
1469                  frompath=None):
1470        """
1471        Write a file to the given path.  If ``content`` is given then
1472        that text is written, otherwise the file in ``frompath`` is
1473        used.  ``frompath`` is relative to ``self.template_path``
1474        """
1475        full = os.path.join(self.base_path, path)
1476        if not os.path.exists(os.path.dirname(full)):
1477            os.makedirs(os.path.dirname(full))
1478        f = open(full, 'wb')
1479        if content is not None:
1480            f.write(content)
1481        if frompath is not None:
1482            if self.template_path:
1483                frompath = os.path.join(self.template_path, frompath)
1484            f2 = open(frompath, 'rb')
1485            f.write(f2.read())
1486            f2.close()
1487        f.close()
1488        return FoundFile(self.base_path, path)
1489
1490class ProcResult(object):
1491
1492    """
1493    Represents the results of running a command in
1494    `TestFileEnvironment
1495    <class-paste.fixture.TestFileEnvironment.html>`_.
1496
1497    Attributes to pay particular attention to:
1498
1499    ``stdout``, ``stderr``:
1500        What is produced
1501
1502    ``files_created``, ``files_deleted``, ``files_updated``:
1503        Dictionaries mapping filenames (relative to the ``base_dir``)
1504        to `FoundFile <class-paste.fixture.FoundFile.html>`_ or
1505        `FoundDir <class-paste.fixture.FoundDir.html>`_ objects.
1506    """
1507
1508    def __init__(self, test_env, args, stdin, stdout, stderr,
1509                 returncode, files_before, files_after):
1510        self.test_env = test_env
1511        self.args = args
1512        self.stdin = stdin
1513        self.stdout = stdout
1514        self.stderr = stderr
1515        self.returncode = returncode
1516        self.files_before = files_before
1517        self.files_after = files_after
1518        self.files_deleted = {}
1519        self.files_updated = {}
1520        self.files_created = files_after.copy()
1521        for path, f in files_before.items():
1522            if path not in files_after:
1523                self.files_deleted[path] = f
1524                continue
1525            del self.files_created[path]
1526            if f.mtime < files_after[path].mtime:
1527                self.files_updated[path] = files_after[path]
1528
1529    def assert_no_error(self):
1530        __tracebackhide__ = True
1531        assert self.returncode == 0, (
1532            "Script returned code: %s" % self.returncode)
1533
1534    def assert_no_stderr(self):
1535        __tracebackhide__ = True
1536        if self.stderr:
1537            print 'Error output:'
1538            print self.stderr
1539            raise AssertionError("stderr output not expected")
1540
1541    def __str__(self):
1542        s = ['Script result: %s' % ' '.join(self.args)]
1543        if self.returncode:
1544            s.append('  return code: %s' % self.returncode)
1545        if self.stderr:
1546            s.append('-- stderr: --------------------')
1547            s.append(self.stderr)
1548        if self.stdout:
1549            s.append('-- stdout: --------------------')
1550            s.append(self.stdout)
1551        for name, files, show_size in [
1552            ('created', self.files_created, True),
1553            ('deleted', self.files_deleted, True),
1554            ('updated', self.files_updated, True)]:
1555            if files:
1556                s.append('-- %s: -------------------' % name)
1557                files = files.items()
1558                files.sort()
1559                last = ''
1560                for path, f in files:
1561                    t = '  %s' % _space_prefix(last, path, indent=4,
1562                                               include_sep=False)
1563                    last = path
1564                    if show_size and f.size != 'N/A':
1565                        t += '  (%s bytes)' % f.size
1566                    s.append(t)
1567        return '\n'.join(s)
1568
1569class FoundFile(object):
1570
1571    """
1572    Represents a single file found as the result of a command.
1573
1574    Has attributes:
1575
1576    ``path``:
1577        The path of the file, relative to the ``base_path``
1578
1579    ``full``:
1580        The full path
1581
1582    ``stat``:
1583        The results of ``os.stat``.  Also ``mtime`` and ``size``
1584        contain the ``.st_mtime`` and ``st_size`` of the stat.
1585
1586    ``bytes``:
1587        The contents of the file.
1588
1589    You may use the ``in`` operator with these objects (tested against
1590    the contents of the file), and the ``.mustcontain()`` method.
1591    """
1592
1593    file = True
1594    dir = False
1595
1596    def __init__(self, base_path, path):
1597        self.base_path = base_path
1598        self.path = path
1599        self.full = os.path.join(base_path, path)
1600        self.stat = os.stat(self.full)
1601        self.mtime = self.stat.st_mtime
1602        self.size = self.stat.st_size
1603        self._bytes = None
1604
1605    def bytes__get(self):
1606        if self._bytes is None:
1607            f = open(self.full, 'rb')
1608            self._bytes = f.read()
1609            f.close()
1610        return self._bytes
1611    bytes = property(bytes__get)
1612
1613    def __contains__(self, s):
1614        return s in self.bytes
1615
1616    def mustcontain(self, s):
1617        __tracebackhide__ = True
1618        bytes = self.bytes
1619        if s not in bytes:
1620            print 'Could not find %r in:' % s
1621            print bytes
1622            assert s in bytes
1623
1624    def __repr__(self):
1625        return '<%s %s:%s>' % (
1626            self.__class__.__name__,
1627            self.base_path, self.path)
1628
1629class FoundDir(object):
1630
1631    """
1632    Represents a directory created by a command.
1633    """
1634
1635    file = False
1636    dir = True
1637
1638    def __init__(self, base_path, path):
1639        self.base_path = base_path
1640        self.path = path
1641        self.full = os.path.join(base_path, path)
1642        self.size = 'N/A'
1643        self.mtime = 'N/A'
1644
1645    def __repr__(self):
1646        return '<%s %s:%s>' % (
1647            self.__class__.__name__,
1648            self.base_path, self.path)
1649
1650def _popget(d, key, default=None):
1651    """
1652    Pop the key if found (else return default)
1653    """
1654    if key in d:
1655        return d.pop(key)
1656    return default
1657
1658def _space_prefix(pref, full, sep=None, indent=None, include_sep=True):
1659    """
1660    Anything shared by pref and full will be replaced with spaces
1661    in full, and full returned.
1662    """
1663    if sep is None:
1664        sep = os.path.sep
1665    pref = pref.split(sep)
1666    full = full.split(sep)
1667    padding = []
1668    while pref and full and pref[0] == full[0]:
1669        if indent is None:
1670            padding.append(' ' * (len(full[0]) + len(sep)))
1671        else:
1672            padding.append(' ' * indent)
1673        full.pop(0)
1674        pref.pop(0)
1675    if padding:
1676        if include_sep:
1677            return ''.join(padding) + sep + sep.join(full)
1678        else:
1679            return ''.join(padding) + sep.join(full)
1680    else:
1681        return sep.join(full)
1682
1683def _make_pattern(pat):
1684    if pat is None:
1685        return None
1686    if isinstance(pat, (str, unicode)):
1687        pat = re.compile(pat)
1688    if hasattr(pat, 'search'):
1689        return pat.search
1690    if callable(pat):
1691        return pat
1692    assert 0, (
1693        "Cannot make callable pattern object out of %r" % pat)
1694
1695def setup_module(module=None):
1696    """
1697    This is used by py.test if it is in the module, so you can
1698    import this directly.
1699
1700    Use like::
1701
1702        from paste.fixture import setup_module
1703    """
1704    # Deprecated June 2008
1705    import warnings
1706    warnings.warn(
1707        'setup_module is deprecated',
1708        DeprecationWarning, 2)
1709    if module is None:
1710        # The module we were called from must be the module...
1711        module = sys._getframe().f_back.f_globals['__name__']
1712    if isinstance(module, (str, unicode)):
1713        module = sys.modules[module]
1714    if hasattr(module, 'reset_state'):
1715        module.reset_state()
1716
1717def html_unquote(v):
1718    """
1719    Unquote (some) entities in HTML.  (incomplete)
1720    """
1721    for ent, repl in [('&nbsp;', ' '), ('&gt;', '>'),
1722                      ('&lt;', '<'), ('&quot;', '"'),
1723                      ('&amp;', '&')]:
1724        v = v.replace(ent, repl)
1725    return v
Note: Veja TracBrowser para ajuda no uso do navegador do trac.
 

The contents and data of this website are published under license:
Creative Commons 4.0 Brasil - Atribuir Fonte - Compartilhar Igual.