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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.