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

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

Import inicial.

File size: 25.8 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"""
4WSGI applications that parse the URL and dispatch to on-disk resources
5"""
6
7import os
8import sys
9import imp
10import mimetypes
11try:
12    import pkg_resources
13except ImportError:
14    pkg_resources = None
15from paste import request
16from paste import fileapp
17from paste.util import import_string
18from paste import httpexceptions
19from httpheaders import ETAG
20from paste.util import converters
21
22class NoDefault(object):
23    pass
24
25__all__ = ['URLParser', 'StaticURLParser', 'PkgResourcesParser']
26
27class URLParser(object):
28
29    """
30    WSGI middleware
31
32    Application dispatching, based on URL.  An instance of `URLParser` is
33    an application that loads and delegates to other applications.  It
34    looks for files in its directory that match the first part of
35    PATH_INFO; these may have an extension, but are not required to have
36    one, in which case the available files are searched to find the
37    appropriate file.  If it is ambiguous, a 404 is returned and an error
38    logged.
39
40    By default there is a constructor for .py files that loads the module,
41    and looks for an attribute ``application``, which is a ready
42    application object, or an attribute that matches the module name,
43    which is a factory for building applications, and is called with no
44    arguments.
45
46    URLParser will also look in __init__.py for special overrides.
47    These overrides are:
48
49    ``urlparser_hook(environ)``
50        This can modify the environment.  Its return value is ignored,
51        and it cannot be used to change the response in any way.  You
52        *can* use this, for example, to manipulate SCRIPT_NAME/PATH_INFO
53        (try to keep them consistent with the original URL -- but
54        consuming PATH_INFO and moving that to SCRIPT_NAME is ok).
55
56    ``urlparser_wrap(environ, start_response, app)``:
57        After URLParser finds the application, it calls this function
58        (if present).  If this function doesn't call
59        ``app(environ, start_response)`` then the application won't be
60        called at all!  This can be used to allocate resources (with
61        ``try:finally:``) or otherwise filter the output of the
62        application.
63
64    ``not_found_hook(environ, start_response)``:
65        If no file can be found (*in this directory*) to match the
66        request, then this WSGI application will be called.  You can
67        use this to change the URL and pass the request back to
68        URLParser again, or on to some other application.  This
69        doesn't catch all ``404 Not Found`` responses, just missing
70        files.
71
72    ``application(environ, start_response)``:
73        This basically overrides URLParser completely, and the given
74        application is used for all requests.  ``urlparser_wrap`` and
75        ``urlparser_hook`` are still called, but the filesystem isn't
76        searched in any way.
77    """
78
79    parsers_by_directory = {}
80
81    # This is lazily initialized
82    init_module = NoDefault
83
84    global_constructors = {}
85
86    def __init__(self, global_conf,
87                 directory, base_python_name,
88                 index_names=NoDefault,
89                 hide_extensions=NoDefault,
90                 ignore_extensions=NoDefault,
91                 constructors=None,
92                 **constructor_conf):
93        """
94        Create a URLParser object that looks at `directory`.
95        `base_python_name` is the package that this directory
96        represents, thus any Python modules in this directory will
97        be given names under this package.
98        """
99        if global_conf:
100            import warnings
101            warnings.warn(
102                'The global_conf argument to URLParser is deprecated; '
103                'either pass in None or {}, or use make_url_parser',
104                DeprecationWarning)
105        else:
106            global_conf = {}
107        if os.path.sep != '/':
108            directory = directory.replace(os.path.sep, '/')
109        self.directory = directory
110        self.base_python_name = base_python_name
111        # This logic here should be deprecated since it is in
112        # make_url_parser
113        if index_names is NoDefault:
114            index_names = global_conf.get(
115                'index_names', ('index', 'Index', 'main', 'Main'))
116        self.index_names = converters.aslist(index_names)
117        if hide_extensions is NoDefault:
118            hide_extensions = global_conf.get(
119                'hide_extensions', ('.pyc', '.bak', '.py~', '.pyo'))
120        self.hide_extensions = converters.aslist(hide_extensions)
121        if ignore_extensions is NoDefault:
122            ignore_extensions = global_conf.get(
123                'ignore_extensions', ())
124        self.ignore_extensions = converters.aslist(ignore_extensions)
125        self.constructors = self.global_constructors.copy()
126        if constructors:
127            self.constructors.update(constructors)
128        # @@: Should we also check the global options for constructors?
129        for name, value in constructor_conf.items():
130            if not name.startswith('constructor '):
131                raise ValueError(
132                    "Only extra configuration keys allowed are "
133                    "'constructor .ext = import_expr'; you gave %r "
134                    "(=%r)" % (name, value))
135            ext = name[len('constructor '):].strip()
136            if isinstance(value, (str, unicode)):
137                value = import_string.eval_import(value)
138            self.constructors[ext] = value
139
140    def __call__(self, environ, start_response):
141        environ['paste.urlparser.base_python_name'] = self.base_python_name
142        if self.init_module is NoDefault:
143            self.init_module = self.find_init_module(environ)
144        path_info = environ.get('PATH_INFO', '')
145        if not path_info:
146            return self.add_slash(environ, start_response)
147        if (self.init_module
148            and getattr(self.init_module, 'urlparser_hook', None)):
149            self.init_module.urlparser_hook(environ)
150        orig_path_info = environ['PATH_INFO']
151        orig_script_name = environ['SCRIPT_NAME']
152        application, filename = self.find_application(environ)
153        if not application:
154            if (self.init_module
155                and getattr(self.init_module, 'not_found_hook', None)
156                and environ.get('paste.urlparser.not_found_parser') is not self):
157                not_found_hook = self.init_module.not_found_hook
158                environ['paste.urlparser.not_found_parser'] = self
159                environ['PATH_INFO'] = orig_path_info
160                environ['SCRIPT_NAME'] = orig_script_name
161                return not_found_hook(environ, start_response)
162            if filename is None:
163                name, rest_of_path = request.path_info_split(environ['PATH_INFO'])
164                if not name:
165                    name = 'one of %s' % ', '.join(
166                        self.index_names or
167                        ['(no index_names defined)'])
168
169                return self.not_found(
170                    environ, start_response,
171                    'Tried to load %s from directory %s'
172                    % (name, self.directory))
173            else:
174                environ['wsgi.errors'].write(
175                    'Found resource %s, but could not construct application\n'
176                    % filename)
177                return self.not_found(
178                    environ, start_response,
179                    'Tried to load %s from directory %s'
180                    % (filename, self.directory))
181        if (self.init_module
182            and getattr(self.init_module, 'urlparser_wrap', None)):
183            return self.init_module.urlparser_wrap(
184                environ, start_response, application)
185        else:
186            return application(environ, start_response)
187
188    def find_application(self, environ):
189        if (self.init_module
190            and getattr(self.init_module, 'application', None)
191            and not environ.get('paste.urlparser.init_application') == environ['SCRIPT_NAME']):
192            environ['paste.urlparser.init_application'] = environ['SCRIPT_NAME']
193            return self.init_module.application, None
194        name, rest_of_path = request.path_info_split(environ['PATH_INFO'])
195        environ['PATH_INFO'] = rest_of_path
196        if name is not None:
197            environ['SCRIPT_NAME'] = environ.get('SCRIPT_NAME', '') + '/' + name
198        if not name:
199            names = self.index_names
200            for index_name in names:
201                filename = self.find_file(environ, index_name)
202                if filename:
203                    break
204            else:
205                # None of the index files found
206                filename = None
207        else:
208            filename = self.find_file(environ, name)
209        if filename is None:
210            return None, filename
211        else:
212            return self.get_application(environ, filename), filename
213
214    def not_found(self, environ, start_response, debug_message=None):
215        exc = httpexceptions.HTTPNotFound(
216            'The resource at %s could not be found'
217            % request.construct_url(environ),
218            comment=debug_message)
219        return exc.wsgi_application(environ, start_response)
220
221    def add_slash(self, environ, start_response):
222        """
223        This happens when you try to get to a directory
224        without a trailing /
225        """
226        url = request.construct_url(environ, with_query_string=False)
227        url += '/'
228        if environ.get('QUERY_STRING'):
229            url += '?' + environ['QUERY_STRING']
230        exc = httpexceptions.HTTPMovedPermanently(
231            'The resource has moved to %s - you should be redirected '
232            'automatically.' % url,
233            headers=[('location', url)])
234        return exc.wsgi_application(environ, start_response)
235
236    def find_file(self, environ, base_filename):
237        possible = []
238        """Cache a few values to reduce function call overhead"""
239        for filename in os.listdir(self.directory):
240            base, ext = os.path.splitext(filename)
241            full_filename = os.path.join(self.directory, filename)
242            if (ext in self.hide_extensions
243                or not base):
244                continue
245            if filename == base_filename:
246                possible.append(full_filename)
247                continue
248            if ext in self.ignore_extensions:
249                continue
250            if base == base_filename:
251                possible.append(full_filename)
252        if not possible:
253            #environ['wsgi.errors'].write(
254            #    'No file found matching %r in %s\n'
255            #    % (base_filename, self.directory))
256            return None
257        if len(possible) > 1:
258            # If there is an exact match, this isn't 'ambiguous'
259            # per se; it might mean foo.gif and foo.gif.back for
260            # instance
261            if full_filename in possible:
262                return full_filename
263            else:
264                environ['wsgi.errors'].write(
265                    'Ambiguous URL: %s; matches files %s\n'
266                    % (request.construct_url(environ),
267                       ', '.join(possible)))
268            return None
269        return possible[0]
270
271    def get_application(self, environ, filename):
272        if os.path.isdir(filename):
273            t = 'dir'
274        else:
275            t = os.path.splitext(filename)[1]
276        constructor = self.constructors.get(t, self.constructors.get('*'))
277        if constructor is None:
278            #environ['wsgi.errors'].write(
279            #    'No constructor found for %s\n' % t)
280            return constructor
281        app = constructor(self, environ, filename)
282        if app is None:
283            #environ['wsgi.errors'].write(
284            #    'Constructor %s return None for %s\n' %
285            #    (constructor, filename))
286            pass
287        return app
288
289    def register_constructor(cls, extension, constructor):
290        """
291        Register a function as a constructor.  Registered constructors
292        apply to all instances of `URLParser`.
293
294        The extension should have a leading ``.``, or the special
295        extensions ``dir`` (for directories) and ``*`` (a catch-all).
296
297        `constructor` must be a callable that takes two arguments:
298        ``environ`` and ``filename``, and returns a WSGI application.
299        """
300        d = cls.global_constructors
301        assert not d.has_key(extension), (
302            "A constructor already exists for the extension %r (%r) "
303            "when attemption to register constructor %r"
304            % (extension, d[extension], constructor))
305        d[extension] = constructor
306    register_constructor = classmethod(register_constructor)
307
308    def get_parser(self, directory, base_python_name):
309        """
310        Get a parser for the given directory, or create one if
311        necessary.  This way parsers can be cached and reused.
312
313        # @@: settings are inherited from the first caller
314        """
315        try:
316            return self.parsers_by_directory[(directory, base_python_name)]
317        except KeyError:
318            parser = self.__class__(
319                {},
320                directory, base_python_name,
321                index_names=self.index_names,
322                hide_extensions=self.hide_extensions,
323                ignore_extensions=self.ignore_extensions,
324                constructors=self.constructors)
325            self.parsers_by_directory[(directory, base_python_name)] = parser
326            return parser
327
328    def find_init_module(self, environ):
329        filename = os.path.join(self.directory, '__init__.py')
330        if not os.path.exists(filename):
331            return None
332        return load_module(environ, filename)
333
334    def __repr__(self):
335        return '<%s directory=%r; module=%s at %s>' % (
336            self.__class__.__name__,
337            self.directory,
338            self.base_python_name,
339            hex(abs(id(self))))
340
341def make_directory(parser, environ, filename):
342    base_python_name = environ['paste.urlparser.base_python_name']
343    if base_python_name:
344        base_python_name += "." + os.path.basename(filename)
345    else:
346        base_python_name = os.path.basename(filename)
347    return parser.get_parser(filename, base_python_name)
348
349URLParser.register_constructor('dir', make_directory)
350
351def make_unknown(parser, environ, filename):
352    return fileapp.FileApp(filename)
353
354URLParser.register_constructor('*', make_unknown)
355
356def load_module(environ, filename):
357    base_python_name = environ['paste.urlparser.base_python_name']
358    module_name = os.path.splitext(os.path.basename(filename))[0]
359    if base_python_name:
360        module_name = base_python_name + '.' + module_name
361    return load_module_from_name(environ, filename, module_name,
362                                 environ['wsgi.errors'])
363
364def load_module_from_name(environ, filename, module_name, errors):
365    if sys.modules.has_key(module_name):
366        return sys.modules[module_name]
367    init_filename = os.path.join(os.path.dirname(filename), '__init__.py')
368    if not os.path.exists(init_filename):
369        try:
370            f = open(init_filename, 'w')
371        except (OSError, IOError), e:
372            errors.write(
373                'Cannot write __init__.py file into directory %s (%s)\n'
374                % (os.path.dirname(filename), e))
375            return None
376        f.write('#\n')
377        f.close()
378    fp = None
379    if sys.modules.has_key(module_name):
380        return sys.modules[module_name]
381    if '.' in module_name:
382        parent_name = '.'.join(module_name.split('.')[:-1])
383        base_name = module_name.split('.')[-1]
384        parent = load_module_from_name(environ, os.path.dirname(filename),
385                                       parent_name, errors)
386    else:
387        base_name = module_name
388    fp = None
389    try:
390        fp, pathname, stuff = imp.find_module(
391            base_name, [os.path.dirname(filename)])
392        module = imp.load_module(module_name, fp, pathname, stuff)
393    finally:
394        if fp is not None:
395            fp.close()
396    return module
397
398def make_py(parser, environ, filename):
399    module = load_module(environ, filename)
400    if not module:
401        return None
402    if hasattr(module, 'application') and module.application:
403        return getattr(module.application, 'wsgi_application', module.application)
404    base_name = module.__name__.split('.')[-1]
405    if hasattr(module, base_name):
406        obj = getattr(module, base_name)
407        if hasattr(obj, 'wsgi_application'):
408            return obj.wsgi_application
409        else:
410            # @@: Old behavior; should probably be deprecated eventually:
411            return getattr(module, base_name)()
412    environ['wsgi.errors'].write(
413        "Cound not find application or %s in %s\n"
414        % (base_name, module))
415    return None
416
417URLParser.register_constructor('.py', make_py)
418
419class StaticURLParser(object):
420    """
421    Like ``URLParser`` but only serves static files.
422
423    ``cache_max_age``:
424      integer specifies Cache-Control max_age in seconds
425    """
426    # @@: Should URLParser subclass from this?
427
428    def __init__(self, directory, root_directory=None,
429                 cache_max_age=None):
430        self.directory = self.normpath(directory)
431        self.root_directory = self.normpath(root_directory or directory)
432        self.cache_max_age = cache_max_age
433
434    def normpath(path):
435        return os.path.normcase(os.path.abspath(path))
436    normpath = staticmethod(normpath)
437
438    def __call__(self, environ, start_response):
439        path_info = environ.get('PATH_INFO', '')
440        if not path_info:
441            return self.add_slash(environ, start_response)
442        if path_info == '/':
443            # @@: This should obviously be configurable
444            filename = 'index.html'
445        else:
446            filename = request.path_info_pop(environ)
447        full = self.normpath(os.path.join(self.directory, filename))
448        if not full.startswith(self.root_directory):
449            # Out of bounds
450            return self.not_found(environ, start_response)
451        if not os.path.exists(full):
452            return self.not_found(environ, start_response)
453        if os.path.isdir(full):
454            # @@: Cache?
455            return self.__class__(full, root_directory=self.root_directory,
456                                  cache_max_age=self.cache_max_age)(environ,
457                                                                   start_response)
458        if environ.get('PATH_INFO') and environ.get('PATH_INFO') != '/':
459            return self.error_extra_path(environ, start_response)
460        if_none_match = environ.get('HTTP_IF_NONE_MATCH')
461        if if_none_match:
462            mytime = os.stat(full).st_mtime
463            if str(mytime) == if_none_match:
464                headers = []
465                ## FIXME: probably should be
466                ## ETAG.update(headers, '"%s"' % mytime)
467                ETAG.update(headers, mytime)
468                start_response('304 Not Modified', headers)
469                return [''] # empty body
470
471        fa = self.make_app(full)
472        if self.cache_max_age:
473            fa.cache_control(max_age=self.cache_max_age)
474        return fa(environ, start_response)
475
476    def make_app(self, filename):
477        return fileapp.FileApp(filename)
478
479    def add_slash(self, environ, start_response):
480        """
481        This happens when you try to get to a directory
482        without a trailing /
483        """
484        url = request.construct_url(environ, with_query_string=False)
485        url += '/'
486        if environ.get('QUERY_STRING'):
487            url += '?' + environ['QUERY_STRING']
488        exc = httpexceptions.HTTPMovedPermanently(
489            'The resource has moved to %s - you should be redirected '
490            'automatically.' % url,
491            headers=[('location', url)])
492        return exc.wsgi_application(environ, start_response)
493
494    def not_found(self, environ, start_response, debug_message=None):
495        exc = httpexceptions.HTTPNotFound(
496            'The resource at %s could not be found'
497            % request.construct_url(environ),
498            comment='SCRIPT_NAME=%r; PATH_INFO=%r; looking in %r; debug: %s'
499            % (environ.get('SCRIPT_NAME'), environ.get('PATH_INFO'),
500               self.directory, debug_message or '(none)'))
501        return exc.wsgi_application(environ, start_response)
502
503    def error_extra_path(self, environ, start_response):
504        exc = httpexceptions.HTTPNotFound(
505            'The trailing path %r is not allowed' % environ['PATH_INFO'])
506        return exc.wsgi_application(environ, start_response)
507
508    def __repr__(self):
509        return '<%s %r>' % (self.__class__.__name__, self.directory)
510
511def make_static(global_conf, document_root, cache_max_age=None):
512    """
513    Return a WSGI application that serves a directory (configured
514    with document_root)
515
516    cache_max_age - integer specifies CACHE_CONTROL max_age in seconds
517    """
518    if cache_max_age is not None:
519        cache_max_age = int(cache_max_age)
520    return StaticURLParser(
521        document_root, cache_max_age=cache_max_age)
522
523class PkgResourcesParser(StaticURLParser):
524
525    def __init__(self, egg_or_spec, resource_name, manager=None, root_resource=None):
526        if pkg_resources is None:
527            raise NotImplementedError("This class requires pkg_resources.")
528        if isinstance(egg_or_spec, (str, unicode)):
529            self.egg = pkg_resources.get_distribution(egg_or_spec)
530        else:
531            self.egg = egg_or_spec
532        self.resource_name = resource_name
533        if manager is None:
534            manager = pkg_resources.ResourceManager()
535        self.manager = manager
536        if root_resource is None:
537            root_resource = resource_name
538        self.root_resource = os.path.normpath(root_resource)
539
540    def __repr__(self):
541        return '<%s for %s:%r>' % (
542            self.__class__.__name__,
543            self.egg.project_name,
544            self.resource_name)
545
546    def __call__(self, environ, start_response):
547        path_info = environ.get('PATH_INFO', '')
548        if not path_info:
549            return self.add_slash(environ, start_response)
550        if path_info == '/':
551            # @@: This should obviously be configurable
552            filename = 'index.html'
553        else:
554            filename = request.path_info_pop(environ)
555        resource = os.path.normcase(os.path.normpath(
556                    self.resource_name + '/' + filename))
557        if self.root_resource is not None and not resource.startswith(self.root_resource):
558            # Out of bounds
559            return self.not_found(environ, start_response)
560        if not self.egg.has_resource(resource):
561            return self.not_found(environ, start_response)
562        if self.egg.resource_isdir(resource):
563            # @@: Cache?
564            child_root = self.root_resource is not None and self.root_resource or \
565                self.resource_name
566            return self.__class__(self.egg, resource, self.manager,
567                                  root_resource=child_root)(environ, start_response)
568        if environ.get('PATH_INFO') and environ.get('PATH_INFO') != '/':
569            return self.error_extra_path(environ, start_response)
570
571        type, encoding = mimetypes.guess_type(resource)
572        if not type:
573            type = 'application/octet-stream'
574        # @@: I don't know what to do with the encoding.
575        try:
576            file = self.egg.get_resource_stream(self.manager, resource)
577        except (IOError, OSError), e:
578            exc = httpexceptions.HTTPForbidden(
579                'You are not permitted to view this file (%s)' % e)
580            return exc.wsgi_application(environ, start_response)
581        start_response('200 OK',
582                       [('content-type', type)])
583        return fileapp._FileIter(file)
584
585    def not_found(self, environ, start_response, debug_message=None):
586        exc = httpexceptions.HTTPNotFound(
587            'The resource at %s could not be found'
588            % request.construct_url(environ),
589            comment='SCRIPT_NAME=%r; PATH_INFO=%r; looking in egg:%s#%r; debug: %s'
590            % (environ.get('SCRIPT_NAME'), environ.get('PATH_INFO'),
591               self.egg, self.resource_name, debug_message or '(none)'))
592        return exc.wsgi_application(environ, start_response)
593
594def make_pkg_resources(global_conf, egg, resource_name=''):
595    """
596    A static file parser that loads data from an egg using
597    ``pkg_resources``.  Takes a configuration value ``egg``, which is
598    an egg spec, and a base ``resource_name`` (default empty string)
599    which is the path in the egg that this starts at.
600    """
601    if pkg_resources is None:
602        raise NotImplementedError("This function requires pkg_resources.")
603    return PkgResourcesParser(egg, resource_name)
604
605def make_url_parser(global_conf, directory, base_python_name,
606                    index_names=None, hide_extensions=None,
607                    ignore_extensions=None,
608                    **constructor_conf):
609    """
610    Create a URLParser application that looks in ``directory``, which
611    should be the directory for the Python package named in
612    ``base_python_name``.  ``index_names`` are used when viewing the
613    directory (like ``'index'`` for ``'index.html'``).
614    ``hide_extensions`` are extensions that are not viewable (like
615    ``'.pyc'``) and ``ignore_extensions`` are viewable but only if an
616    explicit extension is given.
617    """
618    if index_names is None:
619        index_names = global_conf.get(
620            'index_names', ('index', 'Index', 'main', 'Main'))
621    index_names = converters.aslist(index_names)
622
623    if hide_extensions is None:
624        hide_extensions = global_conf.get(
625            'hide_extensions', ('.pyc', 'bak', 'py~'))
626    hide_extensions = converters.aslist(hide_extensions)
627
628    if ignore_extensions is None:
629        ignore_extensions = global_conf.get(
630            'ignore_extensions', ())
631    ignore_extensions = converters.aslist(ignore_extensions)
632    # There's no real way to set constructors currently...
633
634    return URLParser({}, directory, base_python_name,
635                     index_names=index_names,
636                     hide_extensions=hide_extensions,
637                     ignore_extensions=ignore_extensions,
638                     **constructor_conf)
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.