Re: pyblosxom 3000
Steven Armstrong <[email protected]>
| Newsgroups | gmane.comp.web.pyblosxom.devel |
|---|---|
| Message-ID | <[email protected]> |
Ok, here goes. I've changed quite a few things. It's possible that this breaks stuff that (a) I don't have installed and therefor don't know about, (b) haven't tested/thought of. Some changes might look strange/overkill to someone looking at it solely from the CGI point of view. But when looking at it from a twisted, mod_python, whatever point of view I think it's actually pretty cool :-) The patch changes the following files: /pyblosxom/web/pyblosxom.cgi /pyblosxom/Pyblosxom/pyblosxom.py /pyblosxom/Pyblosxom/tools.py /pyblosxom/Pyblosxom/renderers/base.py What I've tested sucessfully: - normal CGI usage - static rendering, static incremental rendering - testing installation plugins I had loaded while testing this: "pystaticfile", "breadcrumbs", "pycalendar", "pycategories", "comments", "rss2renderer", "w3cdate", "rememberdates", "lupy_search", "abstract", "session", "nospam", "xmlrpc", "xmlrpc_blogger", "xmlrpc_metaweblog", "xmlrpc_editor", "flow", "rewrite", "cat" xmlrpc needed to be patched to not read from sys.stdin. rememberdates had to patched to work with static rendering. What say you? cheers Steven
pyblosxom_1.2.diff
(text/plain, 19.8 KB)
Index: Pyblosxom/pyblosxom.py
===================================================================
RCS file: /cvsroot/pyblosxom/pyblosxom/Pyblosxom/pyblosxom.py,v
retrieving revision 1.52
diff -u -r1.52 pyblosxom.py
--- Pyblosxom/pyblosxom.py 5 Jan 2005 21:07:56 -0000 1.52
+++ Pyblosxom/pyblosxom.py 20 Jan 2005 14:25:20 -0000
@@ -2,11 +2,19 @@
This is the main module for PyBlosxom functionality. PyBlosxom's setup
and default handlers are defined here.
"""
+
+# Python imports
from __future__ import nested_scopes
-import os, time, re, sys, StringIO
+import os, time, re, sys
+import cgi
+try: from cStringIO import StringIO
+except ImportError: from StringIO import StringIO
+
+# Pyblosxom imports
import tools
from entries.fileentry import FileEntry
+
VERSION = "1.1"
VERSION_DATE = VERSION + " 01/05/2005"
VERSION_SPLIT = tuple(VERSION.split('.'))
@@ -18,14 +26,23 @@
request through all the steps until the output is rendered and
we're complete.
"""
- def __init__(self, request):
+ def __init__(self, config, environ, data={}):
"""
- Sets the request.
+ Sets configuration and environment.
+ Creates the L{Request} object.
+
+ @param config: A dict containing the configuration variables.
+ @type config: dict
+
+ @param environ: A dict containing the environment variables.
+ @type environ: dict
- @param request: A L{Pyblosxom.pyblosxom.Request} object
- @type request: L{Pyblosxom.pyblosxom.Request} object
+ @param data: A dict containing data variables.
+ @type data: dict
"""
- self._request = request
+ config['pyblosxom_name'] = "pyblosxom"
+ config['pyblosxom_version'] = VERSION_DATE
+ self._request = Request(config, environ, data)
def initialize(self):
"""
@@ -45,7 +62,8 @@
# Get our URL and configure the base_url param
if pyhttp.has_key('SCRIPT_NAME'):
if not config.has_key('base_url'):
- config['base_url'] = 'http://%s%s' % (pyhttp['HTTP_HOST'], pyhttp['SCRIPT_NAME'])
+ # allow http and https
+ config['base_url'] = '%s://%s%s' % (pyhttp['wsgi.url_scheme'], pyhttp['HTTP_HOST'], pyhttp['SCRIPT_NAME'])
else:
config['base_url'] = config.get('base_url', '')
@@ -65,6 +83,26 @@
mappingfunc=lambda x,y:y,
defaultfunc=lambda x:x)
+ def getRequest(self):
+ """
+ Returns the L{Request} object.
+
+ @returns: the request object
+ @rtype: L{Request}
+ """
+ return self._request
+
+ def getResponse(self):
+ """
+ Returns the L{Response} object which handles all output
+ related functionality.
+
+ @see: L{Response}
+ @returns: the reponse object
+ @rtype: L{Response}
+ """
+ return self._request.getResponse()
+
def run(self):
"""
Main loop for pyblosxom. This should be called _after_
@@ -262,6 +300,29 @@
tools.render_url(config, url, q)
+ def testInstallation(self):
+ test_installation(self._request)
+
+
+class EnvDict(dict):
+ """
+ Wrapper arround a dict to provide a backwards compatible way
+ to get the L{form<cgi.FieldStorage>} with syntax as:
+ request.getHttp()['form']
+ instead of:
+ request.getForm()
+ """
+ def __init__(self, request, env):
+ self._request = request
+ for key in env:
+ self[key] = env[key]
+
+ def __getitem__(self, key):
+ if key == "form":
+ return self._request.getForm()
+ else:
+ return dict.__getitem__(self, key)
+
class Request:
"""
This class holds the PyBlosxom request. It holds configuration
@@ -273,18 +334,106 @@
PyBlosxom instance which will do further manipulation on the
Request instance.
"""
- def __init__(self):
+ def __init__(self, config, environ, data):
+ """
+ Sets configuration and environment.
+ Creates the L{Response} object which handles all output
+ related functionality.
+
+ @param config: A dict containing the configuration variables.
+ @type config: dict
+
+ @param environ: A dict containing the environment variables.
+ @type environ: dict
+
+ @param data: A dict containing data variables.
+ @type data: dict
+ """
# this holds configuration data that the user changes
# in config.py
- self._configuration = {}
+ self._configuration = config
# this holds HTTP/CGI oriented data specific to the request
# and the environment in which the request was created
- self._http = {}
+ #self._http = environ
+ self._http = EnvDict(self, environ)
# this holds run-time data which gets created and transformed
# by pyblosxom during execution
- self._data = {}
+ self._data = data
+
+ # this holds the input stream
+ self._in = environ['wsgi.input']
+
+ # this holds the FieldStorage instance.
+ # initialized when request.getForm is called the first time
+ self._form = None
+
+ # create and set the Response
+ self.setResponse(Response(self))
+
+ # if a input stream is given, copy it's read related methods
+ # to the Request object.
+ if self._in:
+ self._copy_members()
+
+ def _copy_members(self):
+ """
+ Copies methods from the underlying input stream to the request object.
+ """
+ props = ['__iter__', 'next', 'read', 'readline', 'readlines', 'seek', 'tell']
+ for prop in props:
+ setattr(self, prop, getattr(self._in, prop))
+
+ def setResponse(self, response):
+ """
+ Sets the L{Response} object.
+
+ @param response: A pyblosxom Response object
+ @type response: L{Response}
+ """
+ self._response = response
+ # for backwards compatibility
+ self.getConfiguration()['stdoutput'] = response
+
+ def getResponse(self):
+ """
+ Returns the L{Response} object which handles all output
+ related functionality.
+
+ @returns: L{Response}
+ """
+ return self._response
+
+ def __getForm(self):
+ """
+ Parses and returns the form data submitted by the client.
+ The input stream self._in is consumed/empty after a call
+ to cgi.FieldStorage. If the created FieldStorage instance
+ has a 'file' member this is set as the new input stream.
+
+ @returns: L{cgi.FieldStorage}
+ """
+ form = cgi.FieldStorage(fp=self._in, environ=self._http, keep_blank_values=0)
+ if form.file:
+ #self._in = environ['wsgi.input'] = form.file
+ self._in = form.file
+ self._in.seek(0)
+ self._copy_members()
+ return form
+
+ def getForm(self):
+ """
+ Returns the form data submitted by the client.
+ The L{form<cgi.FieldStorage>} instance is created
+ only when requested to prevent overhead and unnecessary
+ consumption of the input stream.
+
+ @returns: L{cgi.FieldStorage}
+ """
+ if self._form == None:
+ self._form = self.__getForm()
+ return self._form
def getConfiguration(self):
"""
@@ -386,11 +535,121 @@
return "Request"
+class Response(object):
+ """
+ Response class to handle all output related tasks in one place.
+
+ This class is basically a wrapper arround a StringIO instance.
+ It also provides methods for managing http headers.
+ """
+
+ def __init__(self, request):
+ """
+ Sets the L{Request} object that leaded to this response.
+ Creates a L{StringIO} that is used as a output buffer.
+
+ @param request: request object.
+ @type request: L{Request}
+ """
+ self._request = request
+ self._out = StringIO()
+ self._headers_sent = False
+ self.headers = {}
+ self.status = "200 OK"
+ self._copy_members()
+
+ def _copy_members(self):
+ """
+ Copies methods from the underlying output buffer to the response object.
+ """
+ props = ['__iter__', 'close', 'flush', 'next',
+ 'read', 'readline', 'readlines', 'seek', 'tell',
+ 'write', 'writelines']
+ for prop in props:
+ #if not hasattr(self, prop):
+ setattr(self, prop, getattr(self._out, prop))
+
+ def setStatus(self, status):
+ """
+ Sets the status code for this response.
+
+ @param status: A status code and message like '200 OK'.
+ @type status: str
+ """
+ self.status = status
+
+ def getStatus(self):
+ """
+ Returns the status code and message of this response.
+
+ @returns: str
+ """
+ return self.status
+
+ def addHeader(self, *args):
+ """
+ Populates the HTTP header with lines of text.
+ Sets the status code on this response object if the given argument
+ list containes a 'Status' header.
+
+ @param args: Paired list of headers
+ @type args: argument lists
+ @raises ValueError: This happens when the parameters are not correct
+ """
+ args = list(args)
+ if not len(args) % 2:
+ while args:
+ key = args.pop(0).strip()
+ if key.find(' ') != -1 or key.find(':') != -1:
+ raise ValueError, 'There should be no spaces in header keys'
+ value = args.pop(0).strip()
+
+ if key.lower() == "status":
+ self.setStatus(str(value))
+ else:
+ self.headers.update({key: str(value)})
+ else:
+ raise ValueError, 'Headers recieved are not in the correct form'
+
+ def getHeaders(self):
+ """
+ Returns the headers of this response.
+
+ @returns: the HTTP response headers
+ @rtype: dict
+ """
+ return self.headers
+
+ def sendHeaders(self, out):
+ """
+ Send HTTP Headers to the given output stream.
+
+ @param out: File like object
+ @type out: file
+ """
+ out.write("Status: %s\n" % self.status)
+ out.write('\n'.join(['%s: %s' % (x, self.headers[x])
+ for x in self.headers.keys()]))
+ out.write('\n\n')
+ self._headers_sent = True
+
+ def sendBody(self, out):
+ """
+ Send the response body to the given output stream.
+
+ @param out: File like object
+ @type out: file
+ """
+ #if not self._headers_sent:
+ # self.sendHeaders(out)
+ self.seek(0)
+ out.write(self.read())
+
+
def blosxom_handler(request):
"""
This is the default blosxom handler.
"""
- import cgi
config = request.getConfiguration()
data = request.getData()
@@ -415,9 +674,6 @@
data['renderer'] = r
- if not request.getHttp().has_key("form"):
- request.addHttp( {"form": cgi.FieldStorage() } )
-
# process the path info to determine what kind of blog entry(ies)
# this is
tools.run_callback("pathinfo",
@@ -594,7 +850,7 @@
data = request.getData()
pyhttp = request.getHttp()
- form = pyhttp["form"]
+ form = request.getForm()
data['flavour'] = (form.has_key('flav') and form['flav'].value or
config.get('defaultFlavour', 'html'))
@@ -772,6 +1028,22 @@
print "This must be done before we can go further. Exiting."
return
+# # check datadir permissions
+# try:
+# fn = os.path.join(config["datadir"], "_test_installation_read_write_access.txt")
+# f = file(fn, "a")
+# f.write("testing installation ...\n")
+# f.write("you can safely delete this file\n")
+# f.close()
+# os.remove(fn)
+# except IOError:
+# print "Permissions on datadir '%s' are not correct." % config["datadir"]
+# print "You musst have read/write access within this directory."
+# print "The server running your blog must have at least read access."
+# print ""
+# print "This must be done before we can go further. Exiting."
+# return
+
print "PASS: datadir is fine."
print "------"
Index: Pyblosxom/tools.py
===================================================================
RCS file: /cvsroot/pyblosxom/pyblosxom/Pyblosxom/tools.py,v
retrieving revision 1.38
diff -u -r1.38 tools.py
--- Pyblosxom/tools.py 8 Jan 2005 20:56:57 -0000 1.38
+++ Pyblosxom/tools.py 20 Jan 2005 14:25:21 -0000
@@ -488,7 +488,7 @@
return mycache
-
+_logger_registry = {}
def make_logger(filename):
"""
Create a logging function called log, which logs to the supplied filename
@@ -511,16 +511,21 @@
f.write("\n")
f.close()
else:
- logger = logging.getLogger('trackback')
+ global _logger_registry
# if all loggers have the same name,
# everything is logged to all files.
logger_name = os.path.splitext(os.path.basename(filename))[0]
- logger = logging.getLogger(logger_name)
- hdlr = logging.FileHandler(filename)
- formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
- hdlr.setFormatter(formatter)
- logger.addHandler(hdlr)
- logger.setLevel(logging.INFO)
+ # only add one handler per logger
+ if not logger_name in _logger_registry:
+ _logger = logging.getLogger(logger_name)
+ hdlr = logging.FileHandler(filename)
+ formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
+ hdlr.setFormatter(formatter)
+ _logger.addHandler(hdlr)
+ _logger.setLevel(logging.INFO)
+ _logger_registry[logger_name] = _logger
+
+ logger = _logger_registry[logger_name]
def log(*args):
# adjusted to match the 'manual' log func
@@ -583,12 +588,9 @@
if not staticdir:
raise Exception("You must set static_dir in your config file.")
- from Pyblosxom import pyblosxom
-
- oldstdout = sys.stdout
+ from Pyblosxom.pyblosxom import PyBlosxom
- req = pyblosxom.Request()
- req.addHttp({
+ env = {
"HTTP_USER_AGENT": "static renderer",
"REQUEST_METHOD": "GET",
"HTTP_HOST": "localhost",
@@ -597,32 +599,25 @@
"REQUEST_URI": pathinfo + "?" + querystring,
"PATH_INFO": pathinfo,
"HTTP_REFERER": "",
- "REMOTE_ADDR": ""
- })
- req.addConfiguration(cdict)
- req.addData( {"STATIC": 1} )
-
- buffer = StringIO.StringIO()
- sys.stdout = buffer
- p = pyblosxom.PyBlosxom(req)
+ "REMOTE_ADDR": "",
+ "SCRIPT_NAME": "",
+ "wsgi.errors": sys.stderr,
+ "wsgi.input": None
+ }
+ data = {"STATIC": 1}
+ p = PyBlosxom(cdict, env, data)
p.run()
- sys.stdout = oldstdout
+ response = p.getResponse()
+ response.seek(0)
fn = os.path.normpath(staticdir + os.sep + pathinfo)
if not os.path.isdir(os.path.dirname(fn)):
os.makedirs(os.path.dirname(fn))
- # this is cheesy--we need to remove the HTTP headers
- # from the file.
- output = buffer.getvalue().splitlines()
- while 1:
- if len(output[0].strip()) == 0:
- break
- output.pop(0)
- output.pop(0)
-
+ # by using the response object the cheesy part of removing
+ # the HTTP headers from the file is history.
f = open(fn, "w")
- f.write("\n".join(output))
+ f.write(response.read())
f.close()
@@ -664,7 +659,7 @@
# that accompanies the win32 modules.
#
# Author: Jonathan Feinberg <[email protected]>
-# Version: $Id: tools.py,v 1.38 2005/01/08 20:56:57 willhelm Exp $
+# Version: $Id: tools.py,v 1.1 2005/01/16 13:49:30 sar Exp $
if os.name == 'nt':
import win32con
Index: Pyblosxom/renderers/base.py
===================================================================
RCS file: /cvsroot/pyblosxom/pyblosxom/Pyblosxom/renderers/base.py,v
retrieving revision 1.7
diff -u -r1.7 base.py
--- Pyblosxom/renderers/base.py 8 Dec 2004 01:37:55 -0000 1.7
+++ Pyblosxom/renderers/base.py 20 Jan 2005 14:25:22 -0000
@@ -123,12 +123,12 @@
def showHeaders(self):
"""
- Show HTTP Headers. Override this if your renderer uses headers in a
- different way
+ Updated the headers of the L{Response<Pyblosxom.pyblosxom.Response>} instance.
+ This is just for backwards compatibility.
"""
- self.write('\n'.join(['%s: %s' % (x, self._header[x])
- for x in self._header.keys()]))
- self.write('\n\n')
+ response = self._request.getResponse()
+ for k,v in self._header.items():
+ response.addHeader(k,v)
def render(self, header = 1):
Index: web/pyblosxom.cgi
===================================================================
RCS file: /cvsroot/pyblosxom/pyblosxom/web/pyblosxom.cgi,v
retrieving revision 1.9
diff -u -r1.9 pyblosxom.cgi
--- web/pyblosxom.cgi 16 Dec 2004 19:33:32 -0000 1.9
+++ web/pyblosxom.cgi 20 Jan 2005 14:25:22 -0000
@@ -1,51 +1,58 @@
#!/usr/bin/env python
+#!/path/to/python -u
+# -u turns off character translation to allow transmission
+# of gzip compressed content on Windows and OS/2
+
# Uncomment this if something goes wrong (for debugging)
#import cgitb; cgitb.enable()
# Settings are now in config.py, you should disable access to it by htaccess
# (make it executable or deny access)
-import config
+from config import py as cfg
# If the user defined a "codebase" property in their config file,
# then we insert that into our sys.path because that's where the
# PyBlosxom installation is.
-if config.py.has_key("codebase"):
+if cfg.has_key("codebase"):
import sys
- sys.path.insert(0, config.py["codebase"])
+ sys.path.insert(0, cfg["codebase"])
if __name__ == '__main__':
- import Pyblosxom.pyblosxom
- from Pyblosxom.pyblosxom import Request, test_installation, PyBlosxom
- import os, sys
-
- config.py["pyblosxom_name"] = "pyblosxom"
- config.py["pyblosxom_version"] = Pyblosxom.pyblosxom.VERSION_DATE
- req = Request()
- req.addConfiguration(config.py)
+ import os, sys
+ from Pyblosxom.pyblosxom import PyBlosxom
+
+ env = {}
+ # names taken from wsgi instead of inventing something new
+ env['wsgi.input'] = sys.stdin
+ env['wsgi.errors'] = sys.stderr
+ env['wsgi.url_scheme'] = "http"
+ if os.environ.get("HTTPS") in ('yes','on','1'):
+ env['wsgi.url_scheme'] = "https"
- d = {}
for mem in ["HTTP_HOST", "HTTP_USER_AGENT", "HTTP_REFERER", "PATH_INFO",
"QUERY_STRING", "REMOTE_ADDR", "REQUEST_METHOD", "REQUEST_URI",
"SCRIPT_NAME", "HTTP_IF_NONE_MATCH", "HTTP_IF_MODIFIED_SINCE",
- "HTTP_COOKIE"]:
- d[mem] = os.environ.get(mem, "")
- req.addHttp(d)
+ "HTTP_COOKIE", "CONTENT_LENGTH", "HTTP_ACCEPT", "HTTP_ACCEPT_ENCODING"]:
+ env[mem] = os.environ.get(mem, "")
+
+ p = PyBlosxom(cfg, env)
- if not os.environ.get("REQUEST_METHOD", ""):
+ if not env.get("REQUEST_METHOD", ""):
if len(sys.argv) > 1 and sys.argv[1] == "--static":
if "--incremental" in sys.argv:
incremental = 1
else:
incremental = 0
- p = PyBlosxom(req)
p.runStaticRenderer(incremental)
else:
- test_installation(req)
+ p.testInstallation()
else:
- p = PyBlosxom(req)
p.run()
+ response = p.getResponse()
+ response.sendHeaders(sys.stdout)
+ response.sendBody(sys.stdout)
# vim: shiftwidth=4 tabstop=4 expandtab