Edgewall Software

Changes between Version 1 and Version 2 of TracModPython2.7


Ignore:
Timestamp:
Dec 13, 2007, 7:35:49 PM (15 years ago)
Author:
fg@…
Comment:

Updated the instruction to trac 0.11

Legend:

Unmodified
Added
Removed
Modified
  • TracModPython2.7

    v1 v2  
    11= Trac and mod_python 2.7 =
    22
    3 Trac was written to use Apache 2, but if you want to use Apache 1.3, you couldn't use [http://www.modpython.org/ mod_python] because [http://www.modpython.org/ mod_python] 3 only runs on Apache 2. Since this wasn't an option in my case, I patched [http://www.modpython.org/ mod_python] 2.7.11 and also made a couple of small changes to Trac to make it work.
     3TracModPython setup is for Apache 2, but it may be possible to work with Apache 1.3.
    44
    5 There are two patches, one for [http://www.modpython.org/ mod_python] 2.7.11, and one for Trac 0.8.4. They have only been tested on Mac OS X, with these specific versions, so your mileage may vary.
     5The instructions below have worked on MacOS 10.4
    66
    7 Here's the [http://www.modpython.org/ mod_python] patch.
     7== Installing mod_python ==
     8
     9The instruction below work for:
     10  * the Apache installation that comes with MacOS 10.4
     11  * Python 2.4.3 installed using Fink
     12  * Trac 0.11dev-r6202
     13
     14First, make sure that the python in the {{{$PATH}}} is the python you want to use (you can revert it later):
     15{{{
     16sudo mv /usr/bin/python /usr/bin/python.back
     17sudo ln -s /sw/bin/python2.4 /usr/bin/python
     18}}}
     19
     20Then download and build the [http://www.modpython.org/ mod_python] 2.7 module. Use the most recent 2.7 version, mine was 2.7.11:
     21{{{
     22curl http://www.axint.net/apache/httpd/modpython/mod_python-2.7.11.tgz > mod_python-2.7.11.tgz
     23tar -xzf mod_python-2.7.11.tgz
     24cd mod_python-2.7.11
     25./configure -with-apxs=/usr/sbin/apxs
     26}}}
     27
     28At this stage, I had to edit the build settings, because once installed, I was getting an warning. So in {{{src/Makefile}}} I changed:
    829
    930{{{
    10 diff -r -u mod_python-2.7.11.orig/lib/python/mod_python/util.py mod_python-2.7.11/lib/python/mod_python/util.py
    11 --- mod_python-2.7.11.orig/lib/python/mod_python/util.py        2000-12-13 23:45:48.000000000 +0000
    12 +++ mod_python-2.7.11/lib/python/mod_python/util.py     2005-09-16 12:58:07.000000000 +0100
    13 @@ -47,6 +47,9 @@
    14  import string
    15  import StringIO
    16  
    17 +from types import *
    18 +from exceptions import *
    19 +
    20  parse_qs = apache.parse_qs
    21  parse_qsl = apache.parse_qsl
    22  
    23 @@ -95,6 +98,14 @@
    24      def __del__(self):
    25          self.file.close()
    26  
    27 +class StringField(str):
    28 +    """ This class is basically a string with
    29 +    a value attribute for compatibility with std lib cgi.py
    30 +    """
    31 +
    32 +    def __init__(self, str=""):
    33 +       str.__init__(self, str)
    34 +       self.value = self.__str__()
    35  
    36  class FieldStorage:
    37  
    38 @@ -239,10 +250,10 @@
    39          found = []
    40          for item in self.list:
    41              if item.name == key:
    42 -                if isinstance(item.file, StringIO.StringIO):
    43 -                    found.append(item.value)
    44 -                else:
    45 -                    found.append(item)
    46 +               if isinstance(item.file, FileType):
    47 +                   found.append(item)
    48 +               else:
    49 +                   found.append(StringField(item.value))
    50          if not found:
    51              raise KeyError, key
    52          if len(found) == 1:
    53 @@ -250,6 +261,12 @@
    54          else:
    55              return found
    56  
    57 +    def get(self, key, default):
    58 +       try:
    59 +           return self.__getitem__(key)
    60 +       except KeyError:
    61 +           return default
    62 +
    63      def keys(self):
    64          """Dictionary style keys() method."""
    65          if self.list is None:
    66 @@ -271,23 +288,121 @@
    67          """Dictionary style len(x) support."""
    68          return len(self.keys())
    69  
    70 -           
    71 +    def getfirst(self, key, default=None):
    72 +        """ return the first value received """
    73 +        for item in self.list:
    74 +            if item.name == key:
    75 +                if isinstance(item.file, FileType):
    76 +                    return item
    77 +                else:
    78 +                    return StringField(item.value)
    79 +        return default
    80 +                                                                   
    81 +    def getlist(self, key):
    82 +        """ return a list of received values """
    83 +        if self.list is None:
    84 +            raise TypeError, "not indexable"
    85 +        found = []
    86 +        for item in self.list:
    87 +            if item.name == key:
    88 +                if isinstance(item.file, FileType):
    89 +                    found.append(item)
    90 +                else:
    91 +                    found.append(StringField(item.value))
    92 +        return found
    93 +
    94  def parse_header(line):
    95      """Parse a Content-type like header.
    96  
    97      Return the main content-type and a dictionary of options.
    98  
    99      """
    100 -    plist = map(string.strip, string.splitfields(line, ';'))
    101 -    key = string.lower(plist[0])
    102 +   
    103 +    plist = map(lambda a: a.strip(), line.split(';'))
    104 +    key = plist[0].lower()
    105      del plist[0]
    106      pdict = {}
    107      for p in plist:
    108 -        i = string.find(p, '=')
    109 +        i = p.find('=')
    110          if i >= 0:
    111 -            name = string.lower(string.strip(p[:i]))
    112 -            value = string.strip(p[i+1:])
    113 +            name = p[:i].strip().lower()
    114 +            value = p[i+1:].strip()
    115              if len(value) >= 2 and value[0] == value[-1] == '"':
    116                  value = value[1:-1]
    117              pdict[name] = value
    118      return key, pdict
    119 +
    120 +def apply_fs_data(object, fs, **args):
    121 +    """
    122 +    Apply FieldStorage data to an object - the object must be
    123 +    callable. Examine the args, and match then with fs data,
    124 +    then call the object, return the result.
    125 +    """
    126 +
    127 +    # add form data to args
    128 +    for field in fs.list:
    129 +        if field.filename:
    130 +            val = field
    131 +        else:
    132 +            val = field.value
    133 +        args.setdefault(field.name, []).append(val)
    134 +
    135 +    # replace lists with single values
    136 +    for arg in args:
    137 +        if ((type(args[arg]) is ListType) and
    138 +            (len(args[arg]) == 1)):
    139 +            args[arg] = args[arg][0]
    140 +
    141 +    # we need to weed out unexpected keyword arguments
    142 +    # and for that we need to get a list of them. There
    143 +    # are a few options for callable objects here:
    144 +
    145 +    if type(object) is InstanceType:
    146 +        # instances are callable when they have __call__()
    147 +        object = object.__call__
    148 +
    149 +    expected = []
    150 +    if hasattr(object, "func_code"):
    151 +        # function
    152 +        fc = object.func_code
    153 +        expected = fc.co_varnames[0:fc.co_argcount]
    154 +    elif hasattr(object, 'im_func'):
    155 +        # method
    156 +        fc = object.im_func.func_code
    157 +        expected = fc.co_varnames[1:fc.co_argcount]
    158 +    elif type(object) is ClassType:
    159 +        # class
    160 +        fc = object.__init__.im_func.func_code
    161 +        expected = fc.co_varnames[1:fc.co_argcount]
    162 +
    163 +    # remove unexpected args unless co_flags & 0x08,
    164 +    # meaning function accepts **kw syntax
    165 +    if not (fc.co_flags & 0x08):
    166 +        for name in args.keys():
    167 +            if name not in expected:
    168 +                del args[name]
    169 +
    170 +    return object(**args)
    171 +
    172 +def redirect(req, location, permanent=0, text=None):
    173 +    """
    174 +    A convenience function to provide redirection
    175 +    """
    176 +
    177 +    if req.sent_bodyct:
    178 +        raise IOError, "Cannot redirect after headers have already been sent."
    179 +
    180 +    req.err_headers_out["Location"] = location
    181 +    if permanent:
    182 +        req.status = apache.HTTP_MOVED_PERMANENTLY
    183 +    else:
    184 +        req.status = apache.HTTP_MOVED_TEMPORARILY
    185 +
    186 +    if text is None:
    187 +        req.write('<p>The document has moved'
    188 +                  ' <a href="%s">here</a></p>\n'
    189 +                  % location)
    190 +    else:
    191 +        req.write(text)
    192 +
    193 +    raise apache.SERVER_RETURN, apache.OK
    194 diff -r -u mod_python-2.7.11.orig/src/include/mod_python.h mod_python-2.7.11/src/include/mod_python.h
    195 --- mod_python-2.7.11.orig/src/include/mod_python.h     2003-12-08 04:36:10.000000000 +0000
    196 +++ mod_python-2.7.11/src/include/mod_python.h  2005-09-16 01:01:47.000000000 +0100
    197 @@ -67,7 +67,7 @@
    198  #include "http_protocol.h"
    199  #include "util_script.h"
    200  #include "http_log.h"
    201 -
    202 +#include "multithread.h"
    203  
    204  /* Python headers */
    205  /* this gets rid of some comile warnings */
    206 diff -r -u mod_python-2.7.11.orig/src/mod_python.c mod_python-2.7.11/src/mod_python.c
    207 --- mod_python-2.7.11.orig/src/mod_python.c     2001-05-28 21:00:41.000000000 +0100
    208 +++ mod_python-2.7.11/src/mod_python.c  2005-09-16 01:38:05.000000000 +0100
    209 @@ -264,6 +264,9 @@
    210         server.register_cleanup() */
    211      pool *child_init_pool = NULL;
    212  
    213 +    /* Fudge for OS X */
    214 +    static int initialized = 0;
    215 +
    216      /* mod_python version */
    217      ap_add_version_component(VERSION_COMPONENT);
    218      
    219 @@ -272,8 +275,9 @@
    220      ap_add_version_component(buff);
    221  
    222      /* initialize global Python interpreter if necessary */
    223 -    if (! Py_IsInitialized())
    224 +    if (!initialized || !Py_IsInitialized())
    225      {
    226 +      initialized = 1;
    227  
    228         /* initialze the interpreter */
    229         Py_Initialize();
    230 diff -r -u mod_python-2.7.11.orig/src/requestobject.c mod_python-2.7.11/src/requestobject.c
    231 --- mod_python-2.7.11.orig/src/requestobject.c  2001-05-23 03:49:43.000000000 +0100
    232 +++ mod_python-2.7.11/src/requestobject.c       2005-09-16 02:18:33.000000000 +0100
    233 @@ -849,6 +849,7 @@
    234      {"filename",           T_STRING,    OFF(filename),               },
    235      {"path_info",          T_STRING,    OFF(path_info),            RO},
    236      {"args",               T_STRING,    OFF(args),                 RO},
    237 +    {"user",               T_STRING,                                 },
    238      /* XXX - test an array header */
    239      /* XXX finfo */
    240      /* XXX parsed_uri */
    241 @@ -1015,6 +1016,15 @@
    242      else if (strcmp(name, "hstack") == 0) {
    243         return PyString_FromString(self->hstack);
    244      }
    245 +    else if (strcmp(name, "user") == 0) {
    246 +        if (!self->request_rec->connection
    247 +           || !self->request_rec->connection->user) {
    248 +           Py_INCREF(Py_None);
    249 +           return Py_None;
    250 +        }
    251 +
    252 +        return PyString_FromString(self->request_rec->connection->user);
    253 +    }
    254      else if (strcmp(name, "_content_type_set") == 0) {
    255         return PyInt_FromLong(self->content_type_set);
    256      }
    257 @@ -1058,6 +1068,16 @@
    258             ap_pstrdup(self->request_rec->pool, PyString_AsString(value));
    259         return 0;
    260      }
    261 +    else if (strcmp(name, "user") == 0) {
    262 +        if (!self->request_rec->connection) {
    263 +           PyErr_SetString(PyExc_AttributeError, "no connection");
    264 +           return -1;
    265 +        }
    266 +
    267 +        self->request_rec->connection->user =
    268 +           ap_pstrdup(self->request_rec->pool, PyString_AsString(value));
    269 +       return 0;
    270 +    }
    271      else if (strcmp(name, "hstack") == 0) {
    272         self->hstack = ap_pstrdup(self->request_rec->pool, PyString_AsString(value));
    273         return 0;
     31   CFLAGS=$(OPT) $(INCLUDES)
     32}}}
     33into:
     34{{{
     35   CFLAGS=$(OPT) $(INCLUDES) -DEAPI
    27436}}}
    27537
    276 You also need the following patch for Trac's ModPythonHandler.py:
     38Then, do the build:
     39{{{
     40make clean
     41make
     42sudo make install
     43}}}
     44
     45{{{mod_python}}} is now installed.
     46
     47== Configurating and testing mod_python ==
     48
     49You must add the following lines to your {{{httpd.conf}}}:
     50{{{
     51    LoadModule python_module libexec/mod_python.so
     52}}}
     53(The actual path to mod_python.so may vary, but make install should report at the very end exactly where mod_python.so was placed and how the LoadModule directive should appear), and
     54{{{
     55    AddModule mod_python.c
     56}}}
     57
     58You can test that everything is OK using the instructions on http://www.modpython.org/live/mod_python-2.7.8/doc-html/inst-testing.html
     59
     60Then, modify the settings for your Trac instance into:
     61{{{
     62<Location /projects/myproject>
     63   SetHandler python-program
     64   PythonHandler trac.web.modpython_frontend
     65   PythonOption TracEnv /var/trac/myproject
     66   PythonOption TracUriRoot /projects/myproject
     67</Location>
     68}}}
     69Of course, use your own values for {{{/projects/myproject}}} and {{{/var/trac/myproject}}}. {{{TracUriRoot}}} may not be necessary.
     70
     71Restart Apache after each configuration change.
     72
     73== Patching and testing Trac ==
     74
     75At this stage, opening http://localhost/projects/myproject in your browser will give you an error 500. You can look in the error_log file: it is likely a Python exception, because the {{{trac.web.modpython_frontend}}} code tries to use APIs from mod_python that are different in 3.3.
     76
     77Here's a simple patch for {{{trac/web/modpython_frontend.py}}}. It's just a hack, but it does the job:
    27778
    27879{{{
    279 diff -r -u trac-0.8.4.orig/trac/ModPythonHandler.py trac-0.8.4/trac/ModPythonHandler.py
    280 --- trac-0.8.4.orig/trac/ModPythonHandler.py    2005-06-17 19:22:32.000000000 +0100
    281 +++ trac-0.8.4/trac/ModPythonHandler.py 2005-09-16 16:54:13.000000000 +0100
    282 @@ -99,7 +99,7 @@
    283          self.req.write(data)
     80--- /sw/lib/python2.4/site-packages/Trac-0.11dev_r6202-py2.4.egg/trac/web/modpython_frontend.py.r6202   2007-11-21 16:11:25.000000000 -0800
     81+++ /sw/lib/python2.4/site-packages/Trac-0.11dev_r6202-py2.4.egg/trac/web/modpython_frontend.py 2007-12-13 10:44:05.000000000 -0800
     82@@ -47,25 +47,14 @@
    28483 
    285      def get_header(self, name):
    286 -        return self.req.headers_in.get(name)
    287 +        return self.req.headers_in[name]
     84 class ModPythonGateway(WSGIGateway):
    28885 
    289      def send_response(self, code):
    290          self.req.status = code
    291 @@ -108,11 +108,10 @@
    292          if name.lower() == 'content-type':
    293              self.req.content_type = value
    294          else:
    295 -            self.req.headers_out.add(name, str(value))
    296 +            self.req.headers_out[name] = str(value)
     86-    wsgi_multithread = apache.mpm_query(apache.AP_MPMQ_IS_THREADED) > 0
     87-    wsgi_multiprocess = apache.mpm_query(apache.AP_MPMQ_IS_FORKED) > 0
     88-
     89     def __init__(self, req, options):
     90         environ = {}
     91         environ.update(apache.build_cgi_env(req))
    29792 
    298      def end_headers(self):
    299 -        pass
     93-        if 'TracEnv' in options:
     94-            environ['trac.env_path'] = options['TracEnv']
     95-        if 'TracEnvParentDir' in options:
     96-            environ['trac.env_parent_dir'] = options['TracEnvParentDir']
     97-        if 'TracEnvIndexTemplate' in options:
     98-            environ['trac.env_index_template'] = options['TracEnvIndexTemplate']
     99-        if 'TracTemplateVars' in options:
     100-            environ['trac.template_vars'] = options['TracTemplateVars']
     101-        if 'TracLocale' in options:
     102-            environ['trac.locale'] = options['TracLocale']
     103+       environ['trac.env_path'] = options['TracEnv']   
     104+
     105 
     106-        if 'TracUriRoot' in options:
     107+       if True:
     108             # Special handling of SCRIPT_NAME/PATH_INFO for mod_python, which
     109             # tends to get confused for whatever reason
     110             root_uri = options['TracUriRoot'].rstrip('/')
     111@@ -76,10 +65,6 @@
     112             environ['SCRIPT_NAME'] = root_uri
     113             environ['PATH_INFO'] = urllib.unquote(request_uri[len(root_uri):])
     114 
     115-        egg_cache = req.subprocess_env.get('PYTHON_EGG_CACHE')
     116-        if egg_cache:
     117-            os.environ['PYTHON_EGG_CACHE'] = egg_cache
    300118-
    301 +       self.req.send_http_header()
     119         WSGIGateway.__init__(self, environ, InputWrapper(req),
     120                              _ErrorsWrapper(lambda x: req.log_error(x)))
     121         self.req = req
     122@@ -91,17 +76,11 @@
     123             status, headers = self.headers_sent = self.headers_set
     124             self.req.status = int(status[:3])
     125             for name, value in headers:
     126-                if name.lower() == 'content-length':
     127-                    self.req.set_content_length(int(value))
     128-                elif name.lower() == 'content-type':
     129+                if name.lower() == 'content-type':
     130                     self.req.content_type = value
     131                 else:
     132                     self.req.headers_out.add(name, value)
    302133 
    303  class TracFieldStorage(util.FieldStorage):
    304      """
     134-    def _sendfile(self, fileobj):
     135-        self._send_headers()
     136-        self.req.sendfile(fileobj.name)
     137-
     138     def _write(self, data):
     139         self._send_headers()
     140         try:
    305141}}}
    306