diff --git a/doc/conf.py b/doc/conf.py index 8a3a155..1a8bea0 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -25,7 +25,8 @@ import sys, os # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -86,6 +87,9 @@ pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +intersphinx_mapping = { + 'webob': ('http://docs.webob.org/en/latest/', None) +} # -- Options for HTML output --------------------------------------------------- diff --git a/doc/index.rst b/doc/index.rst index b06ec36..1385b29 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -14,6 +14,10 @@ Contents: :maxdepth: 2 rationale + +.. toctree:: + :maxdepth: 1 + reference/index *Milla* is released under the terms of the `Apache License, version 2.0`_. diff --git a/doc/reference/milla.rst b/doc/reference/milla.rst new file mode 100644 index 0000000..5800ca2 --- /dev/null +++ b/doc/reference/milla.rst @@ -0,0 +1,7 @@ +===== +milla +===== + +.. automodule:: milla + :members: + :inherited-members: diff --git a/src/milla/__init__.py b/src/milla/__init__.py index 2b431c0..0d0c2c8 100644 --- a/src/milla/__init__.py +++ b/src/milla/__init__.py @@ -20,6 +20,13 @@ from milla.auth.decorators import * from webob.exc import * from webob.request import * from webob.response import * +import urllib +try: + import urllib.parse +except ImportError: + import urlparse + urllib.parse = urlparse + def allow(*methods): '''Specify the allowed HTTP verbs for a controller callable @@ -35,3 +42,75 @@ def allow(*methods): func.allowed_methods = methods return func return wrapper + + +class Response(Response): + ''':py:class:`WebOb Response ` with minor tweaks + ''' + + +class Request(Request): + ''':py:class:`WebOb Request ` with minor tweaks + ''' + + ResponseClass = Response + + def relative_url(self, other_url, to_application=True, path_only=True, + **vars): + '''Create a new URL relative to the request URL + + :param other_url: relative path to join with the request URL + :param to_application: If true, generated URL will be relative + to the application's root path, otherwise relative to the + server root + :param path_only: If true, scheme and host will be omitted + + Any other keyword arguments will be encoded and appended to the URL + as querystring arguments. + ''' + + url = super(Request, self).relative_url(other_url, to_application) + if path_only: + url = urllib.parse.urlsplit(url).path + if vars: + url += '?' + urllib.urlencode(vars) + return url + + def static_resource(self, path): + '''Return a URL to the given static resource + + This method combines the defined static resource root URL with the + given path to construct a complete URL to the given resource. The + resource root should be defined in the application configuration + dictionary, under the name ``milla.static_root``, for example:: + + app = milla.Application(dispatcher) + app.config.update({ + 'milla.static_root': '/static/' + }) + + Then, calling ``static_resource`` on a :py:class:`Request` object + (i.e. inside a controller callable) would combine the given path + with ``/static/``, like this:: + + request.static_resource('/images/foo.png') + + would return ``/static/images/foo.png``. + + If no ``milla.static_root`` key is found in the configuration + dictionary, the path will be returned unaltered. + + :param path: Path to the resource, relative to the defined root + ''' + + try: + root = self.config['milla.static_root'] + except KeyError: + return path + + if path.startswith('/'): + path = path[1:] + if not root.endswith('/'): + root += '/' + + return urllib.parse.urljoin(root, path) diff --git a/src/milla/app.py b/src/milla/app.py index e57eed6..d7117e1 100644 --- a/src/milla/app.py +++ b/src/milla/app.py @@ -1,11 +1,11 @@ # Copyright 2011 Dustin C. Hatch -# +# # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -31,19 +31,19 @@ __all__ = ['Application'] class Application(object): '''Represents a Milla web application - + Constructing an ``Application`` instance needs a dispatcher, or alternatively, a root object that will be passed to a new - :py:class:`milla.dispatch.traversal.Traverser`. - + :py:class:`milla.dispatch.traversal.Traverser`. + :param root: A root object, passed to a traverser, which is automatically created if a root is given - :param dispatcher: An object implementing the dispatcher protocol - + :param dispatcher: An object implementing the dispatcher protocol + ``Application`` instances are WSGI applications. - + .. py:attribute:: config - + A mapping of configuration settings. For each request, the configuration is copied and assigned to ``request.config``. ''' @@ -65,7 +65,7 @@ class Application(object): else: return HTTPNotFound()(environ, start_response) - request = webob.Request(environ) + request = milla.Request(environ) request.config = self.config.copy() # Sometimes, hacky applications will try to "emulate" some HTTP @@ -88,7 +88,7 @@ class Application(object): start_response) start_response_wrapper = StartResponseWrapper(start_response) - request.start_response = start_response_wrapper + request.start_response = start_response_wrapper try: # If the callable has an __before__ attribute, call it if hasattr(func, '__before__'): @@ -136,11 +136,11 @@ class Application(object): return response.app_iter class StartResponseWrapper(): - + def __init__(self, start_response): self.start_response = start_response self.called = False - + def __call__(self, *args, **kwargs): self.called = True - return self.start_response(*args, **kwargs) \ No newline at end of file + return self.start_response(*args, **kwargs) diff --git a/src/milla/controllers.py b/src/milla/controllers.py index 576adc4..7e59fa0 100644 --- a/src/milla/controllers.py +++ b/src/milla/controllers.py @@ -23,7 +23,8 @@ from one or more of these classes can make things significantly easier. :Updater: $Author$ ''' -import milla +import datetime +import milla.util import pkg_resources @@ -53,6 +54,9 @@ class FaviconController(Controller): used as the favicon, defaults to 'image/x-icon' (Windows ICO format) ''' + #: Number of days in the future to set the cache expiration for the icon + EXPIRY_DAYS = 365 + def __init__(self, icon=None, content_type='image/x-icon'): if icon: try: @@ -72,4 +76,7 @@ class FaviconController(Controller): response = milla.Response() response.app_iter = self.icon response.headers['Content-Type'] = self.content_type + expires = (datetime.datetime.utcnow() + + datetime.timedelta(days=self.EXPIRY_DAYS)) + response.headers['Expires'] = milla.util.http_date(expires) return response diff --git a/src/milla/dispatch/routing.py b/src/milla/dispatch/routing.py index ff4702f..fcd5f92 100644 --- a/src/milla/dispatch/routing.py +++ b/src/milla/dispatch/routing.py @@ -24,12 +24,7 @@ import functools import milla import re import sys -import urllib -try: - import urllib.parse -except ImportError: - import urlparse - urllib.parse = urlparse +import warnings class Router(object): '''A dispatcher that maps arbitrary paths to controller callables @@ -224,11 +219,20 @@ class Generator(object): A common pattern is to wrap this in a stub function:: url = Generator(request).generate + + .. deprecated:: 0.2 + Use :py:meth:`milla.Request.relative_url` instead. ''' def __init__(self, request, path_only=True): self.request = request self.path_only = path_only + warnings.warn( + 'Use of Generator is deprecated; ' + 'use milla.Request.relative_url instead', + DeprecationWarning, + stacklevel=2 + ) def generate(self, *segments, **vars): '''Combines segments and the application's URL into a new URL @@ -238,10 +242,7 @@ class Generator(object): while path.startswith('/'): path = path[1:] - url = self.request.relative_url(path, to_application=True) - if self.path_only: - split = urllib.parse.urlsplit(url) - url = split.path - if vars: - url += '?' + urllib.urlencode(vars) - return url + return self.request.relative_url(path, + to_application=True, + path_only=self.path_only, + **vars) diff --git a/src/milla/util.py b/src/milla/util.py index d996821..29fbff0 100644 --- a/src/milla/util.py +++ b/src/milla/util.py @@ -21,6 +21,10 @@ Please give me a docstring! :Updater: $Author$ ''' +from wsgiref.handlers import format_date_time +import datetime +import time + def asbool(val): '''Test a value for truth @@ -46,4 +50,19 @@ def asbool(val): pass if val in ('false', 'no', 'f', 'n', 'off', '0'): return False - return True \ No newline at end of file + return True + + +def http_date(date): + '''Format a datetime object as a string in RFC 1123 format + + This function returns a string representing the date according to + RFC 1123. The string returned will always be in English, as + required by the specification. + + :param date: A :py:class:`datetime.datetime` object + :return: RFC 1123-formatted string + ''' + + stamp = time.mktime(date.timetuple()) + return format_date_time(stamp)