root/galaxy-central/eggs/Paste-1.6-py2.6.egg/paste/fixture.py @ 3

リビジョン 3, 56.2 KB (コミッタ: kohda, 14 年 前)

Install Unix tools  http://hannonlab.cshl.edu/galaxy_unix_tools/galaxy.html

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