Author: blackbird Date: 2007-05-22 12:20:26 +0200 (Tue, 22 May 2007) New Revision: 5079 Added: trunk/sandbox/py-rest-doc/sphinx-web.py trunk/sandbox/py-rest-doc/sphinx/web/application.py trunk/sandbox/py-rest-doc/sphinx/web/util.py Removed: trunk/sandbox/py-rest-doc/sphinx/web/models.py trunk/sandbox/py-rest-doc/sphinx/web/urls.py trunk/sandbox/py-rest-doc/sphinx/web/views.py Modified: trunk/sandbox/py-rest-doc/sphinx/builder.py Log: added basic WSGI application for the documentation (and ported too many werkzeug features over, after the app is finished we should remove the unused) Modified: trunk/sandbox/py-rest-doc/sphinx/builder.py =================================================================== --- trunk/sandbox/py-rest-doc/sphinx/builder.py 2007-05-20 09:40:49 UTC (rev 5078) +++ trunk/sandbox/py-rest-doc/sphinx/builder.py 2007-05-22 10:20:26 UTC (rev 5079) @@ -451,9 +451,9 @@ ) -class DjangoHTMLBuilder(StandaloneHTMLBuilder): +class WebHTMLBuilder(StandaloneHTMLBuilder): """ - Builds HTML docs usable with the Django web-based doc server. + Builds HTML docs usable with the web-based doc server. """ # doesn't use the standalone specific options option_spec = Builder.option_spec @@ -486,9 +486,8 @@ fp.close() - builders = { 'html': StandaloneHTMLBuilder, - 'django': DjangoHTMLBuilder, + 'web': WebHTMLBuilder, # 'latex': LatexBuilder, } Added: trunk/sandbox/py-rest-doc/sphinx/web/application.py =================================================================== --- trunk/sandbox/py-rest-doc/sphinx/web/application.py 2007-05-20 09:40:49 UTC (rev 5078) +++ trunk/sandbox/py-rest-doc/sphinx/web/application.py 2007-05-22 10:20:26 UTC (rev 5079) @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +""" + sphinx.web.application + ~~~~~~~~~~~~~~~~~~~~~~ + + A simple WSGI application that serves an interactive version + of the python documentation. + + :copyright: 2007 by Armin Ronacher. + :license: Python license. +""" +from __future__ import with_statement +import cPickle as pickle +from os import path +from ..util import relative_uri +from .util import Request, Response, SharedDataMiddleware, NotFound, \ + render_template + + +special_urls = set(['index', 'genindex', 'modindex', 'search']) + + +def get_target_uri(source_filename): + if source_filename == 'index.rst': + return '' + if source_filename.endswith('/index.rst'): + return source_filename[:-9] # up to / + return source_filename[:-4] + '/' + + +class DocumentationApplication(object): + + def __init__(self, conf): + self.cache = {} + self.data_root = conf['data_root_path'] + + def get_page(self, req, url): + try: + filename, mtime, text = self.cache[url] + except KeyError: + pass + else: + if path.getmtime(filename) == mtime: + return Response(text) + + if url in special_urls: + filename = path.join(self.data_root, 'specials.pickle') + with open(filename, 'rb') as f: + context = pickle.load(f) + templatename = url + '.html' + + else: + for filename in [path.join(self.data_root, url) + '.fpickle', + path.join(self.data_root, url, 'index.fpickle')]: + if not path.exists(filename): + continue + with open(filename, 'rb') as f: + context = pickle.load(f) + break + else: + raise NotFound() + templatename = 'page.html' + + def relative_path_to(otheruri, resource=False): + if not resource: + otheruri = get_target_uri(otheruri) + return relative_uri(url + '/', otheruri) + context['pathto'] = relative_path_to + + text = render_template(templatename, context) + self.cache[url] = (filename, path.getmtime(filename), text) + return Response(text) + + def get_close_matches(self, req, url): + raise NotImplementedError(url) + + def __call__(self, environ, start_response): + req = Request(environ) + url = req.path.strip('/') or 'index' + try: + resp = self.get_page(req, url) + except NotFound: + resp = self.get_close_matches(req, url) + return resp(environ, start_response) + + +def make_app(conf=None): + app = DocumentationApplication(conf or {}) + app = SharedDataMiddleware(app, { + '/style': path.join(path.dirname(__file__), '..', 'style') + }) + return app Deleted: trunk/sandbox/py-rest-doc/sphinx/web/models.py Deleted: trunk/sandbox/py-rest-doc/sphinx/web/urls.py Added: trunk/sandbox/py-rest-doc/sphinx/web/util.py =================================================================== --- trunk/sandbox/py-rest-doc/sphinx/web/util.py 2007-05-20 09:40:49 UTC (rev 5078) +++ trunk/sandbox/py-rest-doc/sphinx/web/util.py 2007-05-22 10:20:26 UTC (rev 5079) @@ -0,0 +1,616 @@ +# -*- coding: utf-8 -*- +""" + sphinx.web.util + ~~~~~~~~~~~~~~~ + + To avoid further dependencies this module collects some of the + classes werkzeug provides and use in other views. + + :copyright: 2007 by Armin Ronacher, Georg Brandl. + :license: Python license. +""" +from __future__ import with_statement +import cgi +import tempfile +from os import path +from time import gmtime +from Cookie import SimpleCookie +from cStringIO import StringIO +from datetime import datetime +from jinja import Environment, FileSystemLoader + + +HTTP_STATUS_CODES = { + 100: 'CONTINUE', + 101: 'SWITCHING PROTOCOLS', + 102: 'PROCESSING', + 200: 'OK', + 201: 'CREATED', + 202: 'ACCEPTED', + 203: 'NON-AUTHORITATIVE INFORMATION', + 204: 'NO CONTENT', + 205: 'RESET CONTENT', + 206: 'PARTIAL CONTENT', + 207: 'MULTI STATUS', + 300: 'MULTIPLE CHOICES', + 301: 'MOVED PERMANENTLY', + 302: 'FOUND', + 303: 'SEE OTHER', + 304: 'NOT MODIFIED', + 305: 'USE PROXY', + 306: 'RESERVED', + 307: 'TEMPORARY REDIRECT', + 400: 'BAD REQUEST', + 401: 'UNAUTHORIZED', + 402: 'PAYMENT REQUIRED', + 403: 'FORBIDDEN', + 404: 'NOT FOUND', + 405: 'METHOD NOT ALLOWED', + 406: 'NOT ACCEPTABLE', + 407: 'PROXY AUTHENTICATION REQUIRED', + 408: 'REQUEST TIMEOUT', + 409: 'CONFLICT', + 410: 'GONE', + 411: 'LENGTH REQUIRED', + 412: 'PRECONDITION FAILED', + 413: 'REQUEST ENTITY TOO LARGE', + 414: 'REQUEST-URI TOO LONG', + 415: 'UNSUPPORTED MEDIA TYPE', + 416: 'REQUESTED RANGE NOT SATISFIABLE', + 417: 'EXPECTATION FAILED', + 500: 'INTERNAL SERVER ERROR', + 501: 'NOT IMPLEMENTED', + 502: 'BAD GATEWAY', + 503: 'SERVICE UNAVAILABLE', + 504: 'GATEWAY TIMEOUT', + 505: 'HTTP VERSION NOT SUPPORTED', + 506: 'VARIANT ALSO VARIES', + 507: 'INSUFFICIENT STORAGE', + 510: 'NOT EXTENDED' +} + + +templates_path = path.join(path.dirname(__file__), '..', 'templates') +jinja_env = Environment(loader=FileSystemLoader(templates_path, + use_memcache=True), + friendly_traceback=False) + + +def render_template(template_name, context): + tmpl = jinja_env.get_template(template_name) + return tmpl.render(context) + + +class lazy_property(object): + """ + Descriptor implementing a "lazy property", i.e. the function + calculating the property value is called only once. + """ + + def __init__(self, func, name=None, doc=None): + self._func = func + self._name = name or func.func_name + self.__doc__ = doc or func.__doc__ + + def __get__(self, obj, objtype=None): + if obj is None: + return self + value = self._func(obj) + setattr(obj, self._name, value) + return value + + +class _StorageHelper(cgi.FieldStorage): + """ + Helper class used by `BaseRequest` to parse submitted file and + form data. Don't use this class directly. + """ + + FieldStorageClass = cgi.FieldStorage + + def __init__(self, environ, get_stream): + cgi.FieldStorage.__init__(self, + fp=environ['wsgi.input'], + environ={ + 'REQUEST_METHOD': environ['REQUEST_METHOD'], + 'CONTENT_TYPE': environ['CONTENT_TYPE'], + 'CONTENT_LENGTH': environ['CONTENT_LENGTH'] + }, + keep_blank_values=True + ) + self.get_stream = get_stream + + def make_file(self, binary=None): + return self.get_stream() + + +class MultiDict(dict): + """ + A dict that takes a list of multiple values as only argument + in order to store multiple values per key. + """ + + def __init__(self, mapping=()): + if isinstance(mapping, MultiDict): + dict.__init__(self, mapping.lists()) + elif isinstance(mapping, dict): + tmp = {} + for key, value in mapping: + tmp[key] = [value] + dict.__init__(self, tmp) + else: + tmp = {} + for key, value in mapping: + tmp.setdefault(key, []).append(value) + dict.__init__(self, tmp) + + def __getitem__(self, key): + """ + Return the first data value for this key; + raises KeyError if not found. + """ + return dict.__getitem__(self, key)[0] + + def __setitem__(self, key, value): + """Set an item as list.""" + dict.__setitem__(self, key, [value]) + + def get(self, key, default=None): + """Return the default value if the requested data doesn't exist""" + try: + return self[key] + except KeyError: + return default + + def getlist(self, key): + """Return an empty list if the requested data doesn't exist""" + try: + return dict.__getitem__(self, key) + except KeyError: + return [] + + def setlist(self, key, new_list): + """Set new values for an key.""" + dict.__setitem__(self, key, list(new_list)) + + def setdefault(self, key, default=None): + if key not in self: + self[key] = default + else: + default = self[key] + return default + + def setlistdefault(self, key, default_list=()): + if key not in self: + default_list = list(default_list) + dict.__setitem__(self, key, default_list) + else: + default_list = self.getlist(key) + return default_list + + def items(self): + """ + Return a list of (key, value) pairs, where value is the last item in + the list associated with the key. + """ + return [(key, self[key]) for key in self.iterkeys()] + + lists = dict.items + + def values(self): + """Returns a list of the last value on every key list.""" + return [self[key] for key in self.iterkeys()] + + listvalues = dict.values + + def iteritems(self): + for key, values in dict.iteritems(self): + yield key, values[0] + + iterlists = dict.iteritems + + def itervalues(self): + for values in dict.itervalues(self): + yield values[0] + + iterlistvalues = dict.itervalues + + def copy(self): + """Return a shallow copy of this object.""" + return self.__class__(self) + + def update(self, other_dict): + """update() extends rather than replaces existing key lists.""" + if isinstance(other_dict, MultiDict): + for key, value_list in other_dict.iterlists(): + self.setlistdefault(key, []).extend(value_list) + elif isinstance(other_dict, dict): + for key, value in other_dict.items(): + self.setlistdefault(key, []).append(value) + else: + for key, value in other_dict: + self.setlistdefault(key, []).append(value) + + def pop(self, *args): + """Pop the first item for a list on the dict.""" + return dict.pop(self, *args)[0] + + def popitem(self): + """Pop an item from the dict.""" + item = dict.popitem(self) + return (item[0], item[1][0]) + + poplist = dict.pop + popitemlist = dict.popitem + + def __repr__(self): + tmp = [] + for key, values in self.iterlists(): + for value in values: + tmp.append((key, value)) + return '%s(%r)' % (self.__class__.__name__, tmp) + + +class Headers(object): + """ + An object that stores some headers. + """ + + def __init__(self, defaults=None): + self._list = [] + if isinstance(defaults, dict): + for key, value in defaults.iteritems(): + if isinstance(value, (tuple, list)): + for v in value: + self._list.append((key, v)) + else: + self._list.append((key, value)) + elif defaults is not None: + for key, value in defaults: + self._list.append((key, value)) + + def __getitem__(self, key): + ikey = key.lower() + for k, v in self._list: + if k.lower() == ikey: + return v + raise KeyError(key) + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + def getlist(self, key): + ikey = key.lower() + result = [] + for k, v in self._list: + if k.lower() == ikey: + result.append((k, v)) + return result + + def setlist(self, key, values): + del self[key] + self.addlist(key, values) + + def addlist(self, key, values): + self._list.extend(values) + + def lists(self, lowercased=False): + if not lowercased: + return self._list[:] + return [(x.lower(), y) for x, y in self._list] + + def iterlists(self, lowercased=False): + for key, value in self._list: + if lowercased: + key = key.lower() + yield key, value + + def iterkeys(self): + for key, _ in self.iterlists(): + yield key + + def itervalues(self): + for _, value in self.iterlists(): + yield value + + def keys(self): + return list(self.iterkeys()) + + def values(self): + return list(self.itervalues()) + + def __delitem__(self, key): + key = key.lower() + new = [] + for k, v in self._list: + if k != key: + new.append((k, v)) + self._list[:] = new + + remove = __delitem__ + + def __contains__(self, key): + key = key.lower() + for k, v in self._list: + if k.lower() == key: + return True + return False + + has_key = __contains__ + + def __iter__(self): + return iter(self._list) + + def add(self, key, value): + """add a new header tuple to the list""" + self._list.append((key, value)) + + def clear(self): + """clears all headers""" + del self._list[:] + + def set(self, key, value): + """remove all header tuples for key and add + a new one + """ + del self[key] + self.add(key, value) + + __setitem__ = set + + def to_list(self, charset): + """Create a str only list of the headers.""" + result = [] + for k, v in self: + if isinstance(v, unicode): + v = v.encode(charset) + else: + v = str(v) + result.append((k, v)) + return result + + def copy(self): + return self.__class__(self._list) + + def __repr__(self): + return '%s(%r)' % ( + self.__class__.__name__, + self._list + ) + + +class Request(object): + charset = 'ascii' + + def __init__(self, environ): + self.environ = environ + self.environ['werkzeug.request'] = self + + def _get_file_stream(self): + """Called to get a stream for the file upload. + + This must provide a file-like class with `read()`, `readline()` + and `seek()` methods that is both writeable and readable.""" + return tempfile.TemporaryFile('w+b') + + def _load_post_data(self): + """Method used internally to retrieve submitted data.""" + self._data = '' + post = [] + files = [] + if self.environ['REQUEST_METHOD'] in ('POST', 'PUT'): + storage = _StorageHelper(self.environ, self._get_file_stream) + for key in storage.keys(): + values = storage[key] + if not isinstance(values, list): + values = [values] + for item in values: + if getattr(item, 'filename', None) is not None: + fn = item.filename.decode(self.charset, 'ignore') + # fix stupid IE bug + if len(fn) > 1 and fn[1] == ':' and '\\' in fn: + fn = fn[fn.index('\\') + 1:] + files.append((key, FileStorage(key, fn, item.type, + item.length, item.file))) + else: + post.append((key, item.value.decode(self.charset, + 'ignore'))) + self._form = MultiDict(post) + self._files = MultiDict(files) + + def read(self, *args): + if not hasattr(self, '_buffered_stream'): + self._buffered_stream = StringIO(self.data) + return self._buffered_stream.read(*args) + + def readline(self, *args): + if not hasattr(self, '_buffered_stream'): + self._buffered_stream = StringIO(self.data) + return self._buffered_stream.readline(*args) + + def args(self): + """URL parameters""" + items = [] + qs = self.environ.get('QUERY_STRING', '') + for key, values in cgi.parse_qs(qs, True).iteritems(): + for value in values: + value = value.decode(self.charset, 'ignore') + items.append((key, value)) + return MultiDict(items) + args = lazy_property(args) + + def data(self): + """raw value of input stream.""" + if not hasattr(self, '_data'): + self._load_post_data() + return self._data + data = lazy_property(data) + + def form(self): + """form parameters.""" + if not hasattr(self, '_form'): + self._load_post_data() + return self._form + form = lazy_property(form) + + def files(self): + """File uploads.""" + if not hasattr(self, '_files'): + self._load_post_data() + return self._files + files = lazy_property(files) + + def cookies(self): + """Stored Cookies.""" + cookie = SimpleCookie() + cookie.load(self.environ.get('HTTP_COOKIE', '')) + result = {} + for key, value in cookie.iteritems(): + result[key] = value.value.decode(self.charset, 'ignore') + return result + cookies = lazy_property(cookies) + + def method(self): + """Request method.""" + return self.environ['REQUEST_METHOD'] + method = property(method, doc=method.__doc__) + + def path(self): + """Requested path.""" + path = '/' + (self.environ.get('PATH_INFO') or '').lstrip('/') + path = path.decode(self.charset, self.charset) + return path.replace('+', ' ') + path = lazy_property(path) + + +class Response(object): + charset = 'utf-8' + default_mimetype = 'text/html' + + def __init__(self, response=None, headers=None, status=200, mimetype=None): + if response is None: + self.response = [] + elif isinstance(response, basestring): + self.response = [response] + else: + self.response = iter(response) + if not headers: + self.headers = Headers() + elif isinstance(headers, Headers): + self.headers = headers + else: + self.headers = Headers(headers) + if mimetype is None and 'Content-Type' not in self.headers: + mimetype = self.default_mimetype + if mimetype is not None: + if 'charset=' not in mimetype and mimetype.startswith('text/'): + mimetype += '; charset=' + self.charset + self.headers['Content-Type'] = mimetype + self.status = status + self._cookies = None + + def write(self, value): + if not isinstance(self.response, list): + raise RuntimeError('cannot write to streaming response') + self.write = self.response.append + self.response.append(value) + + def set_cookie(self, key, value='', max_age=None, expires=None, + path='/', domain=None, secure=None): + if self._cookies is None: + self._cookies = SimpleCookie() + if isinstance(value, unicode): + value = value.encode(self.charset) + self._cookies[key] = value + if max_age is not None: + self._cookies[key]['max-age'] = max_age + if expires is not None: + if isinstance(expires, basestring): + self._cookies[key]['expires'] = expires + expires = None + elif isinstance(expires, datetime): + expires = expires.utctimetuple() + elif not isinstance(expires, (int, long)): + expires = gmtime(expires) + else: + raise ValueError('datetime or integer required') + month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', + 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][expires.tm_mon - 1] + day = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', + 'Friday', 'Saturday', 'Sunday'][expires.tm_wday] + date = '%02d-%s-%s' % ( + expires.tm_mday, month, str(expires.tm_year)[-2:] + ) + d = '%s, %s %02d:%02d:%02d GMT' % (day, date, expires.tm_hour, + expires.tm_min, expires.tm_sec) + self._cookies[key]['expires'] = d + if not path is None: + self._cookies[key]['path'] = path + if not domain is None: + self._cookies[key]['domain'] = domain + if not secure is None: + self._cookies[key]['secure'] = secure + + def delete_cookie(self, key): + if self._cookies is None: + self._cookies = SimpleCookie() + if not key in self._cookies: + self._cookies[key] = '' + self._cookies[key]['max-age'] = 0 + + def __call__(self, environ, start_response): + headers = self.headers.to_list(self.charset) + if self._cookies is not None: + for morsel in self._cookies.values(): + headers.append(('Set-Cookie', morsel.output(header=''))) + status = '%d %s' % (self.status, HTTP_STATUS_CODES[self.status]) + + charset = self.charset or 'ascii' + start_response(status, headers) + for item in self.response: + if isinstance(item, unicode): + yield item.encode(charset) + else: + yield str(item) + + +class SharedDataMiddleware(object): + """ + Redirects calls to an folder with static data. + """ + + def __init__(self, app, exports): + self.app = app + self.exports = exports + + def serve_file(self, filename, start_response): + from mimetypes import guess_type + guessed_type = guess_type(filename) + if guessed_type[0] is None: + mime_type = 'text/plain' + else: + mime_type = guessed_type[0] + start_response('200 OK', [('Content-Type', mime_type)]) + with file(filename, 'rb') as f: + result = f.read() + return iter([result]) + + def __call__(self, environ, start_response): + p = environ.get('PATH_INFO', '') + for search_path, file_path in self.exports.iteritems(): + if not search_path.endswith('/'): + search_path += '/' + if p.startswith(search_path): + real_path = path.join(file_path, p[len(search_path):]) + if path.exists(real_path) and path.isfile(real_path): + return self.serve_file(real_path, start_response) + return self.app(environ, start_response) + + +class NotFound(Exception): + """ + Raise to display the 404 error page. + """ Deleted: trunk/sandbox/py-rest-doc/sphinx/web/views.py Added: trunk/sandbox/py-rest-doc/sphinx-web.py =================================================================== --- trunk/sandbox/py-rest-doc/sphinx-web.py 2007-05-20 09:40:49 UTC (rev 5078) +++ trunk/sandbox/py-rest-doc/sphinx-web.py 2007-05-22 10:20:26 UTC (rev 5079) @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +""" + Sphinx - Python documentation webserver + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2007 by Armin Ronacher. + :license: Python license. +""" +import sys +from wsgiref.simple_server import make_server +from sphinx.web.application import make_app + +if __name__ == '__main__': + if len(sys.argv) != 2: + print 'usage: %s <doc_root>' % sys.argv[0] + sys.exit(-1) + app = make_app({'data_root_path': sys.argv[1]}) + + #XXX: make this configurable + try: + from werkzeug.debug import DebuggedApplication + except ImportError: + pass + else: + app = DebuggedApplication(app, True) + + srv = make_server('localhost', 3000, app) + try: + print 'Running on http://%s:%d/' % srv.socket.getsockname() + srv.serve_forever() + except KeyboardInterrupt: + pass |