backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Tue, 06 Apr 2010 19:08:07 +0200
changeset 5159 2543cfa5d54a
parent 5158 5e9055b8c10a (diff)
parent 5154 834269261ae4 (current diff)
child 5160 27d4cab5db03
backport stable
cwconfig.py
devtools/testlib.py
doc/book/en/development/devcore/appobject.rst
doc/book/en/development/devcore/selectors.rst
doc/book/en/development/devcore/vreg.rst
doc/book/en/development/entityclasses/more.rst
doc/book/en/development/migration/index.rst
doc/book/en/development/profiling/index.rst
doc/book/en/development/testing/index.rst
doc/book/en/intro/concepts/index.rst
web/views/basecontrollers.py
--- a/README	Tue Apr 06 16:04:37 2010 +0200
+++ b/README	Tue Apr 06 19:08:07 2010 +0200
@@ -1,6 +1,15 @@
 CubicWeb semantic web framework
 ===============================
 
+CubicWeb is a entities / relations based knowledge management system
+developped at Logilab.
+
+This package contains:
+* a repository server
+* a RQL command line client to the repository
+* an adaptative modpython interface to the server
+* a bunch of other management tools
+
 Install
 -------
 
--- a/__pkginfo__.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/__pkginfo__.py	Tue Apr 06 19:08:07 2010 +0200
@@ -1,36 +1,21 @@
 # pylint: disable-msg=W0622,C0103
 """cubicweb global packaging information for the cubicweb knowledge management
 software
+
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
 
-distname = "cubicweb"
-modname = "cubicweb"
+modname = distname = "cubicweb"
 
 numversion = (3, 7, 3)
 version = '.'.join(str(num) for num in numversion)
 
-license = 'LGPL'
-copyright = '''Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE).
-http://www.logilab.fr/ -- mailto:contact@logilab.fr'''
-
+description = "a repository of entities / relations for knowledge management"
 author = "Logilab"
 author_email = "contact@logilab.fr"
-
-short_desc = "a repository of entities / relations for knowledge management"
-long_desc = """CubicWeb is a entities / relations based knowledge management system
-developped at Logilab.
-
-This package contains:
-* a repository server
-* a RQL command line client to the repository
-* an adaptative modpython interface to the server
-* a bunch of other management tools
-"""
-
 web = 'http://www.cubicweb.org'
 ftp = 'ftp://ftp.logilab.org/pub/cubicweb'
-pyversions = ['2.5', '2.6']
+license = 'LGPL'
 
 classifiers = [
            'Environment :: Web Environment',
@@ -39,6 +24,34 @@
            'Programming Language :: JavaScript',
 ]
 
+__depends__ = {
+    'logilab-common': '>= 0.49.0',
+    'logilab-mtconverter': '>= 0.6.0',
+    'rql': '>= 0.26.0',
+    'yams': '>= 0.28.1',
+    'docutils': '>= 0.6',
+    #gettext                    # for xgettext, msgcat, etc...
+    # web dependancies
+    'simplejson': '>= 2.0.9',
+    'lxml': '',
+    'Twisted': '',
+    # XXX graphviz
+    # server dependencies
+    'logilab-database': '',
+    'pysqlite': '>= 2.5.5', # XXX install pysqlite2
+    }
+
+__recommends__ = {
+    'Pyro': '>= 3.9.1',
+    'PIL': '',                  # for captcha
+    'pycrypto': '',             # for crypto extensions
+    'fyzz': '>= 0.1.0',         # for sparql
+    'pysixt': '>= 0.1.0',       # XXX for pdf export
+    'python-gettext': '>= 1.0', # XXX for pdf export
+    'vobject': '>= 0.6.0',      # for ical view
+    #'Products.FCKeditor':'',
+    #'SimpleTAL':'>= 4.1.6',
+    }
 
 import sys
 from os import listdir, environ
@@ -49,57 +62,53 @@
            if not s.endswith('.bat')]
 include_dirs = [join('test', 'data'),
                 join('server', 'test', 'data'),
+                join('hooks', 'test', 'data'),
                 join('web', 'test', 'data'),
                 join('devtools', 'test', 'data'),
                 'skeleton']
 
 
-entities_dir = 'entities'
-schema_dir = 'schemas'
-sobjects_dir = 'sobjects'
-server_migration_dir = join('misc', 'migration')
-data_dir = join('web', 'data')
-wdoc_dir = join('web', 'wdoc')
-wdocimages_dir = join(wdoc_dir, 'images')
-views_dir = join('web', 'views')
-i18n_dir = 'i18n'
+_server_migration_dir = join('misc', 'migration')
+_data_dir = join('web', 'data')
+_wdoc_dir = join('web', 'wdoc')
+_wdocimages_dir = join(_wdoc_dir, 'images')
+_views_dir = join('web', 'views')
+_i18n_dir = 'i18n'
 
-if environ.get('APYCOT_ROOT'):
+_pyversion = '.'.join(str(num) for num in sys.version_info[0:2])
+if '--home' in sys.argv:
     # --home install
-    pydir = 'python'
+    pydir = 'python' + _pyversion
 else:
-    python_version = '.'.join(str(num) for num in sys.version_info[0:2])
-    pydir = join('python' + python_version, 'site-packages')
+    pydir = join('python' + _pyversion, 'site-packages')
 
 try:
     data_files = [
-        # common data
-        #[join('share', 'cubicweb', 'entities'),
-        # [join(entities_dir, filename) for filename in listdir(entities_dir)]],
         # server data
         [join('share', 'cubicweb', 'schemas'),
-         [join(schema_dir, filename) for filename in listdir(schema_dir)]],
-        #[join('share', 'cubicweb', 'sobjects'),
-        # [join(sobjects_dir, filename) for filename in listdir(sobjects_dir)]],
+         [join('schemas', filename) for filename in listdir('schemas')]],
         [join('share', 'cubicweb', 'migration'),
-         [join(server_migration_dir, filename)
-          for filename in listdir(server_migration_dir)]],
+         [join(_server_migration_dir, filename)
+          for filename in listdir(_server_migration_dir)]],
         # web data
         [join('share', 'cubicweb', 'cubes', 'shared', 'data'),
-         [join(data_dir, fname) for fname in listdir(data_dir) if not isdir(join(data_dir, fname))]],
+         [join(_data_dir, fname) for fname in listdir(_data_dir)
+          if not isdir(join(_data_dir, fname))]],
         [join('share', 'cubicweb', 'cubes', 'shared', 'data', 'timeline'),
-         [join(data_dir, 'timeline', fname) for fname in listdir(join(data_dir, 'timeline'))]],
+         [join(_data_dir, 'timeline', fname) for fname in listdir(join(_data_dir, 'timeline'))]],
         [join('share', 'cubicweb', 'cubes', 'shared', 'data', 'images'),
-         [join(data_dir, 'images', fname) for fname in listdir(join(data_dir, 'images'))]],
+         [join(_data_dir, 'images', fname) for fname in listdir(join(_data_dir, 'images'))]],
         [join('share', 'cubicweb', 'cubes', 'shared', 'wdoc'),
-         [join(wdoc_dir, fname) for fname in listdir(wdoc_dir) if not isdir(join(wdoc_dir, fname))]],
+         [join(_wdoc_dir, fname) for fname in listdir(_wdoc_dir)
+          if not isdir(join(_wdoc_dir, fname))]],
         [join('share', 'cubicweb', 'cubes', 'shared', 'wdoc', 'images'),
-         [join(wdocimages_dir, fname) for fname in listdir(wdocimages_dir)]],
-        # XXX: .pt install should be handled properly in a near future version
+         [join(_wdocimages_dir, fname) for fname in listdir(_wdocimages_dir)]],
+        [join('share', 'cubicweb', 'cubes', 'shared', 'i18n'),
+         [join(_i18n_dir, fname) for fname in listdir(_i18n_dir)]],
+        # XXX: drop .pt files
         [join('lib', pydir, 'cubicweb', 'web', 'views'),
-         [join(views_dir, fname) for fname in listdir(views_dir) if fname.endswith('.pt')]],
-        [join('share', 'cubicweb', 'cubes', 'shared', 'i18n'),
-         [join(i18n_dir, fname) for fname in listdir(i18n_dir)]],
+         [join(_views_dir, fname) for fname in listdir(_views_dir)
+          if fname.endswith('.pt')]],
         # skeleton
         ]
 except OSError:
--- a/cwconfig.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/cwconfig.py	Tue Apr 06 19:08:07 2010 +0200
@@ -126,12 +126,11 @@
 import sys
 import os
 import logging
-import tempfile
 from smtplib import SMTP
 from threading import Lock
-from os.path import exists, join, expanduser, abspath, normpath, basename, isdir
+from os.path import (exists, join, expanduser, abspath, normpath,
+                     basename, isdir, dirname)
 from warnings import warn
-
 from logilab.common.decorators import cached, classproperty
 from logilab.common.deprecation import deprecated
 from logilab.common.logging_ext import set_log_methods, init_log
@@ -179,6 +178,23 @@
                                  % (directory, modes))
     return modes[0]
 
+def _find_prefix(start_path=CW_SOFTWARE_ROOT):
+    """Runs along the parent directories of *start_path* (default to cubicweb source directory)
+    looking for one containing a 'share/cubicweb' directory.
+    The first matching directory is assumed as the prefix installation of cubicweb
+
+    Returns the matching prefix or None.
+    """
+    prefix = start_path
+    old_prefix = None
+    if not isdir(start_path):
+        prefix = dirname(start_path)
+    while not isdir(join(prefix, 'share', 'cubicweb')) and prefix != old_prefix:
+        old_prefix = prefix
+        prefix = dirname(prefix)
+    if isdir(join(prefix, 'share', 'cubicweb')):
+        return prefix
+    return sys.prefix
 
 # persistent options definition
 PERSISTENT_OPTIONS = (
@@ -251,6 +267,11 @@
 
 CWDEV = exists(join(CW_SOFTWARE_ROOT, '.hg'))
 
+try:
+    _INSTALL_PREFIX = os.environ['CW_INSTALL_PREFIX']
+except KeyError:
+    _INSTALL_PREFIX = _find_prefix()
+
 class CubicWebNoAppConfiguration(ConfigurationMixIn):
     """base class for cubicweb configuration without a specific instance directory
     """
@@ -264,25 +285,16 @@
     # debug mode
     debugmode = False
 
-    if os.environ.get('APYCOT_ROOT'):
-        mode = 'test'
-        # allow to test cubes within apycot using cubicweb not installed by
-        # apycot
-        if __file__.startswith(os.environ['APYCOT_ROOT']):
-            CUBES_DIR = '%(APYCOT_ROOT)s/local/share/cubicweb/cubes/' % os.environ
-            # create __init__ file
-            file(join(CUBES_DIR, '__init__.py'), 'w').close()
-        else:
-            CUBES_DIR = '/usr/share/cubicweb/cubes/'
-    elif (CWDEV and _forced_mode != 'system'):
+
+    if (CWDEV and _forced_mode != 'system'):
         mode = 'user'
-        CUBES_DIR = abspath(normpath(join(CW_SOFTWARE_ROOT, '../cubes')))
+        _CUBES_DIR = join(CW_SOFTWARE_ROOT, '../cubes')
     else:
-        if _forced_mode == 'user':
-            mode = 'user'
-        else:
-            mode = 'system'
-        CUBES_DIR = '/usr/share/cubicweb/cubes/'
+        mode = _forced_mode or 'system'
+        _CUBES_DIR = join(_INSTALL_PREFIX, 'share', 'cubicweb', 'cubes')
+
+    CUBES_DIR = env_path('CW_CUBES_DIR', _CUBES_DIR, 'cubes', checkexists=False)
+    CUBES_PATH = os.environ.get('CW_CUBES_PATH', '').split(os.pathsep)
 
     options = (
        ('log-threshold',
@@ -344,7 +356,6 @@
           }),
         )
     # static and class methods used to get instance independant resources ##
-
     @staticmethod
     def cubicweb_version():
         """return installed cubicweb version"""
@@ -383,21 +394,17 @@
                            % directory)
                 continue
             for cube in os.listdir(directory):
-                if isdir(join(directory, cube)) and not cube == 'shared':
+                if cube in ('CVS', '.svn', 'shared', '.hg'):
+                    continue
+                if isdir(join(directory, cube)):
                     cubes.add(cube)
         return sorted(cubes)
 
     @classmethod
     def cubes_search_path(cls):
         """return the path of directories where cubes should be searched"""
-        path = []
-        try:
-            for directory in os.environ['CW_CUBES_PATH'].split(os.pathsep):
-                directory = abspath(normpath(directory))
-                if exists(directory) and not directory in path:
-                    path.append(directory)
-        except KeyError:
-            pass
+        path = [abspath(normpath(directory)) for directory in cls.CUBES_PATH
+                if directory.strip() and exists(directory.strip())]
         if not cls.CUBES_DIR in path and exists(cls.CUBES_DIR):
             path.append(cls.CUBES_DIR)
         return path
@@ -413,7 +420,7 @@
     @classmethod
     def cube_dir(cls, cube):
         """return the cube directory for the given cube id,
-        raise ConfigurationError if it doesn't exists
+        raise `ConfigurationError` if it doesn't exists
         """
         for directory in cls.cubes_search_path():
             cubedir = join(directory, cube)
@@ -431,10 +438,12 @@
         """return the information module for the given cube"""
         cube = CW_MIGRATION_MAP.get(cube, cube)
         try:
-            return getattr(__import__('cubes.%s.__pkginfo__' % cube), cube).__pkginfo__
+            parent = __import__('cubes.%s.__pkginfo__' % cube)
+            return getattr(parent, cube).__pkginfo__
         except Exception, ex:
-            raise ConfigurationError('unable to find packaging information for '
-                                     'cube %s (%s: %s)' % (cube, ex.__class__.__name__, ex))
+            raise ConfigurationError(
+                'unable to find packaging information for cube %s (%s: %s)'
+                % (cube, ex.__class__.__name__, ex))
 
     @classmethod
     def cube_version(cls, cube):
@@ -446,14 +455,33 @@
         return Version(version)
 
     @classmethod
+    def _cube_deps(cls, cube, key, oldkey):
+        """return cubicweb cubes used by the given cube"""
+        pkginfo = cls.cube_pkginfo(cube)
+        try:
+            deps = getattr(pkginfo, key)
+        except AttributeError:
+            if hasattr(pkginfo, oldkey):
+                warn('[3.6] %s is deprecated, use %s dict' % (oldkey, key),
+                     DeprecationWarning)
+                deps = getattr(pkginfo, oldkey)
+            else:
+                deps = {}
+        if not isinstance(deps, dict):
+            deps = dict((key, None) for key in deps)
+            warn('[3.6] cube %s should define %s as a dict' % (cube, key),
+                 DeprecationWarning)
+        return deps
+
+    @classmethod
     def cube_dependencies(cls, cube):
         """return cubicweb cubes used by the given cube"""
-        return getattr(cls.cube_pkginfo(cube), '__use__', ())
+        return cls._cube_deps(cube, '__depends_cubes__', '__use__')
 
     @classmethod
     def cube_recommends(cls, cube):
         """return cubicweb cubes recommended by the given cube"""
-        return getattr(cls.cube_pkginfo(cube), '__recommend__', ())
+        return cls._cube_deps(cube, '__recommends_cubes__', '__recommend__')
 
     @classmethod
     def expand_cubes(cls, cubes, with_recommends=False):
@@ -486,9 +514,10 @@
         graph = {}
         for cube in cubes:
             cube = CW_MIGRATION_MAP.get(cube, cube)
-            deps = cls.cube_dependencies(cube) + \
-                   cls.cube_recommends(cube)
-            graph[cube] = set(dep for dep in deps if dep in cubes)
+            graph[cube] = set(dep for dep in cls.cube_dependencies(cube)
+                              if dep in cubes)
+            graph[cube] |= set(dep for dep in cls.cube_recommends(cube)
+                               if dep in cubes)
         cycles = get_cycles(graph)
         if cycles:
             cycles = '\n'.join(' -> '.join(cycle) for cycle in cycles)
@@ -636,6 +665,7 @@
             cw_rest_init()
 
     def adjust_sys_path(self):
+        # overriden in CubicWebConfiguration
         self.cls_adjust_sys_path()
 
     def init_log(self, logthreshold=None, debug=False,
@@ -685,35 +715,24 @@
         """
         return None
 
+
 class CubicWebConfiguration(CubicWebNoAppConfiguration):
     """base class for cubicweb server and web configurations"""
 
-    INSTANCES_DATA_DIR = None
-    if os.environ.get('APYCOT_ROOT'):
-        root = os.environ['APYCOT_ROOT']
-        REGISTRY_DIR = '%s/etc/cubicweb.d/' % root
-        if not exists(REGISTRY_DIR):
-            os.makedirs(REGISTRY_DIR)
-        RUNTIME_DIR = tempfile.gettempdir()
-        # allow to test cubes within apycot using cubicweb not installed by
-        # apycot
-        if __file__.startswith(os.environ['APYCOT_ROOT']):
-            MIGRATION_DIR = '%s/local/share/cubicweb/migration/' % root
+    if CubicWebNoAppConfiguration.mode == 'user':
+        _INSTANCES_DIR = expanduser('~/etc/cubicweb.d/')
+    else: #mode = 'system'
+        if _INSTALL_PREFIX == '/usr':
+            _INSTANCES_DIR = '/etc/cubicweb.d/'
         else:
-            MIGRATION_DIR = '/usr/share/cubicweb/migration/'
-    else:
-        if CubicWebNoAppConfiguration.mode == 'user':
-            REGISTRY_DIR = expanduser('~/etc/cubicweb.d/')
-            RUNTIME_DIR = tempfile.gettempdir()
-            INSTANCES_DATA_DIR = REGISTRY_DIR
-        else: #mode = 'system'
-            REGISTRY_DIR = '/etc/cubicweb.d/'
-            RUNTIME_DIR = '/var/run/cubicweb/'
-            INSTANCES_DATA_DIR = '/var/lib/cubicweb/instances/'
-        if CWDEV:
-            MIGRATION_DIR = join(CW_SOFTWARE_ROOT, 'misc', 'migration')
-        else:
-            MIGRATION_DIR = '/usr/share/cubicweb/migration/'
+            _INSTANCES_DIR = join(_INSTALL_PREFIX, 'etc', 'cubicweb.d')
+
+    if os.environ.get('APYCOT_ROOT'):
+        _cubes_init = join(CubicWebNoAppConfiguration.CUBES_DIR, '__init__.py')
+        if not exists(_cubes_init):
+            file(join(_cubes_init), 'w').close()
+        if not exists(_INSTANCES_DIR):
+            os.makedirs(_INSTANCES_DIR)
 
     # for some commands (creation...) we don't want to initialize gettext
     set_language = True
@@ -757,25 +776,19 @@
         )
 
     @classmethod
-    def runtime_dir(cls):
-        """run time directory for pid file..."""
-        return env_path('CW_RUNTIME_DIR', cls.RUNTIME_DIR, 'run time')
-
-    @classmethod
-    def registry_dir(cls):
+    def instances_dir(cls):
         """return the control directory"""
-        return env_path('CW_INSTANCES_DIR', cls.REGISTRY_DIR, 'registry')
-
-    @classmethod
-    def instance_data_dir(cls):
-        """return the instance data directory"""
-        return env_path('CW_INSTANCES_DATA_DIR', cls.INSTANCES_DATA_DIR,
-                        'additional data')
+        return env_path('CW_INSTANCES_DIR', cls._INSTANCES_DIR, 'registry')
 
     @classmethod
     def migration_scripts_dir(cls):
         """cubicweb migration scripts directory"""
-        return env_path('CW_MIGRATION_DIR', cls.MIGRATION_DIR, 'migration')
+        if CWDEV:
+            return join(CW_SOFTWARE_ROOT, 'misc', 'migration')
+        mdir = join(_INSTALL_PREFIX, 'share', 'cubicweb', 'migration')
+        if not exists(mdir):
+            raise ConfigurationError('migration path %s doesn\'t exist' % mdir)
+        return mdir
 
     @classmethod
     def config_for(cls, appid, config=None):
@@ -798,9 +811,10 @@
         """return the home directory of the instance with the given
         instance id
         """
-        home = join(cls.registry_dir(), appid)
+        home = join(cls.instances_dir(), appid)
         if not exists(home):
-            raise ConfigurationError('no such instance %s (check it exists with "cubicweb-ctl list")' % appid)
+            raise ConfigurationError('no such instance %s (check it exists with'
+                                     ' "cubicweb-ctl list")' % appid)
         return home
 
     MODES = ('common', 'repository', 'Any', 'web')
@@ -823,7 +837,9 @@
     def default_log_file(self):
         """return default path to the log file of the instance'server"""
         if self.mode == 'user':
-            basepath = join(tempfile.gettempdir(), '%s-%s' % (basename(self.appid), self.name))
+            import tempfile
+            basepath = join(tempfile.gettempdir(), '%s-%s' % (
+                basename(self.appid), self.name))
             path = basepath + '.log'
             i = 1
             while exists(path) and i < 100: # arbitrary limit to avoid infinite loop
@@ -838,7 +854,13 @@
 
     def default_pid_file(self):
         """return default path to the pid file of the instance'server"""
-        return join(self.runtime_dir(), '%s-%s.pid' % (self.appid, self.name))
+        if self.mode == 'system':
+            # XXX not under _INSTALL_PREFIX, right?
+            rtdir = env_path('CW_RUNTIME_DIR', '/var/run/cubicweb/', 'run time')
+        else:
+            import tempfile
+            rtdir = env_path('CW_RUNTIME_DIR', tempfile.gettempdir(), 'run time')
+        return join(rtdir, '%s-%s.pid' % (self.appid, self.name))
 
     # instance methods used to get instance specific resources #############
 
@@ -858,11 +880,17 @@
 
     @property
     def apphome(self):
-        return join(self.registry_dir(), self.appid)
+        return join(self.instances_dir(), self.appid)
 
     @property
     def appdatahome(self):
-        return join(self.instance_data_dir(), self.appid)
+        if self.mode == 'system':
+            # XXX not under _INSTALL_PREFIX, right?
+            iddir = '/var/lib/cubicweb/instances/'
+        else:
+            iddir = self.instances_dir()
+        iddir = env_path('CW_INSTANCES_DATA_DIR', iddir, 'additional data')
+        return join(iddir, self.appid)
 
     def init_cubes(self, cubes):
         assert self._cubes is None, self._cubes
@@ -927,7 +955,8 @@
                 if exists(sitefile) and not sitefile in self._site_loaded:
                     self._load_site_cubicweb(sitefile)
                     self._site_loaded.add(sitefile)
-                    self.warning('[3.5] site_erudi.py is deprecated, should be renamed to site_cubicweb.py')
+                    self.warning('[3.5] site_erudi.py is deprecated, should be '
+                                 'renamed to site_cubicweb.py')
 
     def _load_site_cubicweb(self, sitefile):
         # XXX extrapath argument to load_module_from_file only in lgc > 0.46
--- a/cwctl.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/cwctl.py	Tue Apr 06 19:08:07 2010 +0200
@@ -13,6 +13,7 @@
 # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
 # completion). So import locally in command helpers.
 import sys
+from warnings import warn
 from os import remove, listdir, system, pathsep
 try:
     from os import kill, getpgid
@@ -85,7 +86,7 @@
         Instance used by another one should appears first in the file (one
         instance per line)
         """
-        regdir = cwcfg.registry_dir()
+        regdir = cwcfg.instances_dir()
         _allinstances = list_instances(regdir)
         if isfile(join(regdir, 'startorder')):
             allinstances = []
@@ -168,84 +169,6 @@
 
 # base commands ###############################################################
 
-def version_strictly_lower(a, b):
-    from logilab.common.changelog import Version
-    if a:
-        a = Version(a)
-    if b:
-        b = Version(b)
-    return a < b
-
-def max_version(a, b):
-    from logilab.common.changelog import Version
-    return str(max(Version(a), Version(b)))
-
-class ConfigurationProblem(object):
-    """Each cube has its own list of dependencies on other cubes/versions.
-
-    The ConfigurationProblem is used to record the loaded cubes, then to detect
-    inconsistencies in their dependencies.
-
-    See configuration management on wikipedia for litterature.
-    """
-
-    def __init__(self):
-        self.cubes = {}
-
-    def add_cube(self, name, info):
-        self.cubes[name] = info
-
-    def solve(self):
-        self.warnings = []
-        self.errors = []
-        self.read_constraints()
-        for cube, versions in sorted(self.constraints.items()):
-            oper, version = None, None
-            # simplify constraints
-            if versions:
-                for constraint in versions:
-                    op, ver = constraint
-                    if oper is None:
-                        oper = op
-                        version = ver
-                    elif op == '>=' and oper == '>=':
-                        version = max_version(ver, version)
-                    else:
-                        print 'unable to handle this case', oper, version, op, ver
-            # "solve" constraint satisfaction problem
-            if cube not in self.cubes:
-                self.errors.append( ('add', cube, version) )
-            elif versions:
-                lower_strict = version_strictly_lower(self.cubes[cube].version, version)
-                if oper in ('>=','='):
-                    if lower_strict:
-                        self.errors.append( ('update', cube, version) )
-                else:
-                    print 'unknown operator', oper
-
-    def read_constraints(self):
-        self.constraints = {}
-        self.reverse_constraints = {}
-        for cube, info in self.cubes.items():
-            if hasattr(info,'__depends_cubes__'):
-                use = info.__depends_cubes__
-                if not isinstance(use, dict):
-                    use = dict((key, None) for key in use)
-                    self.warnings.append('cube %s should define __depends_cubes__ as a dict not a list')
-            else:
-                self.warnings.append('cube %s should define __depends_cubes__' % cube)
-                use = dict((key, None) for key in info.__use__)
-            for name, constraint in use.items():
-                self.constraints.setdefault(name,set())
-                if constraint:
-                    try:
-                        oper, version = constraint.split()
-                        self.constraints[name].add( (oper, version) )
-                    except:
-                        self.warnings.append('cube %s depends on %s but constraint badly formatted: %s'
-                                             % (cube, name, constraint))
-                self.reverse_constraints.setdefault(name, set()).add(cube)
-
 class ListCommand(Command):
     """List configurations, cubes and instances.
 
@@ -262,6 +185,7 @@
         """run the command with its specific arguments"""
         if args:
             raise BadCommandUsage('Too much arguments')
+        from cubicweb.migration import ConfigurationProblem
         print 'CubicWeb %s (%s mode)' % (cwcfg.cubicweb_version(), cwcfg.mode)
         print
         print 'Available configurations:'
@@ -273,7 +197,7 @@
                     continue
                 print '   ', line
         print
-        cfgpb = ConfigurationProblem()
+        cfgpb = ConfigurationProblem(cwcfg)
         try:
             cubesdir = pathsep.join(cwcfg.cubes_search_path())
             namesize = max(len(x) for x in cwcfg.available_cubes())
@@ -284,26 +208,31 @@
         else:
             print 'Available cubes (%s):' % cubesdir
             for cube in cwcfg.available_cubes():
-                if cube in ('CVS', '.svn', 'shared', '.hg'):
-                    continue
                 try:
                     tinfo = cwcfg.cube_pkginfo(cube)
                     tversion = tinfo.version
-                    cfgpb.add_cube(cube, tinfo)
+                    cfgpb.add_cube(cube, tversion)
                 except ConfigurationError:
                     tinfo = None
                     tversion = '[missing cube information]'
                 print '* %s %s' % (cube.ljust(namesize), tversion)
                 if self.config.verbose:
-                    shortdesc = tinfo and (getattr(tinfo, 'short_desc', '')
-                                           or tinfo.__doc__)
-                    if shortdesc:
-                        print '    '+ '    \n'.join(shortdesc.splitlines())
+                    if tinfo:
+                        descr = getattr(tinfo, 'description', '')
+                        if not descr:
+                            descr = getattr(tinfo, 'short_desc', '')
+                            if descr:
+                                warn('[3.8] short_desc is deprecated, update %s'
+                                     ' pkginfo' % cube, DeprecationWarning)
+                            else:
+                                descr = tinfo.__doc__
+                        if descr:
+                            print '    '+ '    \n'.join(descr.splitlines())
                     modes = detect_available_modes(cwcfg.cube_dir(cube))
                     print '    available modes: %s' % ', '.join(modes)
         print
         try:
-            regdir = cwcfg.registry_dir()
+            regdir = cwcfg.instances_dir()
         except ConfigurationError, ex:
             print 'No instance available:', ex
             print
@@ -611,7 +540,7 @@
     actionverb = 'restarted'
 
     def run_args(self, args, askconfirm):
-        regdir = cwcfg.registry_dir()
+        regdir = cwcfg.instances_dir()
         if not isfile(join(regdir, 'startorder')) or len(args) <= 1:
             # no specific startorder
             super(RestartInstanceCommand, self).run_args(args, askconfirm)
@@ -953,7 +882,7 @@
 
     def run(self, args):
         """run the command with its specific arguments"""
-        regdir = cwcfg.registry_dir()
+        regdir = cwcfg.instances_dir()
         for appid in sorted(listdir(regdir)):
             print appid
 
--- a/debian/control	Tue Apr 06 16:04:37 2010 +0200
+++ b/debian/control	Tue Apr 06 19:08:07 2010 +0200
@@ -97,7 +97,7 @@
 Package: cubicweb-common
 Architecture: all
 XB-Python-Version: ${python:Versions}
-Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.49.0), python-yams (>= 0.28.1), python-rql (>= 0.25.0), python-lxml
+Depends: ${python:Depends}, graphviz, gettext, python-logilab-mtconverter (>= 0.6.0), python-logilab-common (>= 0.49.0), python-yams (>= 0.28.1), python-rql (>= 0.26.0), python-lxml
 Recommends: python-simpletal (>= 4.0), python-crypto
 Conflicts: cubicweb-core
 Replaces: cubicweb-core
--- a/devtools/__init__.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/devtools/__init__.py	Tue Apr 06 19:08:07 2010 +0200
@@ -97,9 +97,6 @@
           }),
         ))
 
-    if not os.environ.get('APYCOT_ROOT'):
-        REGISTRY_DIR = normpath(join(CW_SOFTWARE_ROOT, '../cubes'))
-
     def __init__(self, appid, log_threshold=logging.CRITICAL+10):
         ServerConfiguration.__init__(self, appid)
         self.init_log(log_threshold, force=True)
--- a/devtools/testlib.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/devtools/testlib.py	Tue Apr 06 19:08:07 2010 +0200
@@ -5,6 +5,8 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
+from __future__ import with_statement
+
 __docformat__ = "restructuredtext en"
 
 import os
@@ -30,6 +32,7 @@
 from cubicweb.dbapi import repo_connect, ConnectionProperties, ProgrammingError
 from cubicweb.sobjects import notification
 from cubicweb.web import Redirect, application
+from cubicweb.server.session import security_enabled
 from cubicweb.devtools import SYSTEM_ENTITIES, SYSTEM_RELATIONS, VIEW_VALIDATORS
 from cubicweb.devtools import fake, htmlparser
 
@@ -784,6 +787,10 @@
         """this method populates the database with `how_many` entities
         of each possible type. It also inserts random relations between them
         """
+        with security_enabled(self.session, read=False, write=False):
+            self._auto_populate(how_many)
+
+    def _auto_populate(self, how_many):
         cu = self.cursor()
         self.custom_populate(how_many, cu)
         vreg = self.vreg
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/etwist/http.py	Tue Apr 06 19:08:07 2010 +0200
@@ -0,0 +1,71 @@
+"""twisted server for CubicWeb web instances
+
+:organization: Logilab
+:copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
+"""
+
+__docformat__ = "restructuredtext en"
+
+from cubicweb.web.http_headers import Headers
+
+class HTTPResponse(object):
+    """An object representing an HTTP Response to be sent to the client.
+    """
+    def __init__(self, twisted_request, code=None, headers=None, stream=None):
+        self._headers_out = headers
+        self._twreq = twisted_request
+        self._stream = stream
+        self._code = code
+
+        self._init_headers()
+        self._finalize()
+
+    def _init_headers(self):
+        if self._headers_out is None:
+            return
+
+        # initialize cookies
+        cookies = self._headers_out.getHeader('set-cookie') or []
+        for cookie in cookies:
+            self._twreq.addCookie(cookie.name, cookie.value, cookie.expires,
+                                  cookie.domain, cookie.path, #TODO max-age
+                                  comment = cookie.comment, secure=cookie.secure)
+        self._headers_out.removeHeader('set-cookie')
+
+        # initialize other headers
+        for k, v in self._headers_out.getAllRawHeaders():
+            self._twreq.setHeader(k, v[0])
+
+        # add content-length if not present
+        if (self._headers_out.getHeader('content-length') is None
+            and self._stream is not None):
+           self._twreq.setHeader('content-length', len(self._stream))
+
+
+    def _finalize(self):
+        if self._stream is not None:
+            self._twreq.write(str(self._stream))
+        if self._code is not None:
+            self._twreq.setResponseCode(self._code)
+        self._twreq.finish()
+
+    def __repr__(self):
+        return "<%s.%s code=%d>" % (self.__module__, self.__class__.__name__, self._code)
+
+
+def not_modified_response(twisted_request, headers_in):
+    headers_out = Headers()
+
+    for header in (
+        # Required from sec 10.3.5:
+        'date', 'etag', 'content-location', 'expires',
+        'cache-control', 'vary',
+        # Others:
+        'server', 'proxy-authenticate', 'www-authenticate', 'warning'):
+        value = headers_in.getRawHeaders(header)
+        if value is not None:
+            headers_out.setRawHeaders(header, value)
+    return HTTPResponse(twisted_request=twisted_request,
+                        headers=headers_out)
--- a/etwist/request.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/etwist/request.py	Tue Apr 06 19:08:07 2010 +0200
@@ -9,22 +9,13 @@
 
 from datetime import datetime
 
-from twisted.web2 import http, http_headers
+from twisted.web import http
 
 from cubicweb.web import DirectResponse
 from cubicweb.web.request import CubicWebRequestBase
 from cubicweb.web.httpcache import GMTOFFSET
-
-def cleanup_files(dct, encoding):
-    d = {}
-    for k, infos in dct.items():
-        for (filename, mt, stream) in infos:
-            if filename:
-                # XXX: suppose that no file submitted <-> no filename
-                filename = unicode(filename, encoding)
-                mt = u'%s/%s' % (mt.mediaType, mt.mediaSubtype)
-                d[k] = (filename, mt, stream)
-    return d
+from cubicweb.web.http_headers import Headers
+from cubicweb.etwist.http import not_modified_response
 
 
 class CubicWebTwistedRequestAdapter(CubicWebRequestBase):
@@ -32,10 +23,15 @@
         self._twreq = req
         self._base_url = base_url
         super(CubicWebTwistedRequestAdapter, self).__init__(vreg, https, req.args)
-        self.form.update(cleanup_files(req.files, self.encoding))
-        # prepare output headers
-        self.headers_out = http_headers.Headers()
-        self._headers = req.headers
+        for key, (name, stream) in req.files.iteritems():
+            if name is None:
+                self.form[key] = (name, stream)
+            else:
+                self.form[key] = (unicode(name, self.encoding), stream)
+        # XXX can't we keep received_headers?
+        self._headers_in = Headers()
+        for k, v in req.received_headers.iteritems():
+            self._headers_in.addRawHeader(k, v)
 
     def base_url(self):
         """return the root url of the instance"""
@@ -63,29 +59,8 @@
         raise KeyError if the header is not set
         """
         if raw:
-            return self._twreq.headers.getRawHeaders(header, [default])[0]
-        return self._twreq.headers.getHeader(header, default)
-
-    def set_header(self, header, value, raw=True):
-        """set an output HTTP header"""
-        if raw:
-            # adding encoded header is important, else page content
-            # will be reconverted back to unicode and apart unefficiency, this
-            # may cause decoding problem (e.g. when downloading a file)
-            self.headers_out.setRawHeaders(header, [str(value)])
-        else:
-            self.headers_out.setHeader(header, value)
-
-    def add_header(self, header, value):
-        """add an output HTTP header"""
-        # adding encoded header is important, else page content
-        # will be reconverted back to unicode and apart unefficiency, this
-        # may cause decoding problem (e.g. when downloading a file)
-        self.headers_out.addRawHeader(header, str(value))
-
-    def remove_header(self, header):
-        """remove an output HTTP header"""
-        self.headers_out.removeHeader(header)
+            return self._headers_in.getRawHeaders(header, [default])[0]
+        return self._headers_in.getHeader(header, default)
 
     def _validate_cache(self):
         """raise a `DirectResponse` exception if a cached page along the way
@@ -95,11 +70,32 @@
             # Expires header seems to be required by IE7
             self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
             return
-        try:
-            http.checkPreconditions(self._twreq, _PreResponse(self))
-        except http.HTTPError, ex:
-            self.info('valid http cache, no actual rendering')
-            raise DirectResponse(ex.response)
+
+        # when using both 'Last-Modified' and 'ETag' response headers
+        # (i.e. using respectively If-Modified-Since and If-None-Match request
+        # headers, see
+        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.4 for
+        # reference
+
+        cached_because_not_modified_since = False
+
+        last_modified = self.headers_out.getHeader('last-modified')
+        if last_modified is not None:
+            cached_because_not_modified_since = (self._twreq.setLastModified(last_modified)
+                                                 == http.CACHED)
+
+        if not cached_because_not_modified_since:
+            return
+
+        cached_because_etag_is_same = False
+        etag = self.headers_out.getRawHeaders('etag')
+        if etag is not None:
+            cached_because_etag_is_same = self._twreq.setETag(etag[0]) == http.CACHED
+
+        if cached_because_etag_is_same:
+            response = not_modified_response(self._twreq, self._headers_in)
+            raise DirectResponse(response)
+
         # Expires header seems to be required by IE7
         self.add_header('Expires', 'Sat, 01 Jan 2000 00:00:00 GMT')
 
@@ -120,9 +116,3 @@
             # :/ twisted is returned a localized time stamp
             return datetime.fromtimestamp(mtime) + GMTOFFSET
         return None
-
-
-class _PreResponse(object):
-    def __init__(self, request):
-        self.headers = request.headers_out
-        self.code = 200
--- a/etwist/server.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/etwist/server.py	Tue Apr 06 19:08:07 2010 +0200
@@ -14,19 +14,25 @@
 from time import mktime
 from datetime import date, timedelta
 from urlparse import urlsplit, urlunsplit
+from cgi import FieldStorage, parse_header
 
 from twisted.internet import reactor, task, threads
 from twisted.internet.defer import maybeDeferred
-from twisted.web2 import channel, http, server, iweb
-from twisted.web2 import static, resource, responsecode
+from twisted.web import http, server
+from twisted.web import static, resource
+from twisted.web.server import NOT_DONE_YET
+
+from logilab.common.decorators import monkeypatch
 
 from cubicweb import ConfigurationError, CW_EVENT_MANAGER
 from cubicweb.web import (AuthenticationError, NotFound, Redirect,
                           RemoteCallFailed, DirectResponse, StatusResponse,
                           ExplicitLogin)
+
 from cubicweb.web.application import CubicWebPublisher
 
 from cubicweb.etwist.request import CubicWebTwistedRequestAdapter
+from cubicweb.etwist.http import HTTPResponse
 
 def daemonize():
     # XXX unix specific
@@ -67,8 +73,20 @@
     return baseurl
 
 
-class LongTimeExpiringFile(static.File):
-    """overrides static.File and sets a far futre ``Expires`` date
+class ForbiddenDirectoryLister(resource.Resource):
+    def render(self, request):
+        return HTTPResponse(twisted_request=request,
+                            code=http.FORBIDDEN,
+                            stream='Access forbidden')
+
+class File(static.File):
+    """Prevent from listing directories"""
+    def directoryListing(self):
+        return ForbiddenDirectoryLister()
+
+
+class LongTimeExpiringFile(File):
+    """overrides static.File and sets a far future ``Expires`` date
     on the resouce.
 
     versions handling is done by serving static files by different
@@ -79,22 +97,19 @@
       etc.
 
     """
-    def renderHTTP(self, request):
+    def render(self, request):
         def setExpireHeader(response):
-            response = iweb.IResponse(response)
             # Don't provide additional resource information to error responses
             if response.code < 400:
                 # the HTTP RFC recommands not going further than 1 year ahead
                 expires = date.today() + timedelta(days=6*30)
                 response.headers.setHeader('Expires', mktime(expires.timetuple()))
             return response
-        d = maybeDeferred(super(LongTimeExpiringFile, self).renderHTTP, request)
+        d = maybeDeferred(super(LongTimeExpiringFile, self).render, request)
         return d.addCallback(setExpireHeader)
 
 
-class CubicWebRootResource(resource.PostableResource):
-    addSlash = False
-
+class CubicWebRootResource(resource.Resource):
     def __init__(self, config, debug=None):
         self.debugmode = debug
         self.config = config
@@ -104,6 +119,7 @@
         self.base_url = config['base-url']
         self.https_url = config['https-url']
         self.versioned_datadir = 'data%s' % config.instance_md5_version()
+        self.children = {}
 
     def init_publisher(self):
         config = self.config
@@ -145,35 +161,35 @@
         except select.error:
             return
 
-    def locateChild(self, request, segments):
+    def getChild(self, path, request):
         """Indicate which resource to use to process down the URL's path"""
-        if segments:
-            if segments[0] == 'https':
-                segments = segments[1:]
-            if len(segments) >= 2:
-                if segments[0] in (self.versioned_datadir, 'data', 'static'):
-                    # Anything in data/, static/ is treated as static files
-                    if segments[0] == 'static':
-                        # instance static directory
-                        datadir = self.config.static_directory
-                    elif segments[1] == 'fckeditor':
-                        fckeditordir = self.config.ext_resources['FCKEDITOR_PATH']
-                        return static.File(fckeditordir), segments[2:]
-                    else:
-                        # cube static data file
-                        datadir = self.config.locate_resource(segments[1])
-                        if datadir is None:
-                            return None, []
-                    self.debug('static file %s from %s', segments[-1], datadir)
-                    if segments[0] == 'data':
-                        return static.File(str(datadir)), segments[1:]
-                    else:
-                        return LongTimeExpiringFile(datadir), segments[1:]
-                elif segments[0] == 'fckeditor':
-                    fckeditordir = self.config.ext_resources['FCKEDITOR_PATH']
-                    return static.File(fckeditordir), segments[1:]
+        pre_path = request.prePathURL()
+        # XXX testing pre_path[0] not enough?
+        if any(s in pre_path
+               for s in (self.versioned_datadir, 'data', 'static')):
+            # Anything in data/, static/ is treated as static files
+
+            if 'static' in pre_path:
+                # instance static directory
+                datadir = self.config.static_directory
+            elif 'fckeditor' in pre_path:
+                fckeditordir = self.config.ext_resources['FCKEDITOR_PATH']
+                return File(fckeditordir)
+            else:
+                # cube static data file
+                datadir = self.config.locate_resource(path)
+                if datadir is None:
+                    return self
+            self.info('static file %s from %s', path, datadir)
+            if 'data' in pre_path:
+                return File(os.path.join(datadir, path))
+            else:
+                return LongTimeExpiringFile(datadir)
+        elif path == 'fckeditor':
+            fckeditordir = self.config.ext_resources['FCKEDITOR_PATH']
+            return File(fckeditordir)
         # Otherwise we use this single resource
-        return self, ()
+        return self
 
     def render(self, request):
         """Render a page from the root resource"""
@@ -183,7 +199,8 @@
         if self.config['profile']: # default profiler don't trace threads
             return self.render_request(request)
         else:
-            return threads.deferToThread(self.render_request, request)
+            deferred = threads.deferToThread(self.render_request, request)
+            return NOT_DONE_YET
 
     def render_request(self, request):
         origpath = request.path
@@ -209,12 +226,12 @@
         try:
             self.appli.connect(req)
         except AuthenticationError:
-            return self.request_auth(req)
+            return self.request_auth(request=req)
         except Redirect, ex:
-            return self.redirect(req, ex.location)
+            return self.redirect(request=req, location=ex.location)
         if https and req.cnx.anonymous_connection:
             # don't allow anonymous on https connection
-            return self.request_auth(req)
+            return self.request_auth(request=req)
         if self.url_rewriter is not None:
             # XXX should occur before authentication?
             try:
@@ -231,234 +248,115 @@
         except DirectResponse, ex:
             return ex.response
         except StatusResponse, ex:
-            return http.Response(stream=ex.content, code=ex.status,
-                                 headers=req.headers_out or None)
+            return HTTPResponse(stream=ex.content, code=ex.status,
+                                twisted_request=req._twreq,
+                                headers=req.headers_out)
         except RemoteCallFailed, ex:
             req.set_header('content-type', 'application/json')
-            return http.Response(stream=ex.dumps(),
-                                 code=responsecode.INTERNAL_SERVER_ERROR)
+            return HTTPResponse(twisted_request=req._twreq, code=http.INTERNAL_SERVER_ERROR,
+                                stream=ex.dumps(), headers=req.headers_out)
         except NotFound:
             result = self.appli.notfound_content(req)
-            return http.Response(stream=result, code=responsecode.NOT_FOUND,
-                                 headers=req.headers_out or None)
+            return HTTPResponse(twisted_request=req._twreq, code=http.NOT_FOUND,
+                                stream=result, headers=req.headers_out)
+
         except ExplicitLogin:  # must be before AuthenticationError
-            return self.request_auth(req)
+            return self.request_auth(request=req)
         except AuthenticationError, ex:
             if self.config['auth-mode'] == 'cookie' and getattr(ex, 'url', None):
-                return self.redirect(req, ex.url)
+                return self.redirect(request=req, location=ex.url)
             # in http we have to request auth to flush current http auth
             # information
-            return self.request_auth(req, loggedout=True)
+            return self.request_auth(request=req, loggedout=True)
         except Redirect, ex:
-            return self.redirect(req, ex.location)
+            return self.redirect(request=req, location=ex.location)
         # request may be referenced by "onetime callback", so clear its entity
         # cache to avoid memory usage
         req.drop_entity_cache()
-        return http.Response(stream=result, code=responsecode.OK,
-                             headers=req.headers_out or None)
 
-    def redirect(self, req, location):
-        req.headers_out.setHeader('location', str(location))
-        self.debug('redirecting to %s', location)
-        # 303 See other
-        return http.Response(code=303, headers=req.headers_out)
+        return HTTPResponse(twisted_request=req._twreq, code=http.OK,
+                            stream=result, headers=req.headers_out)
 
-    def request_auth(self, req, loggedout=False):
-        if self.https_url and req.base_url() != self.https_url:
-            req.headers_out.setHeader('location', self.https_url + 'login')
-            return http.Response(code=303, headers=req.headers_out)
+    def redirect(self, request, location):
+        self.debug('redirecting to %s', str(location))
+        request.headers_out.setHeader('location', str(location))
+        # 303 See other
+        return HTTPResponse(twisted_request=request._twreq, code=303,
+                            headers=request.headers_out)
+
+    def request_auth(self, request, loggedout=False):
+        if self.https_url and request.base_url() != self.https_url:
+            return self.redirect(request, self.https_url + 'login')
         if self.config['auth-mode'] == 'http':
-            code = responsecode.UNAUTHORIZED
+            code = http.UNAUTHORIZED
         else:
-            code = responsecode.FORBIDDEN
+            code = http.FORBIDDEN
         if loggedout:
-            if req.https:
-                req._base_url =  self.base_url
-                req.https = False
-            content = self.appli.loggedout_content(req)
+            if request.https:
+                request._base_url =  self.base_url
+                request.https = False
+            content = self.appli.loggedout_content(request)
         else:
-            content = self.appli.need_login_content(req)
-        return http.Response(code, req.headers_out, content)
+            content = self.appli.need_login_content(request)
+        return HTTPResponse(twisted_request=request._twreq,
+                            stream=content, code=code,
+                            headers=request.headers_out)
 
-from twisted.internet import defer
-from twisted.web2 import fileupload
+#TODO
+# # XXX max upload size in the configuration
 
-# XXX set max file size to 100Mo: put max upload size in the configuration
-# line below for twisted >= 8.0, default param value for earlier version
-resource.PostableResource.maxSize = 100*1024*1024
-def parsePOSTData(request, maxMem=100*1024, maxFields=1024,
-                  maxSize=100*1024*1024):
-    if request.stream.length == 0:
-        return defer.succeed(None)
+@monkeypatch(http.Request)
+def requestReceived(self, command, path, version):
+    """Called by channel when all data has been received.
 
-    ctype = request.headers.getHeader('content-type')
-
-    if ctype is None:
-        return defer.succeed(None)
-
-    def updateArgs(data):
-        args = data
-        request.args.update(args)
-
-    def updateArgsAndFiles(data):
-        args, files = data
-        request.args.update(args)
-        request.files.update(files)
-
-    def error(f):
-        f.trap(fileupload.MimeFormatError)
-        raise http.HTTPError(responsecode.BAD_REQUEST)
-
-    if ctype.mediaType == 'application' and ctype.mediaSubtype == 'x-www-form-urlencoded':
-        d = fileupload.parse_urlencoded(request.stream, keep_blank_values=True)
-        d.addCallbacks(updateArgs, error)
-        return d
-    elif ctype.mediaType == 'multipart' and ctype.mediaSubtype == 'form-data':
-        boundary = ctype.params.get('boundary')
-        if boundary is None:
-            return defer.fail(http.HTTPError(
-                http.StatusResponse(responsecode.BAD_REQUEST,
-                                    "Boundary not specified in Content-Type.")))
-        d = fileupload.parseMultipartFormData(request.stream, boundary,
-                                              maxMem, maxFields, maxSize)
-        d.addCallbacks(updateArgsAndFiles, error)
-        return d
+    This method is not intended for users.
+    """
+    self.content.seek(0,0)
+    self.args = {}
+    self.files = {}
+    self.stack = []
+    self.method, self.uri = command, path
+    self.clientproto = version
+    x = self.uri.split('?', 1)
+    if len(x) == 1:
+        self.path = self.uri
     else:
-        raise http.HTTPError(responsecode.BAD_REQUEST)
-
-server.parsePOSTData = parsePOSTData
+        self.path, argstring = x
+        self.args = http.parse_qs(argstring, 1)
+    # cache the client and server information, we'll need this later to be
+    # serialized and sent with the request so CGIs will work remotely
+    self.client = self.channel.transport.getPeer()
+    self.host = self.channel.transport.getHost()
+    # Argument processing
+    ctype = self.getHeader('content-type')
+    if self.method == "POST" and ctype:
+        key, pdict = parse_header(ctype)
+        if key == 'application/x-www-form-urlencoded':
+            self.args.update(http.parse_qs(self.content.read(), 1))
+        elif key == 'multipart/form-data':
+            self.content.seek(0,0)
+            form = FieldStorage(self.content, self.received_headers,
+                                environ={'REQUEST_METHOD': 'POST'},
+                                keep_blank_values=1,
+                                strict_parsing=1)
+            for key in form:
+                value = form[key]
+                if isinstance(value, list):
+                    self.args[key] = [v.value for v in value]
+                elif value.filename:
+                    if value.done != -1: # -1 is transfer has been interrupted
+                        self.files[key] = (value.filename, value.file)
+                    else:
+                        self.files[key] = (None, None)
+                else:
+                    self.args[key] = value.value
+    self.process()
 
 
 from logging import getLogger
 from cubicweb import set_log_methods
-set_log_methods(CubicWebRootResource, getLogger('cubicweb.twisted'))
-
-
-listiterator = type(iter([]))
-
-def _gc_debug(all=True):
-    import gc
-    from pprint import pprint
-    from cubicweb.appobject import AppObject
-    gc.collect()
-    count = 0
-    acount = 0
-    fcount = 0
-    rcount = 0
-    ccount = 0
-    scount = 0
-    ocount = {}
-    from rql.stmts import Union
-    from cubicweb.schema import CubicWebSchema
-    from cubicweb.rset import ResultSet
-    from cubicweb.dbapi import Connection, Cursor
-    from cubicweb.req import RequestSessionBase
-    from cubicweb.server.repository import Repository
-    from cubicweb.server.sources.native import NativeSQLSource
-    from cubicweb.server.session import Session
-    from cubicweb.devtools.testlib import CubicWebTC
-    from logilab.common.testlib import TestSuite
-    from optparse import Values
-    import types, weakref
-    for obj in gc.get_objects():
-        if isinstance(obj, RequestSessionBase):
-            count += 1
-            if isinstance(obj, Session):
-                print '   session', obj, referrers(obj, True)
-        elif isinstance(obj, AppObject):
-            acount += 1
-        elif isinstance(obj, ResultSet):
-            rcount += 1
-            #print '   rset', obj, referrers(obj)
-        elif isinstance(obj, Repository):
-            print '   REPO', obj, referrers(obj, True)
-        #elif isinstance(obj, NativeSQLSource):
-        #    print '   SOURCe', obj, referrers(obj)
-        elif isinstance(obj, CubicWebTC):
-            print '   TC', obj, referrers(obj)
-        elif isinstance(obj, TestSuite):
-            print '   SUITE', obj, referrers(obj)
-        #elif isinstance(obj, Values):
-        #    print '   values', '%#x' % id(obj), referrers(obj, True)
-        elif isinstance(obj, Connection):
-            ccount += 1
-            #print '   cnx', obj, referrers(obj)
-        #elif isinstance(obj, Cursor):
-        #    ccount += 1
-        #    print '   cursor', obj, referrers(obj)
-        elif isinstance(obj, file):
-            fcount += 1
-        #    print '   open file', file.name, file.fileno
-        elif isinstance(obj, CubicWebSchema):
-            scount += 1
-            print '   schema', obj, referrers(obj)
-        elif not isinstance(obj, (type, tuple, dict, list, set, frozenset,
-                                  weakref.ref, weakref.WeakKeyDictionary,
-                                  listiterator,
-                                  property, classmethod,
-                                  types.ModuleType, types.MemberDescriptorType,
-                                  types.FunctionType, types.MethodType)):
-            try:
-                ocount[obj.__class__] += 1
-            except KeyError:
-                ocount[obj.__class__] = 1
-            except AttributeError:
-                pass
-    if count:
-        print ' NB REQUESTS/SESSIONS', count
-    if acount:
-        print ' NB APPOBJECTS', acount
-    if ccount:
-        print ' NB CONNECTIONS', ccount
-    if rcount:
-        print ' NB RSETS', rcount
-    if scount:
-        print ' NB SCHEMAS', scount
-    if fcount:
-        print ' NB FILES', fcount
-    if all:
-        ocount = sorted(ocount.items(), key=lambda x: x[1], reverse=True)[:20]
-        pprint(ocount)
-    if gc.garbage:
-        print 'UNREACHABLE', gc.garbage
-
-def referrers(obj, showobj=False):
-    try:
-        return sorted(set((type(x), showobj and x or getattr(x, '__name__', '%#x' % id(x)))
-                          for x in _referrers(obj)))
-    except TypeError:
-        s = set()
-        unhashable = []
-        for x in _referrers(obj):
-            try:
-                s.add(x)
-            except TypeError:
-                unhashable.append(x)
-        return sorted(s) + unhashable
-
-def _referrers(obj, seen=None, level=0):
-    import gc, types
-    from cubicweb.schema import CubicWebRelationSchema, CubicWebEntitySchema
-    interesting = []
-    if seen is None:
-        seen = set()
-    for x in gc.get_referrers(obj):
-        if id(x) in seen:
-            continue
-        seen.add(id(x))
-        if isinstance(x, types.FrameType):
-            continue
-        if isinstance(x, (CubicWebRelationSchema, CubicWebEntitySchema)):
-            continue
-        if isinstance(x, (list, tuple, set, dict, listiterator)):
-            if level >= 5:
-                pass
-                #interesting.append(x)
-            else:
-                interesting += _referrers(x, seen, level+1)
-        else:
-            interesting.append(x)
-    return interesting
+LOGGER = getLogger('cubicweb.twisted')
+set_log_methods(CubicWebRootResource, LOGGER)
 
 def run(config, debug):
     # create the site
@@ -466,7 +364,7 @@
     website = server.Site(root_resource)
     # serve it via standard HTTP on port set in the configuration
     port = config['port'] or 8080
-    reactor.listenTCP(port, channel.HTTPFactory(website))
+    reactor.listenTCP(port, website)
     logger = getLogger('cubicweb.twisted')
     if not debug:
         if sys.platform == 'win32':
--- a/migration.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/migration.py	Tue Apr 06 19:08:07 2010 +0200
@@ -16,6 +16,7 @@
 from logilab.common.decorators import cached
 from logilab.common.configuration import REQUIRED, read_old_config
 from logilab.common.shellutils import ASK
+from logilab.common.changelog import Version
 
 from cubicweb import ConfigurationError
 
@@ -374,3 +375,75 @@
 from logging import getLogger
 from cubicweb import set_log_methods
 set_log_methods(MigrationHelper, getLogger('cubicweb.migration'))
+
+
+def version_strictly_lower(a, b):
+    if a:
+        a = Version(a)
+    if b:
+        b = Version(b)
+    return a < b
+
+def max_version(a, b):
+    return str(max(Version(a), Version(b)))
+
+class ConfigurationProblem(object):
+    """Each cube has its own list of dependencies on other cubes/versions.
+
+    The ConfigurationProblem is used to record the loaded cubes, then to detect
+    inconsistencies in their dependencies.
+
+    See configuration management on wikipedia for litterature.
+    """
+
+    def __init__(self, config):
+        self.cubes = {}
+        self.config = config
+
+    def add_cube(self, name, version):
+        self.cubes[name] = version
+
+    def solve(self):
+        self.warnings = []
+        self.errors = []
+        self.read_constraints()
+        for cube, versions in sorted(self.constraints.items()):
+            oper, version = None, None
+            # simplify constraints
+            if versions:
+                for constraint in versions:
+                    op, ver = constraint
+                    if oper is None:
+                        oper = op
+                        version = ver
+                    elif op == '>=' and oper == '>=':
+                        version = max_version(ver, version)
+                    else:
+                        print 'unable to handle this case', oper, version, op, ver
+            # "solve" constraint satisfaction problem
+            if cube not in self.cubes:
+                self.errors.append( ('add', cube, version) )
+            elif versions:
+                lower_strict = version_strictly_lower(self.cubes[cube], version)
+                if oper in ('>=','='):
+                    if lower_strict:
+                        self.errors.append( ('update', cube, version) )
+                else:
+                    print 'unknown operator', oper
+
+    def read_constraints(self):
+        self.constraints = {}
+        self.reverse_constraints = {}
+        for cube in self.cubes:
+            use = self.config.cube_dependencies(cube)
+            for name, constraint in use.iteritems():
+                self.constraints.setdefault(name,set())
+                if constraint:
+                    try:
+                        oper, version = constraint.split()
+                        self.constraints[name].add( (oper, version) )
+                    except:
+                        self.warnings.append(
+                            'cube %s depends on %s but constraint badly '
+                            'formatted: %s' % (cube, name, constraint))
+                self.reverse_constraints.setdefault(name, set()).add(cube)
--- a/pytestconf.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/pytestconf.py	Tue Apr 06 19:08:07 2010 +0200
@@ -5,8 +5,6 @@
 from os.path import split, splitext
 from logilab.common.pytest import PyTester
 
-from cubicweb.etwist.server import _gc_debug
-
 class CustomPyTester(PyTester):
     def testfile(self, filename, batchmode=False):
         try:
@@ -22,7 +20,6 @@
                 if getattr(cls, '__module__', None) != modname:
                     continue
                 clean_repo_test_cls(cls)
-            #_gc_debug()
 
 def clean_repo_test_cls(cls):
     if 'repo' in cls.__dict__:
--- a/rqlrewrite.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/rqlrewrite.py	Tue Apr 06 19:08:07 2010 +0200
@@ -41,15 +41,15 @@
         except KeyError:
             continue
         stinfo = var.stinfo
-        if stinfo.get('uidrels'):
+        if stinfo.get('uidrel') is not None:
             continue # eid specified, no need for additional type specification
         try:
-            typerels = rqlst.defined_vars[varname].stinfo.get('typerels')
+            typerel = rqlst.defined_vars[varname].stinfo.get('typerel')
         except KeyError:
             assert varname in rqlst.aliases
             continue
-        if newroot is rqlst and typerels:
-            mytyperel = iter(typerels).next()
+        if newroot is rqlst and typerel is not None:
+            mytyperel = typerel
         else:
             for vref in newroot.defined_vars[varname].references():
                 rel = vref.relation()
@@ -80,7 +80,7 @@
                 # tree is not annotated yet, no scope set so add the restriction
                 # to the root
                 rel = newroot.add_type_restriction(var, possibletypes)
-            stinfo['typerels'] = frozenset((rel,))
+            stinfo['typerel'] = rel
             stinfo['possibletypes'] = possibletypes
 
 
--- a/server/msplanner.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/server/msplanner.py	Tue Apr 06 19:08:07 2010 +0200
@@ -309,21 +309,20 @@
         # find for each source which variable/solution are supported
         for varname, varobj in self.rqlst.defined_vars.items():
             # if variable has an eid specified, we can get its source directly
-            # NOTE: use uidrels and not constnode to deal with "X eid IN(1,2,3,4)"
-            if varobj.stinfo['uidrels']:
-                vrels = varobj.stinfo['relations'] - varobj.stinfo['uidrels']
-                for rel in varobj.stinfo['uidrels']:
-                    for const in rel.children[1].get_nodes(Constant):
-                        eid = const.eval(self.plan.args)
-                        source = self._session.source_from_eid(eid)
-                        if vrels and not any(source.support_relation(r.r_type)
-                                             for r in vrels):
-                            self._set_source_for_term(self.system_source, varobj)
-                        else:
-                            self._set_source_for_term(source, varobj)
+            # NOTE: use uidrel and not constnode to deal with "X eid IN(1,2,3,4)"
+            if varobj.stinfo['uidrel'] is not None:
+                rel = varobj.stinfo['uidrel']
+                for const in rel.children[1].get_nodes(Constant):
+                    eid = const.eval(self.plan.args)
+                    source = self._session.source_from_eid(eid)
+                    if not any(source.support_relation(r.r_type)
+                               for r in varobj.stinfo['relations'] if not r is rel):
+                        self._set_source_for_term(self.system_source, varobj)
+                    else:
+                        self._set_source_for_term(source, varobj)
                 continue
             rels = varobj.stinfo['relations']
-            if not rels and not varobj.stinfo['typerels']:
+            if not rels and varobj.stinfo['typerel'] is None:
                 # (rare) case where the variable has no type specified nor
                 # relation accessed ex. "Any MAX(X)"
                 self._set_source_for_term(self.system_source, varobj)
@@ -700,7 +699,7 @@
                     for var in select.defined_vars.itervalues():
                         if not var in terms:
                             stinfo = var.stinfo
-                            for ovar, rtype in stinfo['attrvars']:
+                            for ovar, rtype in stinfo.get('attrvars', ()):
                                 if ovar in terms:
                                     needsel.add(var.name)
                                     terms.append(var)
@@ -778,20 +777,19 @@
             # variable is refed by an outer scope and should be substituted
             # using an 'identity' relation (else we'll get a conflict of
             # temporary tables)
-            if rhsvar in terms and not lhsvar in terms:
+            if rhsvar in terms and not lhsvar in terms and lhsvar.scope is lhsvar.stmt:
                 self._identity_substitute(rel, lhsvar, terms, needsel)
-            elif lhsvar in terms and not rhsvar in terms:
+            elif lhsvar in terms and not rhsvar in terms and rhsvar.scope is rhsvar.stmt:
                 self._identity_substitute(rel, rhsvar, terms, needsel)
 
     def _identity_substitute(self, relation, var, terms, needsel):
         newvar = self._insert_identity_variable(relation.scope, var)
-        if newvar is not None:
-            # ensure relation is using '=' operator, else we rely on a
-            # sqlgenerator side effect (it won't insert an inequality operator
-            # in this case)
-            relation.children[1].operator = '='
-            terms.append(newvar)
-            needsel.add(newvar.name)
+        # ensure relation is using '=' operator, else we rely on a
+        # sqlgenerator side effect (it won't insert an inequality operator
+        # in this case)
+        relation.children[1].operator = '='
+        terms.append(newvar)
+        needsel.add(newvar.name)
 
     def _choose_term(self, sourceterms):
         """pick one term among terms supported by a source, which will be used
@@ -1418,7 +1416,7 @@
             return False
         if not var in terms or used_in_outer_scope(var, self.current_scope):
             return False
-        if any(v for v, _ in var.stinfo['attrvars'] if not v in terms):
+        if any(v for v, _ in var.stinfo.get('attrvars', ()) if not v in terms):
             return False
         return True
 
--- a/server/mssteps.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/server/mssteps.py	Tue Apr 06 19:08:07 2010 +0200
@@ -61,7 +61,7 @@
             if not isinstance(vref, VariableRef):
                 continue
             var = vref.variable
-            if var.stinfo['attrvars']:
+            if var.stinfo.get('attrvars'):
                 for lhsvar, rtype in var.stinfo['attrvars']:
                     if lhsvar.name in srqlst.defined_vars:
                         key = '%s.%s' % (lhsvar.name, rtype)
--- a/server/querier.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/server/querier.py	Tue Apr 06 19:08:07 2010 +0200
@@ -319,16 +319,9 @@
         varkwargs = {}
         if not session.transaction_data.get('security-rqlst-cache'):
             for var in rqlst.defined_vars.itervalues():
-                for rel in var.stinfo['uidrels']:
-                    const = rel.children[1].children[0]
-                    try:
-                        varkwargs[var.name] = typed_eid(const.eval(self.args))
-                        break
-                    except AttributeError:
-                        #from rql.nodes import Function
-                        #assert isinstance(const, Function)
-                        # X eid IN(...)
-                        pass
+                if var.stinfo['constnode'] is not None:
+                    eid = var.stinfo['constnode'].eval(self.args)
+                    varkwargs[var.name] = typed_eid(eid)
         # dictionnary of variables restricted for security reason
         localchecks = {}
         restricted_vars = set()
--- a/server/rqlannotation.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/server/rqlannotation.py	Tue Apr 06 19:08:07 2010 +0200
@@ -38,7 +38,7 @@
             stinfo['invariant'] = False
             stinfo['principal'] = _select_main_var(stinfo['rhsrelations'])
             continue
-        if not stinfo['relations'] and not stinfo['typerels']:
+        if not stinfo['relations'] and stinfo['typerel'] is None:
             # Any X, Any MAX(X)...
             # those particular queries should be executed using the system
             # entities table unless there is some type restriction
@@ -80,7 +80,7 @@
                 continue
             rschema = getrschema(rel.r_type)
             if rel.optional:
-                if rel in stinfo['optrelations']:
+                if rel in stinfo.get('optrelations', ()):
                     # optional variable can't be invariant if this is the lhs
                     # variable of an inlined relation
                     if not rel in stinfo['rhsrelations'] and rschema.inlined:
@@ -296,7 +296,7 @@
     def compute(self, rqlst):
         # set domains for each variable
         for varname, var in rqlst.defined_vars.iteritems():
-            if var.stinfo['uidrels'] or \
+            if var.stinfo['uidrel'] is not None or \
                    self.eschema(rqlst.solutions[0][varname]).final:
                 ptypes = var.stinfo['possibletypes']
             else:
@@ -339,7 +339,11 @@
     def set_rel_constraint(self, term, rel, etypes_func):
         if isinstance(term, VariableRef) and self.is_ambiguous(term.variable):
             var = term.variable
-            if len(var.stinfo['relations'] - var.stinfo['typerels']) == 1 \
+            if var.stinfo['typerel'] is not None:
+                sub = 1
+            else:
+                sub = 0
+            if len(var.stinfo['relations']) - sub == 1 \
                    or rel.sqlscope is var.sqlscope or rel.r_type == 'identity':
                 self.restrict(var, frozenset(etypes_func()))
                 try:
@@ -356,7 +360,7 @@
         if isinstance(other, VariableRef) and isinstance(other.variable, Variable):
             deambiguifier = other.variable
             if not var is self.deambification_map.get(deambiguifier):
-                if not var.stinfo['typerels']:
+                if var.stinfo['typerel'] is None:
                     otheretypes = deambiguifier.stinfo['possibletypes']
                 elif not self.is_ambiguous(deambiguifier):
                     otheretypes = self.varsols[deambiguifier]
@@ -364,7 +368,7 @@
                     # we know variable won't be invariant, try to use
                     # it to deambguify the current variable
                     otheretypes = self.varsols[deambiguifier]
-            if not deambiguifier.stinfo['typerels']:
+            if deambiguifier.stinfo['typerel'] is None:
                 # if deambiguifier has no type restriction using 'is',
                 # don't record it
                 deambiguifier = None
--- a/server/sources/rql2sql.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/server/sources/rql2sql.py	Tue Apr 06 19:08:07 2010 +0200
@@ -87,7 +87,7 @@
     modified = False
     for varname in tuple(unstable):
         var = select.defined_vars[varname]
-        if not var.stinfo['optrelations']:
+        if not var.stinfo.get('optrelations'):
             continue
         modified = True
         unstable.remove(varname)
@@ -114,13 +114,13 @@
             var.stinfo['relations'].remove(rel)
             newvar.stinfo['relations'].add(newrel)
             if rel.optional in ('left', 'both'):
-                newvar.stinfo['optrelations'].add(newrel)
+                newvar.add_optional_relation(newrel)
             for vref in newrel.children[1].iget_nodes(VariableRef):
                 var = vref.variable
                 var.stinfo['relations'].add(newrel)
                 var.stinfo['rhsrelations'].add(newrel)
                 if rel.optional in ('right', 'both'):
-                    var.stinfo['optrelations'].add(newrel)
+                    var.add_optional_relation(newrel)
         # extract subquery solutions
         mysolutions = [sol.copy() for sol in solutions]
         cleanup_solutions(newselect, mysolutions)
@@ -888,7 +888,7 @@
                         condition = '%s=%s' % (lhssql, rhsconst.accept(self))
                         if relation.r_type != 'identity':
                             condition = '(%s OR %s IS NULL)' % (condition, lhssql)
-                        if not lhsvar.stinfo['optrelations']:
+                        if not lhsvar.stinfo.get('optrelations'):
                             return condition
                         self.add_outer_join_condition(lhsvar, t1, condition)
                     return
@@ -987,7 +987,7 @@
                 sql = '%s%s' % (lhssql, rhssql)
         except AttributeError:
             sql = '%s%s' % (lhssql, rhssql)
-        if lhs.variable.stinfo['optrelations']:
+        if lhs.variable.stinfo.get('optrelations'):
             self.add_outer_join_condition(lhs.variable, table, sql)
         else:
             return sql
@@ -1002,7 +1002,7 @@
         lhsvar = lhs.variable
         me_is_principal = lhsvar.stinfo.get('principal') is rel
         if me_is_principal:
-            if not lhsvar.stinfo['typerels']:
+            if lhsvar.stinfo['typerel'] is None:
                 # the variable is using the fti table, no join needed
                 jointo = None
             elif not lhsvar.name in self._varmap:
@@ -1135,7 +1135,7 @@
                 vtablename = '_' + variable.name
                 self.add_table('entities AS %s' % vtablename, vtablename)
                 sql = '%s.eid' % vtablename
-                if variable.stinfo['typerels']:
+                if variable.stinfo['typerel'] is not None:
                     # add additional restriction on entities.type column
                     pts = variable.stinfo['possibletypes']
                     if len(pts) == 1:
@@ -1297,7 +1297,7 @@
             tablealias = self._state.outer_tables[table]
             actualtables = self._state.actual_tables[-1]
         except KeyError:
-            for rel in var.stinfo['optrelations']:
+            for rel in var.stinfo.get('optrelations'):
                 self.visit_relation(rel)
             assert self._state.outer_tables
             self.add_outer_join_condition(var, table, condition)
--- a/server/test/unittest_rql2sql.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/server/test/unittest_rql2sql.py	Tue Apr 06 19:08:07 2010 +0200
@@ -1605,8 +1605,8 @@
 class removeUnsusedSolutionsTC(TestCase):
     def test_invariant_not_varying(self):
         rqlst = mock_object(defined_vars={})
-        rqlst.defined_vars['A'] = mock_object(scope=rqlst, stinfo={'optrelations':False}, _q_invariant=True)
-        rqlst.defined_vars['B'] = mock_object(scope=rqlst, stinfo={'optrelations':False}, _q_invariant=False)
+        rqlst.defined_vars['A'] = mock_object(scope=rqlst, stinfo={}, _q_invariant=True)
+        rqlst.defined_vars['B'] = mock_object(scope=rqlst, stinfo={}, _q_invariant=False)
         self.assertEquals(remove_unused_solutions(rqlst, [{'A': 'RugbyGroup', 'B': 'RugbyTeam'},
                                                           {'A': 'FootGroup', 'B': 'FootTeam'}], {}, None),
                           ([{'A': 'RugbyGroup', 'B': 'RugbyTeam'},
@@ -1616,8 +1616,8 @@
 
     def test_invariant_varying(self):
         rqlst = mock_object(defined_vars={})
-        rqlst.defined_vars['A'] = mock_object(scope=rqlst, stinfo={'optrelations':False}, _q_invariant=True)
-        rqlst.defined_vars['B'] = mock_object(scope=rqlst, stinfo={'optrelations':False}, _q_invariant=False)
+        rqlst.defined_vars['A'] = mock_object(scope=rqlst, stinfo={}, _q_invariant=True)
+        rqlst.defined_vars['B'] = mock_object(scope=rqlst, stinfo={}, _q_invariant=False)
         self.assertEquals(remove_unused_solutions(rqlst, [{'A': 'RugbyGroup', 'B': 'RugbyTeam'},
                                                           {'A': 'FootGroup', 'B': 'RugbyTeam'}], {}, None),
                           ([{'A': 'RugbyGroup', 'B': 'RugbyTeam'}], {}, set())
--- a/setup.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/setup.py	Tue Apr 06 19:08:07 2010 +0200
@@ -24,38 +24,44 @@
 import os
 import sys
 import shutil
-from distutils.core import setup
-from distutils.command import install_lib
 from os.path import isdir, exists, join, walk
 
+try:
+   if os.environ.get('NO_SETUPTOOLS'):
+      raise ImportError() # do as there is no setuptools
+   from setuptools import setup
+   from setuptools.command import install_lib
+   USE_SETUPTOOLS = True
+except ImportError:
+   from distutils.core import setup
+   from distutils.command import install_lib
+   USE_SETUPTOOLS = False
+
 # import required features
-from __pkginfo__ import modname, version, license, short_desc, long_desc, \
-     web, author, author_email
+from __pkginfo__ import modname, version, license, description, web, \
+     author, author_email
+
+if exists('README'):
+   long_description = file('README').read()
+
 # import optional features
-try:
-    from __pkginfo__ import distname
-except ImportError:
-    distname = modname
-try:
-    from __pkginfo__ import scripts
-except ImportError:
-    scripts = []
-try:
-    from __pkginfo__ import data_files
-except ImportError:
-    data_files = None
-try:
-    from __pkginfo__ import subpackage_of
-except ImportError:
-    subpackage_of = None
-try:
-    from __pkginfo__ import include_dirs
-except ImportError:
-    include_dirs = []
-try:
-    from __pkginfo__ import ext_modules
-except ImportError:
-    ext_modules = None
+import __pkginfo__
+if USE_SETUPTOOLS:
+   requires = {}
+   for entry in ("__depends__", "__recommends__"):
+      requires.update(getattr(__pkginfo__, entry, {}))
+   install_requires = [("%s %s" % (d, v and v or "")).strip()
+                       for d, v in requires.iteritems()]
+else:
+   install_requires = []
+
+distname = getattr(__pkginfo__, 'distname', modname)
+scripts = getattr(__pkginfo__, 'scripts', ())
+include_dirs = getattr(__pkginfo__, 'include_dirs', ())
+data_files = getattr(__pkginfo__, 'data_files', None)
+subpackage_of = getattr(__pkginfo__, 'subpackage_of', None)
+ext_modules = getattr(__pkginfo__, 'ext_modules', None)
+
 
 BASE_BLACKLIST = ('CVS', 'debian', 'dist', 'build', '__buildlog')
 IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc')
@@ -92,7 +98,8 @@
 
 def export(from_dir, to_dir,
            blacklist=BASE_BLACKLIST,
-           ignore_ext=IGNORED_EXTENSIONS):
+           ignore_ext=IGNORED_EXTENSIONS,
+           verbose=True):
     """make a mirror of from_dir in to_dir, omitting directories and files
     listed in the black list
     """
@@ -111,7 +118,8 @@
                 continue
             src = '%s/%s' % (directory, filename)
             dest = to_dir + src[len(from_dir):]
-            print >> sys.stderr, src, '->', dest
+            if verbose:
+               print >> sys.stderr, src, '->', dest
             if os.path.isdir(src):
                 if not exists(dest):
                     os.mkdir(dest)
@@ -154,29 +162,31 @@
                 base = modname
             for directory in include_dirs:
                 dest = join(self.install_dir, base, directory)
-                export(directory, dest)
+                export(directory, dest, verbose=False)
 
 def install(**kwargs):
     """setup entry point"""
+    if not USE_SETUPTOOLS and '--install-layout=deb' in sys.argv and \
+           sys.versioninfo < (2, 5, 4):
+       sys.argv.remove('--install-layout=deb')
+       print "W: remove '--install-layout=deb' option"
     if subpackage_of:
         package = subpackage_of + '.' + modname
         kwargs['package_dir'] = {package : '.'}
         packages = [package] + get_packages(os.getcwd(), package)
+        if USE_SETUPTOOLS:
+            kwargs['namespace_packages'] = [subpackage_of]
     else:
         kwargs['package_dir'] = {modname : '.'}
         packages = [modname] + get_packages(os.getcwd(), modname)
     kwargs['packages'] = packages
-    return setup(name = distname,
-                 version = version,
-                 license =license,
-                 description = short_desc,
-                 long_description = long_desc,
-                 author = author,
-                 author_email = author_email,
-                 url = web,
-                 scripts = ensure_scripts(scripts),
-                 data_files=data_files,
+    return setup(name=distname, version=version, license=license, url=web,
+                 description=description, long_description=long_description,
+                 author=author, author_email=author_email,
+                 scripts=ensure_scripts(scripts), data_files=data_files,
                  ext_modules=ext_modules,
+                 install_requires=install_requires,
+                 #dependency_links=["http://alain:alain@intranet.logilab.fr/~alain/"],
                  cmdclass={'install_lib': MyInstallLib},
                  **kwargs
                  )
--- a/skeleton/__pkginfo__.py.tmpl	Tue Apr 06 16:04:37 2010 +0200
+++ b/skeleton/__pkginfo__.py.tmpl	Tue Apr 06 19:08:07 2010 +0200
@@ -8,14 +8,11 @@
 version = '.'.join(str(num) for num in numversion)
 
 license = 'LCL'
-copyright = '''Copyright (c) %(year)s %(author)s.
-%(author-web-site)s -- mailto:%(author-email)s'''
 
 author = '%(author)s'
 author_email = '%(author-email)s'
 
-short_desc = '%(shortdesc)s'
-long_desc = '''%(longdesc)s'''
+description = '%(shortdesc)s'
 
 web = 'http://www.cubicweb.org/project/%%s' %% distname
 
@@ -43,8 +40,10 @@
 # Note: here, you'll need to add subdirectories if you want
 # them to be included in the debian package
 
-__depends_cubes__ = {}
 __depends__ = {'cubicweb': '>= 3.6.0'}
-__use__ = (%(dependancies)s)
-__recommend__ = ()
+__depends_cubes__ = dict( (x[len('cubicweb-):], v) for x, v in __depends__
+                          if x.startswith('cubicweb-'))
+__recommends__ = {}
+__recommends_cubes__ = dict( (x[len('cubicweb-):], v) for x, v in __recommends__
+                             if x.startswith('cubicweb-'))
 
--- a/skeleton/setup.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/skeleton/setup.py	Tue Apr 06 19:08:07 2010 +0200
@@ -1,14 +1,12 @@
 #!/usr/bin/env python
-"""
+"""Generic Setup script, takes package info from __pkginfo__.py file
 
 :organization: Logilab
 :copyright: 2001-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
-# pylint: disable-msg=W0404,W0622,W0704,W0613,W0152
-# Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE).
-# http://www.logilab.fr/ -- mailto:contact@logilab.fr
+# pylint: disable-msg=W0142,W0403,W0404,W0613,W0622,W0622,W0704,R0904,C0103,E0611
 #
 # This program is free software; you can redistribute it and/or modify it under
 # the terms of the GNU General Public License as published by the Free Software
@@ -22,36 +20,176 @@
 # You should have received a copy of the GNU General Public License along with
 # this program; if not, write to the Free Software Foundation, Inc.,
 # 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
-""" Generic Setup script, takes package info from __pkginfo__.py file """
+
+import os
+import sys
+import shutil
+from os.path import isdir, exists, join, walk
 
-from distutils.core import setup
+try:
+   if os.environ.get('NO_SETUPTOOLS'):
+      raise ImportError() # do as there is no setuptools
+   from setuptools import setup
+   from setuptools.command import install_lib
+   USE_SETUPTOOLS = True
+except ImportError:
+   from distutils.core import setup
+   from distutils.command import install_lib
+   USE_SETUPTOOLS = False
 
 # import required features
-from __pkginfo__ import distname, version, license, short_desc, long_desc, \
-     web, author, author_email
+from __pkginfo__ import modname, version, license, description, web, \
+     author, author_email
+
+if exists('README'):
+   long_description = file('README').read()
+
 # import optional features
-try:
-    from __pkginfo__ import data_files
-except ImportError:
-    data_files = None
-try:
-    from __pkginfo__ import include_dirs
-except ImportError:
-    include_dirs = []
+import __pkginfo__
+if USE_SETUPTOOLS:
+   requires = {}
+   for entry in ("__depends__", "__recommends__"):
+      requires.update(getattr(__pkginfo__, entry, {}))
+   install_requires = [("%s %s" % (d, v and v or "")).strip()
+                       for d, v in requires.iteritems()]
+else:
+   install_requires = []
+
+distname = getattr(__pkginfo__, 'distname', modname)
+scripts = getattr(__pkginfo__, 'scripts', ())
+include_dirs = getattr(__pkginfo__, 'include_dirs', ())
+data_files = getattr(__pkginfo__, 'data_files', None)
+subpackage_of = getattr(__pkginfo__, 'subpackage_of', None)
+ext_modules = getattr(__pkginfo__, 'ext_modules', None)
+
+
+BASE_BLACKLIST = ('CVS', 'debian', 'dist', 'build', '__buildlog')
+IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc')
+
+
+def ensure_scripts(linux_scripts):
+    """
+    Creates the proper script names required for each platform
+    (taken from 4Suite)
+    """
+    from distutils import util
+    if util.get_platform()[:3] == 'win':
+        scripts_ = [script + '.bat' for script in linux_scripts]
+    else:
+        scripts_ = linux_scripts
+    return scripts_
+
+
+def get_packages(directory, prefix):
+    """return a list of subpackages for the given directory
+    """
+    result = []
+    for package in os.listdir(directory):
+        absfile = join(directory, package)
+        if isdir(absfile):
+            if exists(join(absfile, '__init__.py')) or \
+                   package in ('test', 'tests'):
+                if prefix:
+                    result.append('%s.%s' % (prefix, package))
+                else:
+                    result.append(package)
+                result += get_packages(absfile, result[-1])
+    return result
+
+def export(from_dir, to_dir,
+           blacklist=BASE_BLACKLIST,
+           ignore_ext=IGNORED_EXTENSIONS,
+           verbose=True):
+    """make a mirror of from_dir in to_dir, omitting directories and files
+    listed in the black list
+    """
+    def make_mirror(arg, directory, fnames):
+        """walk handler"""
+        for norecurs in blacklist:
+            try:
+                fnames.remove(norecurs)
+            except ValueError:
+                pass
+        for filename in fnames:
+            # don't include binary files
+            if filename[-4:] in ignore_ext:
+                continue
+            if filename[-1] == '~':
+                continue
+            src = '%s/%s' % (directory, filename)
+            dest = to_dir + src[len(from_dir):]
+            if verbose:
+               print >> sys.stderr, src, '->', dest
+            if os.path.isdir(src):
+                if not exists(dest):
+                    os.mkdir(dest)
+            else:
+                if exists(dest):
+                    os.remove(dest)
+                shutil.copy2(src, dest)
+    try:
+        os.mkdir(to_dir)
+    except OSError, ex:
+        # file exists ?
+        import errno
+        if ex.errno != errno.EEXIST:
+            raise
+    walk(from_dir, make_mirror, None)
+
+
+EMPTY_FILE = '"""generated file, don\'t modify or your data will be lost"""\n'
+
+class MyInstallLib(install_lib.install_lib):
+    """extend install_lib command to handle  package __init__.py and
+    include_dirs variable if necessary
+    """
+    def run(self):
+        """overridden from install_lib class"""
+        install_lib.install_lib.run(self)
+        # create Products.__init__.py if needed
+        if subpackage_of:
+            product_init = join(self.install_dir, subpackage_of, '__init__.py')
+            if not exists(product_init):
+                self.announce('creating %s' % product_init)
+                stream = open(product_init, 'w')
+                stream.write(EMPTY_FILE)
+                stream.close()
+        # manually install included directories if any
+        if include_dirs:
+            if subpackage_of:
+                base = join(subpackage_of, modname)
+            else:
+                base = modname
+            for directory in include_dirs:
+                dest = join(self.install_dir, base, directory)
+                export(directory, dest, verbose=False)
 
 def install(**kwargs):
     """setup entry point"""
-    #kwargs['distname'] = modname
-    return setup(name=distname,
-                 version=version,
-                 license=license,
-                 description=short_desc,
-                 long_description=long_desc,
-                 author=author,
-                 author_email=author_email,
-                 url=web,
-                 data_files=data_files,
-                 **kwargs)
+    if not USE_SETUPTOOLS and '--install-layout=deb' in sys.argv and \
+           sys.versioninfo < (2, 5, 4):
+       sys.argv.remove('--install-layout=deb')
+       print "W: remove '--install-layout=deb' option"
+    if subpackage_of:
+        package = subpackage_of + '.' + modname
+        kwargs['package_dir'] = {package : '.'}
+        packages = [package] + get_packages(os.getcwd(), package)
+        if USE_SETUPTOOLS:
+            kwargs['namespace_packages'] = [subpackage_of]
+    else:
+        kwargs['package_dir'] = {modname : '.'}
+        packages = [modname] + get_packages(os.getcwd(), modname)
+    kwargs['packages'] = packages
+    return setup(name=distname, version=version, license=license, url=web,
+                 description=description, long_description=long_description,
+                 author=author, author_email=author_email,
+                 scripts=ensure_scripts(scripts), data_files=data_files,
+                 ext_modules=ext_modules,
+                 install_requires=install_requires,
+                 #dependency_links=["http://alain:alain@intranet.logilab.fr/~alain/"],
+                 cmdclass={'install_lib': MyInstallLib},
+                 **kwargs
+                 )
 
 if __name__ == '__main__' :
     install()
--- a/test/data/cubes/file/__pkginfo__.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/test/data/cubes/file/__pkginfo__.py	Tue Apr 06 19:08:07 2010 +0200
@@ -13,48 +13,3 @@
 numversion = (1, 4, 3)
 version = '.'.join(str(num) for num in numversion)
 
-license = 'LGPL'
-copyright = '''Copyright (c) 2003-2010 LOGILAB S.A. (Paris, FRANCE).
-http://www.logilab.fr/ -- mailto:contact@logilab.fr'''
-
-author = "Logilab"
-author_email = "contact@logilab.fr"
-web = ''
-
-short_desc = "Raw file support for the CubicWeb framework"
-long_desc = """CubicWeb is a entities / relations bases knowledge management system
-developped at Logilab.
-.
-This package provides schema and views to store files and images in cubicweb
-applications.
-.
-"""
-
-from os import listdir
-from os.path import join
-
-CUBES_DIR = join('share', 'cubicweb', 'cubes')
-try:
-    data_files = [
-        [join(CUBES_DIR, 'file'),
-         [fname for fname in listdir('.')
-          if fname.endswith('.py') and fname != 'setup.py']],
-        [join(CUBES_DIR, 'file', 'data'),
-         [join('data', fname) for fname in listdir('data')]],
-        [join(CUBES_DIR, 'file', 'wdoc'),
-         [join('wdoc', fname) for fname in listdir('wdoc')]],
-        [join(CUBES_DIR, 'file', 'views'),
-         [join('views', fname) for fname in listdir('views') if fname.endswith('.py')]],
-        [join(CUBES_DIR, 'file', 'i18n'),
-         [join('i18n', fname) for fname in listdir('i18n')]],
-        [join(CUBES_DIR, 'file', 'migration'),
-         [join('migration', fname) for fname in listdir('migration')]],
-        ]
-except OSError:
-    # we are in an installed directory
-    pass
-
-
-cube_eid = 20320
-# used packages
-__use__ = ()
--- a/test/unittest_cwconfig.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/test/unittest_cwconfig.py	Tue Apr 06 19:08:07 2010 +0200
@@ -7,13 +7,16 @@
 """
 import sys
 import os
+import tempfile
 from os.path import dirname, join, abspath
 
 from logilab.common.modutils import cleanup_sys_modules
-from logilab.common.testlib import TestCase, unittest_main
+from logilab.common.testlib import (TestCase, unittest_main,
+                                    with_tempdir)
 from logilab.common.changelog import Version
 
 from cubicweb.devtools import ApptestConfiguration
+from cubicweb.cwconfig import _find_prefix
 
 def unabsolutize(path):
     parts = path.split(os.sep)
@@ -32,7 +35,7 @@
         self.config._cubes = ('email', 'file')
 
     def tearDown(self):
-        os.environ.pop('CW_CUBES_PATH', None)
+        ApptestConfiguration.CUBES_PATH = []
 
     def test_reorder_cubes(self):
         # jpl depends on email and file and comment
@@ -52,7 +55,7 @@
 
     def test_reorder_cubes_recommends(self):
         from cubes.comment import __pkginfo__ as comment_pkginfo
-        comment_pkginfo.__recommend__ = ('file',)
+        comment_pkginfo.__recommends_cubes__ = {'file': None}
         try:
             # email recommends comment
             # comment recommends file
@@ -65,7 +68,7 @@
             self.assertEquals(self.config.reorder_cubes(('comment', 'forge', 'email', 'file')),
                               ('forge', 'email', 'comment', 'file'))
         finally:
-            comment_pkginfo.__use__ = ()
+            comment_pkginfo.__recommends_cubes__ = {}
 
 
 #     def test_vc_config(self):
@@ -91,11 +94,11 @@
         # make sure we don't import the email cube, but the stdlib email package
         import email
         self.assertNotEquals(dirname(email.__file__), self.config.CUBES_DIR)
-        os.environ['CW_CUBES_PATH'] = CUSTOM_CUBES_DIR
+        self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR]
         self.assertEquals(self.config.cubes_search_path(),
                           [CUSTOM_CUBES_DIR, self.config.CUBES_DIR])
-        os.environ['CW_CUBES_PATH'] = os.pathsep.join([
-            CUSTOM_CUBES_DIR, self.config.CUBES_DIR, 'unexistant'])
+        self.config.__class__.CUBES_PATH = [CUSTOM_CUBES_DIR,
+                                            self.config.CUBES_DIR, 'unexistant']
         # filter out unexistant and duplicates
         self.assertEquals(self.config.cubes_search_path(),
                           [CUSTOM_CUBES_DIR,
@@ -114,6 +117,91 @@
         from cubes import file
         self.assertEquals(file.__path__, [join(CUSTOM_CUBES_DIR, 'file')])
 
+class FindPrefixTC(TestCase):
+    def make_dirs(self, *args):
+        path = join(tempfile.tempdir, *args)
+        if not os.path.exists(path):
+            os.makedirs(path)
+        return path
+
+    def make_file(self, *args):
+        self.make_dirs(*args[: -1])
+        file_path = join(tempfile.tempdir, *args)
+        file_obj = open(file_path, 'w')
+        file_obj.write('""" None """')
+        file_obj.close()
+        return file_path
+
+    @with_tempdir
+    def test_samedir(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        self.assertEquals(_find_prefix(prefix), prefix)
+
+    @with_tempdir
+    def test_samedir_filepath(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        file_path = self.make_file('bob.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_dir_inside_prefix(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        dir_path = self.make_dirs('bob')
+        self.assertEquals(_find_prefix(dir_path), prefix)
+
+    @with_tempdir
+    def test_file_in_dir_inside_prefix(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        file_path = self.make_file('bob', 'toto.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_file_in_deeper_dir_inside_prefix(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        file_path = self.make_file('bob', 'pyves', 'alain', 'adim', 'syt', 'toto.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_multiple_candidate_prefix(self):
+        self.make_dirs('share', 'cubicweb')
+        prefix = self.make_dirs('bob')
+        self.make_dirs('bob', 'share', 'cubicweb')
+        file_path = self.make_file('bob', 'pyves', 'alain', 'adim', 'syt', 'toto.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_sister_candidate_prefix(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        self.make_dirs('bob', 'share', 'cubicweb')
+        file_path = self.make_file('bell', 'toto.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_multiple_parent_candidate_prefix(self):
+        self.make_dirs('share', 'cubicweb')
+        prefix = self.make_dirs('share', 'cubicweb', 'bob')
+        self.make_dirs('share', 'cubicweb', 'bob', 'share', 'cubicweb')
+        file_path = self.make_file('share', 'cubicweb', 'bob', 'pyves', 'alain', 'adim', 'syt', 'toto.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_upper_candidate_prefix(self):
+        prefix = tempfile.tempdir
+        self.make_dirs('share', 'cubicweb')
+        self.make_dirs('bell','bob',  'share', 'cubicweb')
+        file_path = self.make_file('bell', 'toto.py')
+        self.assertEquals(_find_prefix(file_path), prefix)
+
+    @with_tempdir
+    def test_no_prefix(self):
+        prefix = tempfile.tempdir
+        self.assertEquals(_find_prefix(prefix), sys.prefix)
 
 if __name__ == '__main__':
     unittest_main()
--- a/test/unittest_cwctl.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/test/unittest_cwctl.py	Tue Apr 06 19:08:07 2010 +0200
@@ -10,15 +10,8 @@
 from cStringIO import StringIO
 from logilab.common.testlib import TestCase, unittest_main
 
-if os.environ.get('APYCOT_ROOT'):
-    root = os.environ['APYCOT_ROOT']
-    CUBES_DIR = '%s/local/share/cubicweb/cubes/' % root
-    os.environ['CW_CUBES_PATH'] = CUBES_DIR
-    REGISTRY_DIR = '%s/etc/cubicweb.d/' % root
-    os.environ['CW_INSTANCES_DIR'] = REGISTRY_DIR
-
 from cubicweb.cwconfig import CubicWebConfiguration
-CubicWebConfiguration.load_cwctl_plugins()
+CubicWebConfiguration.load_cwctl_plugins() # XXX necessary?
 
 class CubicWebCtlTC(TestCase):
     def setUp(self):
--- a/toolsutils.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/toolsutils.py	Tue Apr 06 19:08:07 2010 +0200
@@ -184,7 +184,7 @@
                 config_file, ex)
     return config
 
-def env_path(env_var, default, name):
+def env_path(env_var, default, name, checkexists=True):
     """get a path specified in a variable or using the default value and return
     it.
 
@@ -203,8 +203,8 @@
     :raise `ConfigurationError`: if the returned path does not exist
     """
     path = environ.get(env_var, default)
-    if not exists(path):
-        raise ConfigurationError('%s path %s doesn\'t exist' % (name, path))
+    if checkexists and not exists(path):
+        raise ConfigurationError('%s directory %s doesn\'t exist' % (name, path))
     return abspath(path)
 
 
--- a/web/facet.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/web/facet.py	Tue Apr 06 19:08:07 2010 +0200
@@ -8,7 +8,6 @@
 """
 __docformat__ = "restructuredtext en"
 
-from itertools import chain
 from copy import deepcopy
 from datetime import date, datetime, timedelta
 
@@ -199,7 +198,7 @@
     # add attribute variable to selection
     rqlst.add_selected(attrvar)
     # add is restriction if necessary
-    if not mainvar.stinfo['typerels']:
+    if mainvar.stinfo['typerel'] is None:
         etypes = frozenset(sol[mainvar.name] for sol in rqlst.solutions)
         rqlst.add_type_restriction(mainvar, etypes)
     return var
@@ -228,12 +227,16 @@
         for ovarname in linkedvars:
             vargraph[ovarname].remove(trvarname)
         # remove relation using this variable
-        for rel in chain(trvar.stinfo['relations'], trvar.stinfo['typerels']):
+        for rel in trvar.stinfo['relations']:
             if rel in removed:
                 # already removed
                 continue
             rqlst.remove_node(rel)
             removed.add(rel)
+        rel = trvar.stinfo['typerel']
+        if rel is not None and not rel in removed:
+            rqlst.remove_node(rel)
+            removed.add(rel)
         # cleanup groupby clause
         if rqlst.groupby:
             for vref in rqlst.groupby[:]:
--- a/web/formfields.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/web/formfields.py	Tue Apr 06 19:08:07 2010 +0200
@@ -589,8 +589,7 @@
             # raise UnmodifiedField instead of returning None, since the later
             # will try to remove already attached file if any
             raise UnmodifiedField()
-        # skip browser submitted mime type
-        filename, _, stream = value
+        filename, stream = value
         # value is a  3-uple (filename, mimetype, stream)
         value = Binary(stream.read())
         if not value.getvalue(): # usually an unexistant file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/http_headers.py	Tue Apr 06 19:08:07 2010 +0200
@@ -0,0 +1,1542 @@
+# This file has been extracted from the abandoned TwistedWeb2 project
+# http://twistedmatrix.com/trac/wiki/TwistedWeb2
+
+
+from __future__ import generators
+
+import types, time
+from calendar import timegm
+import base64
+import re
+
+def dashCapitalize(s):
+    ''' Capitalize a string, making sure to treat - as a word seperator '''
+    return '-'.join([ x.capitalize() for x in s.split('-')])
+
+# datetime parsing and formatting
+weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+weekdayname_lower = [name.lower() for name in weekdayname]
+monthname = [None,
+             'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
+             'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+monthname_lower = [name and name.lower() for name in monthname]
+
+# HTTP Header parsing API
+
+header_case_mapping = {}
+
+def casemappingify(d):
+    global header_case_mapping
+    newd = dict([(key.lower(),key) for key in d.keys()])
+    header_case_mapping.update(newd)
+
+def lowerify(d):
+    return dict([(key.lower(),value) for key,value in d.items()])
+
+
+class HeaderHandler(object):
+    """HeaderHandler manages header generating and parsing functions.
+    """
+    HTTPParsers = {}
+    HTTPGenerators = {}
+
+    def __init__(self, parsers=None, generators=None):
+        """
+        @param parsers: A map of header names to parsing functions.
+        @type parsers: L{dict}
+
+        @param generators: A map of header names to generating functions.
+        @type generators: L{dict}
+        """
+
+        if parsers:
+            self.HTTPParsers.update(parsers)
+        if generators:
+            self.HTTPGenerators.update(generators)
+
+    def parse(self, name, header):
+        """
+        Parse the given header based on its given name.
+
+        @param name: The header name to parse.
+        @type name: C{str}
+
+        @param header: A list of unparsed headers.
+        @type header: C{list} of C{str}
+
+        @return: The return value is the parsed header representation,
+            it is dependent on the header.  See the HTTP Headers document.
+        """
+        parser = self.HTTPParsers.get(name, None)
+        if parser is None:
+            raise ValueError("No header parser for header '%s', either add one or use getHeaderRaw." % (name,))
+
+        try:
+            for p in parser:
+                # print "Parsing %s: %s(%s)" % (name, repr(p), repr(h))
+                header = p(header)
+                # if isinstance(h, types.GeneratorType):
+                #     h=list(h)
+        except ValueError,v:
+            # print v
+            header=None
+
+        return header
+
+    def generate(self, name, header):
+        """
+        Generate the given header based on its given name.
+
+        @param name: The header name to generate.
+        @type name: C{str}
+
+        @param header: A parsed header, such as the output of
+            L{HeaderHandler}.parse.
+
+        @return: C{list} of C{str} each representing a generated HTTP header.
+        """
+        generator = self.HTTPGenerators.get(name, None)
+
+        if generator is None:
+            # print self.generators
+            raise ValueError("No header generator for header '%s', either add one or use setHeaderRaw." % (name,))
+
+        for g in generator:
+            header = g(header)
+
+        #self._raw_headers[name] = h
+        return header
+
+    def updateParsers(self, parsers):
+        """Update en masse the parser maps.
+
+        @param parsers: Map of header names to parser chains.
+        @type parsers: C{dict}
+        """
+        casemappingify(parsers)
+        self.HTTPParsers.update(lowerify(parsers))
+
+    def addParser(self, name, value):
+        """Add an individual parser chain for the given header.
+
+        @param name: Name of the header to add
+        @type name: C{str}
+
+        @param value: The parser chain
+        @type value: C{str}
+        """
+        self.updateParsers({name: value})
+
+    def updateGenerators(self, generators):
+        """Update en masse the generator maps.
+
+        @param parsers: Map of header names to generator chains.
+        @type parsers: C{dict}
+        """
+        casemappingify(generators)
+        self.HTTPGenerators.update(lowerify(generators))
+
+    def addGenerators(self, name, value):
+        """Add an individual generator chain for the given header.
+
+        @param name: Name of the header to add
+        @type name: C{str}
+
+        @param value: The generator chain
+        @type value: C{str}
+        """
+        self.updateGenerators({name: value})
+
+    def update(self, parsers, generators):
+        """Conveniently update parsers and generators all at once.
+        """
+        self.updateParsers(parsers)
+        self.updateGenerators(generators)
+
+
+DefaultHTTPHandler = HeaderHandler()
+
+
+## HTTP DateTime parser
+def parseDateTime(dateString):
+    """Convert an HTTP date string (one of three formats) to seconds since epoch."""
+    parts = dateString.split()
+
+    if not parts[0][0:3].lower() in weekdayname_lower:
+        # Weekday is stupid. Might have been omitted.
+        try:
+            return parseDateTime("Sun, "+dateString)
+        except ValueError:
+            # Guess not.
+            pass
+
+    partlen = len(parts)
+    if (partlen == 5 or partlen == 6) and parts[1].isdigit():
+        # 1st date format: Sun, 06 Nov 1994 08:49:37 GMT
+        # (Note: "GMT" is literal, not a variable timezone)
+        # (also handles without "GMT")
+        # This is the normal format
+        day = parts[1]
+        month = parts[2]
+        year = parts[3]
+        time = parts[4]
+    elif (partlen == 3 or partlen == 4) and parts[1].find('-') != -1:
+        # 2nd date format: Sunday, 06-Nov-94 08:49:37 GMT
+        # (Note: "GMT" is literal, not a variable timezone)
+        # (also handles without without "GMT")
+        # Two digit year, yucko.
+        day, month, year = parts[1].split('-')
+        time = parts[2]
+        year=int(year)
+        if year < 69:
+            year = year + 2000
+        elif year < 100:
+            year = year + 1900
+    elif len(parts) == 5:
+        # 3rd date format: Sun Nov  6 08:49:37 1994
+        # ANSI C asctime() format.
+        day = parts[2]
+        month = parts[1]
+        year = parts[4]
+        time = parts[3]
+    else:
+        raise ValueError("Unknown datetime format %r" % dateString)
+
+    day = int(day)
+    month = int(monthname_lower.index(month.lower()))
+    year = int(year)
+    hour, min, sec = map(int, time.split(':'))
+    return int(timegm((year, month, day, hour, min, sec)))
+
+
+##### HTTP tokenizer
+class Token(str):
+    __slots__=[]
+    tokens = {}
+    def __new__(self, char):
+        token = Token.tokens.get(char)
+        if token is None:
+            Token.tokens[char] = token = str.__new__(self, char)
+        return token
+
+    def __repr__(self):
+        return "Token(%s)" % str.__repr__(self)
+
+
+http_tokens = " \t\"()<>@,;:\\/[]?={}"
+http_ctls = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f"
+
+def tokenize(header, foldCase=True):
+    """Tokenize a string according to normal HTTP header parsing rules.
+
+    In particular:
+     - Whitespace is irrelevant and eaten next to special separator tokens.
+       Its existance (but not amount) is important between character strings.
+     - Quoted string support including embedded backslashes.
+     - Case is insignificant (and thus lowercased), except in quoted strings.
+        (unless foldCase=False)
+     - Multiple headers are concatenated with ','
+
+    NOTE: not all headers can be parsed with this function.
+
+    Takes a raw header value (list of strings), and
+    Returns a generator of strings and Token class instances.
+    """
+    tokens=http_tokens
+    ctls=http_ctls
+
+    string = ",".join(header)
+    list = []
+    start = 0
+    cur = 0
+    quoted = False
+    qpair = False
+    inSpaces = -1
+    qstring = None
+
+    for x in string:
+        if quoted:
+            if qpair:
+                qpair = False
+                qstring = qstring+string[start:cur-1]+x
+                start = cur+1
+            elif x == '\\':
+                qpair = True
+            elif x == '"':
+                quoted = False
+                yield qstring+string[start:cur]
+                qstring=None
+                start = cur+1
+        elif x in tokens:
+            if start != cur:
+                if foldCase:
+                    yield string[start:cur].lower()
+                else:
+                    yield string[start:cur]
+
+            start = cur+1
+            if x == '"':
+                quoted = True
+                qstring = ""
+                inSpaces = False
+            elif x in " \t":
+                if inSpaces is False:
+                    inSpaces = True
+            else:
+                inSpaces = -1
+                yield Token(x)
+        elif x in ctls:
+            raise ValueError("Invalid control character: %d in header" % ord(x))
+        else:
+            if inSpaces is True:
+                yield Token(' ')
+                inSpaces = False
+
+            inSpaces = False
+        cur = cur+1
+
+    if qpair:
+        raise ValueError, "Missing character after '\\'"
+    if quoted:
+        raise ValueError, "Missing end quote"
+
+    if start != cur:
+        if foldCase:
+            yield string[start:cur].lower()
+        else:
+            yield string[start:cur]
+
+def split(seq, delim):
+    """The same as str.split but works on arbitrary sequences.
+    Too bad it's not builtin to python!"""
+
+    cur = []
+    for item in seq:
+        if item == delim:
+            yield cur
+            cur = []
+        else:
+            cur.append(item)
+    yield cur
+
+# def find(seq, *args):
+#     """The same as seq.index but returns -1 if not found, instead
+#     Too bad it's not builtin to python!"""
+#     try:
+#         return seq.index(value, *args)
+#     except ValueError:
+#         return -1
+
+
+def filterTokens(seq):
+    """Filter out instances of Token, leaving only a list of strings.
+
+    Used instead of a more specific parsing method (e.g. splitting on commas)
+    when only strings are expected, so as to be a little lenient.
+
+    Apache does it this way and has some comments about broken clients which
+    forget commas (?), so I'm doing it the same way. It shouldn't
+    hurt anything, in any case.
+    """
+
+    l=[]
+    for x in seq:
+        if not isinstance(x, Token):
+            l.append(x)
+    return l
+
+##### parser utilities:
+def checkSingleToken(tokens):
+    if len(tokens) != 1:
+        raise ValueError, "Expected single token, not %s." % (tokens,)
+    return tokens[0]
+
+def parseKeyValue(val):
+    if len(val) == 1:
+        return val[0],None
+    elif len(val) == 3 and val[1] == Token('='):
+        return val[0],val[2]
+    raise ValueError, "Expected key or key=value, but got %s." % (val,)
+
+def parseArgs(field):
+    args=split(field, Token(';'))
+    val = args.next()
+    args = [parseKeyValue(arg) for arg in args]
+    return val,args
+
+def listParser(fun):
+    """Return a function which applies 'fun' to every element in the
+    comma-separated list"""
+    def listParserHelper(tokens):
+        fields = split(tokens, Token(','))
+        for field in fields:
+            if len(field) != 0:
+                yield fun(field)
+
+    return listParserHelper
+
+def last(seq):
+    """Return seq[-1]"""
+
+    return seq[-1]
+
+##### Generation utilities
+def quoteString(s):
+    return '"%s"' % s.replace('\\', '\\\\').replace('"', '\\"')
+
+def listGenerator(fun):
+    """Return a function which applies 'fun' to every element in
+    the given list, then joins the result with generateList"""
+    def listGeneratorHelper(l):
+        return generateList([fun(e) for e in l])
+
+    return listGeneratorHelper
+
+def generateList(seq):
+    return ", ".join(seq)
+
+def singleHeader(item):
+    return [item]
+
+def generateKeyValues(kvs):
+    l = []
+    # print kvs
+    for k,v in kvs:
+        if v is None:
+            l.append('%s' % k)
+        else:
+            l.append('%s=%s' % (k,v))
+    return ";".join(l)
+
+
+class MimeType(object):
+    def fromString(klass, mimeTypeString):
+        """Generate a MimeType object from the given string.
+
+        @param mimeTypeString: The mimetype to parse
+
+        @return: L{MimeType}
+        """
+        return DefaultHTTPHandler.parse('content-type', [mimeTypeString])
+
+    fromString = classmethod(fromString)
+
+    def __init__(self, mediaType, mediaSubtype, params={}, **kwargs):
+        """
+        @type mediaType: C{str}
+
+        @type mediaSubtype: C{str}
+
+        @type params: C{dict}
+        """
+        self.mediaType = mediaType
+        self.mediaSubtype = mediaSubtype
+        self.params = dict(params)
+
+        if kwargs:
+            self.params.update(kwargs)
+
+    def __eq__(self, other):
+        if not isinstance(other, MimeType): return NotImplemented
+        return (self.mediaType == other.mediaType and
+                self.mediaSubtype == other.mediaSubtype and
+                self.params == other.params)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __repr__(self):
+        return "MimeType(%r, %r, %r)" % (self.mediaType, self.mediaSubtype, self.params)
+
+    def __hash__(self):
+        return hash(self.mediaType)^hash(self.mediaSubtype)^hash(tuple(self.params.iteritems()))
+
+##### Specific header parsers.
+def parseAccept(field):
+    type,args = parseArgs(field)
+
+    if len(type) != 3 or type[1] != Token('/'):
+        raise ValueError, "MIME Type "+str(type)+" invalid."
+
+    # okay, this spec is screwy. A 'q' parameter is used as the separator
+    # between MIME parameters and (as yet undefined) additional HTTP
+    # parameters.
+
+    num = 0
+    for arg in args:
+        if arg[0] == 'q':
+            mimeparams=tuple(args[0:num])
+            params=args[num:]
+            break
+        num = num + 1
+    else:
+        mimeparams=tuple(args)
+        params=[]
+
+    # Default values for parameters:
+    qval = 1.0
+
+    # Parse accept parameters:
+    for param in params:
+        if param[0] =='q':
+            qval = float(param[1])
+        else:
+            # Warn? ignored parameter.
+            pass
+
+    ret = MimeType(type[0],type[2],mimeparams),qval
+    return ret
+
+def parseAcceptQvalue(field):
+    type,args=parseArgs(field)
+
+    type = checkSingleToken(type)
+
+    qvalue = 1.0 # Default qvalue is 1
+    for arg in args:
+        if arg[0] == 'q':
+            qvalue = float(arg[1])
+    return type,qvalue
+
+def addDefaultCharset(charsets):
+    if charsets.get('*') is None and charsets.get('iso-8859-1') is None:
+        charsets['iso-8859-1'] = 1.0
+    return charsets
+
+def addDefaultEncoding(encodings):
+    if encodings.get('*') is None and encodings.get('identity') is None:
+        # RFC doesn't specify a default value for identity, only that it
+        # "is acceptable" if not mentioned. Thus, give it a very low qvalue.
+        encodings['identity'] = .0001
+    return encodings
+
+
+def parseContentType(header):
+    # Case folding is disabled for this header, because of use of
+    # Content-Type: multipart/form-data; boundary=CaSeFuLsTuFf
+    # So, we need to explicitly .lower() the type/subtype and arg keys.
+
+    type,args = parseArgs(header)
+
+    if len(type) != 3 or type[1] != Token('/'):
+        raise ValueError, "MIME Type "+str(type)+" invalid."
+
+    args = [(kv[0].lower(), kv[1]) for kv in args]
+
+    return MimeType(type[0].lower(), type[2].lower(), tuple(args))
+
+def parseContentMD5(header):
+    try:
+        return base64.decodestring(header)
+    except Exception,e:
+        raise ValueError(e)
+
+def parseContentRange(header):
+    """Parse a content-range header into (kind, start, end, realLength).
+
+    realLength might be None if real length is not known ('*').
+    start and end might be None if start,end unspecified (for response code 416)
+    """
+    kind, other = header.strip().split()
+    if kind.lower() != "bytes":
+        raise ValueError("a range of type %r is not supported")
+    startend, realLength = other.split("/")
+    if startend.strip() == '*':
+        start,end=None,None
+    else:
+        start, end = map(int, startend.split("-"))
+    if realLength == "*":
+        realLength = None
+    else:
+        realLength = int(realLength)
+    return (kind, start, end, realLength)
+
+def parseExpect(field):
+    type,args=parseArgs(field)
+
+    type=parseKeyValue(type)
+    return (type[0], (lambda *args:args)(type[1], *args))
+
+def parseExpires(header):
+    # """HTTP/1.1 clients and caches MUST treat other invalid date formats,
+    #    especially including the value 0, as in the past (i.e., "already expired")."""
+
+    try:
+        return parseDateTime(header)
+    except ValueError:
+        return 0
+
+def parseIfModifiedSince(header):
+    # Ancient versions of netscape and *current* versions of MSIE send
+    #   If-Modified-Since: Thu, 05 Aug 2004 12:57:27 GMT; length=123
+    # which is blantantly RFC-violating and not documented anywhere
+    # except bug-trackers for web frameworks.
+
+    # So, we'll just strip off everything after a ';'.
+    return parseDateTime(header.split(';', 1)[0])
+
+def parseIfRange(headers):
+    try:
+        return ETag.parse(tokenize(headers))
+    except ValueError:
+        return parseDateTime(last(headers))
+
+def parseRange(range):
+    range = list(range)
+    if len(range) < 3 or range[1] != Token('='):
+        raise ValueError("Invalid range header format: %s" %(range,))
+
+    type=range[0]
+    if type != 'bytes':
+        raise ValueError("Unknown range unit: %s." % (type,))
+    rangeset=split(range[2:], Token(','))
+    ranges = []
+
+    for byterangespec in rangeset:
+        if len(byterangespec) != 1:
+            raise ValueError("Invalid range header format: %s" % (range,))
+        start,end=byterangespec[0].split('-')
+
+        if not start and not end:
+            raise ValueError("Invalid range header format: %s" % (range,))
+
+        if start:
+            start = int(start)
+        else:
+            start = None
+
+        if end:
+            end = int(end)
+        else:
+            end = None
+
+        if start and end and start > end:
+            raise ValueError("Invalid range header, start > end: %s" % (range,))
+        ranges.append((start,end))
+    return type,ranges
+
+def parseRetryAfter(header):
+    try:
+        # delta seconds
+        return time.time() + int(header)
+    except ValueError:
+        # or datetime
+        return parseDateTime(header)
+
+# WWW-Authenticate and Authorization
+
+def parseWWWAuthenticate(tokenized):
+    headers = []
+
+    tokenList = list(tokenized)
+
+    while tokenList:
+        scheme = tokenList.pop(0)
+        challenge = {}
+        last = None
+        kvChallenge = False
+
+        while tokenList:
+            token = tokenList.pop(0)
+            if token == Token('='):
+                kvChallenge = True
+                challenge[last] = tokenList.pop(0)
+                last = None
+
+            elif token == Token(','):
+                if kvChallenge:
+                    if len(tokenList) > 1 and tokenList[1] != Token('='):
+                        break
+
+                else:
+                    break
+
+            else:
+                last = token
+
+        if last and scheme and not challenge and not kvChallenge:
+            challenge = last
+            last = None
+
+        headers.append((scheme, challenge))
+
+    if last and last not in (Token('='), Token(',')):
+        if headers[-1] == (scheme, challenge):
+            scheme = last
+            challenge = {}
+            headers.append((scheme, challenge))
+
+    return headers
+
+def parseAuthorization(header):
+    scheme, rest = header.split(' ', 1)
+    # this header isn't tokenized because it may eat characters
+    # in the unquoted base64 encoded credentials
+    return scheme.lower(), rest
+
+#### Header generators
+def generateAccept(accept):
+    mimeType,q = accept
+
+    out="%s/%s"%(mimeType.mediaType, mimeType.mediaSubtype)
+    if mimeType.params:
+        out+=';'+generateKeyValues(mimeType.params.iteritems())
+
+    if q != 1.0:
+        out+=(';q=%.3f' % (q,)).rstrip('0').rstrip('.')
+
+    return out
+
+def removeDefaultEncoding(seq):
+    for item in seq:
+        if item[0] != 'identity' or item[1] != .0001:
+            yield item
+
+def generateAcceptQvalue(keyvalue):
+    if keyvalue[1] == 1.0:
+        return "%s" % keyvalue[0:1]
+    else:
+        return ("%s;q=%.3f" % keyvalue).rstrip('0').rstrip('.')
+
+def parseCacheControl(kv):
+    k, v = parseKeyValue(kv)
+    if k == 'max-age' or k == 'min-fresh' or k == 's-maxage':
+        # Required integer argument
+        if v is None:
+            v = 0
+        else:
+            v = int(v)
+    elif k == 'max-stale':
+        # Optional integer argument
+        if v is not None:
+            v = int(v)
+    elif k == 'private' or k == 'no-cache':
+        # Optional list argument
+        if v is not None:
+            v = [field.strip().lower() for field in v.split(',')]
+    return k, v
+
+def generateCacheControl((k, v)):
+    if v is None:
+        return str(k)
+    else:
+        if k == 'no-cache' or k == 'private':
+            # quoted list of values
+            v = quoteString(generateList(
+                [header_case_mapping.get(name) or dashCapitalize(name) for name in v]))
+        return '%s=%s' % (k,v)
+
+def generateContentRange(tup):
+    """tup is (type, start, end, len)
+    len can be None.
+    """
+    type, start, end, len = tup
+    if len == None:
+        len = '*'
+    else:
+        len = int(len)
+    if start == None and end == None:
+        startend = '*'
+    else:
+        startend = '%d-%d' % (start, end)
+
+    return '%s %s/%s' % (type, startend, len)
+
+def generateDateTime(secSinceEpoch):
+    """Convert seconds since epoch to HTTP datetime string."""
+    year, month, day, hh, mm, ss, wd, y, z = time.gmtime(secSinceEpoch)
+    s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
+        weekdayname[wd],
+        day, monthname[month], year,
+        hh, mm, ss)
+    return s
+
+def generateExpect(item):
+    if item[1][0] is None:
+        out = '%s' % (item[0],)
+    else:
+        out = '%s=%s' % (item[0], item[1][0])
+    if len(item[1]) > 1:
+        out += ';'+generateKeyValues(item[1][1:])
+    return out
+
+def generateRange(range):
+    def noneOr(s):
+        if s is None:
+            return ''
+        return s
+
+    type,ranges=range
+
+    if type != 'bytes':
+        raise ValueError("Unknown range unit: "+type+".")
+
+    return (type+'='+
+            ','.join(['%s-%s' % (noneOr(startend[0]), noneOr(startend[1]))
+                      for startend in ranges]))
+
+def generateRetryAfter(when):
+    # always generate delta seconds format
+    return str(int(when - time.time()))
+
+def generateContentType(mimeType):
+    out="%s/%s"%(mimeType.mediaType, mimeType.mediaSubtype)
+    if mimeType.params:
+        out+=';'+generateKeyValues(mimeType.params.iteritems())
+    return out
+
+def generateIfRange(dateOrETag):
+    if isinstance(dateOrETag, ETag):
+        return dateOrETag.generate()
+    else:
+        return generateDateTime(dateOrETag)
+
+# WWW-Authenticate and Authorization
+
+def generateWWWAuthenticate(headers):
+    _generated = []
+    for seq in headers:
+        scheme, challenge = seq[0], seq[1]
+
+        # If we're going to parse out to something other than a dict
+        # we need to be able to generate from something other than a dict
+
+        try:
+            l = []
+            for k,v in dict(challenge).iteritems():
+                l.append("%s=%s" % (k, quoteString(v)))
+
+            _generated.append("%s %s" % (scheme, ", ".join(l)))
+        except ValueError:
+            _generated.append("%s %s" % (scheme, challenge))
+
+    return _generated
+
+def generateAuthorization(seq):
+    return [' '.join(seq)]
+
+
+####
+class ETag(object):
+    def __init__(self, tag, weak=False):
+        self.tag = str(tag)
+        self.weak = weak
+
+    def match(self, other, strongCompare):
+        # Sec 13.3.
+        # The strong comparison function: in order to be considered equal, both
+        #   validators MUST be identical in every way, and both MUST NOT be weak.
+        #
+        # The weak comparison function: in order to be considered equal, both
+        #   validators MUST be identical in every way, but either or both of
+        #   them MAY be tagged as "weak" without affecting the result.
+
+        if not isinstance(other, ETag) or other.tag != self.tag:
+            return False
+
+        if strongCompare and (other.weak or self.weak):
+            return False
+        return True
+
+    def __eq__(self, other):
+        return isinstance(other, ETag) and other.tag == self.tag and other.weak == self.weak
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+    def __repr__(self):
+        return "Etag(%r, weak=%r)" % (self.tag, self.weak)
+
+    def parse(tokens):
+        tokens=tuple(tokens)
+        if len(tokens) == 1 and not isinstance(tokens[0], Token):
+            return ETag(tokens[0])
+
+        if(len(tokens) == 3 and tokens[0] == "w"
+           and tokens[1] == Token('/')):
+            return ETag(tokens[2], weak=True)
+
+        raise ValueError("Invalid ETag.")
+
+    parse=staticmethod(parse)
+
+    def generate(self):
+        if self.weak:
+            return 'W/'+quoteString(self.tag)
+        else:
+            return quoteString(self.tag)
+
+def parseStarOrETag(tokens):
+    tokens=tuple(tokens)
+    if tokens == ('*',):
+        return '*'
+    else:
+        return ETag.parse(tokens)
+
+def generateStarOrETag(etag):
+    if etag=='*':
+        return etag
+    else:
+        return etag.generate()
+
+#### Cookies. Blech!
+class Cookie(object):
+    # __slots__ = ['name', 'value', 'path', 'domain', 'ports', 'expires', 'discard', 'secure', 'comment', 'commenturl', 'version']
+
+    def __init__(self, name, value, path=None, domain=None, ports=None, expires=None, discard=False, secure=False, comment=None, commenturl=None, version=0):
+        self.name=name
+        self.value=value
+        self.path=path
+        self.domain=domain
+        self.ports=ports
+        self.expires=expires
+        self.discard=discard
+        self.secure=secure
+        self.comment=comment
+        self.commenturl=commenturl
+        self.version=version
+
+    def __repr__(self):
+        s="Cookie(%r=%r" % (self.name, self.value)
+        if self.path is not None: s+=", path=%r" % (self.path,)
+        if self.domain is not None: s+=", domain=%r" % (self.domain,)
+        if self.ports is not None: s+=", ports=%r" % (self.ports,)
+        if self.expires is not None: s+=", expires=%r" % (self.expires,)
+        if self.secure is not False: s+=", secure=%r" % (self.secure,)
+        if self.comment is not None: s+=", comment=%r" % (self.comment,)
+        if self.commenturl is not None: s+=", commenturl=%r" % (self.commenturl,)
+        if self.version != 0: s+=", version=%r" % (self.version,)
+        s+=")"
+        return s
+
+    def __eq__(self, other):
+        return (isinstance(other, Cookie) and
+                other.path == self.path and
+                other.domain == self.domain and
+                other.ports == self.ports and
+                other.expires == self.expires and
+                other.secure == self.secure and
+                other.comment == self.comment and
+                other.commenturl == self.commenturl and
+                other.version == self.version)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+def parseCookie(headers):
+    """Bleargh, the cookie spec sucks.
+    This surely needs interoperability testing.
+    There are two specs that are supported:
+    Version 0) http://wp.netscape.com/newsref/std/cookie_spec.html
+    Version 1) http://www.faqs.org/rfcs/rfc2965.html
+    """
+
+    cookies = []
+    # There can't really be multiple cookie headers according to RFC, because
+    # if multiple headers are allowed, they must be joinable with ",".
+    # Neither new RFC2965 cookies nor old netscape cookies are.
+
+    header = ';'.join(headers)
+    if header[0:8].lower() == "$version":
+        # RFC2965 cookie
+        h=tokenize([header], foldCase=False)
+        r_cookies = split(h, Token(','))
+        for r_cookie in r_cookies:
+            last_cookie = None
+            rr_cookies = split(r_cookie, Token(';'))
+            for cookie in rr_cookies:
+                nameval = tuple(split(cookie, Token('=')))
+                if len(nameval) == 2:
+                    (name,), (value,) = nameval
+                else:
+                    (name,), = nameval
+                    value = None
+
+                name=name.lower()
+                if name == '$version':
+                    continue
+                if name[0] == '$':
+                    if last_cookie is not None:
+                        if name == '$path':
+                            last_cookie.path=value
+                        elif name == '$domain':
+                            last_cookie.domain=value
+                        elif name == '$port':
+                            if value is None:
+                                last_cookie.ports = ()
+                            else:
+                                last_cookie.ports=tuple([int(s) for s in value.split(',')])
+                else:
+                    last_cookie = Cookie(name, value, version=1)
+                    cookies.append(last_cookie)
+    else:
+        # Oldstyle cookies don't do quoted strings or anything sensible.
+        # All characters are valid for names except ';' and '=', and all
+        # characters are valid for values except ';'. Spaces are stripped,
+        # however.
+        r_cookies = header.split(';')
+        for r_cookie in r_cookies:
+            name,value = r_cookie.split('=', 1)
+            name=name.strip(' \t')
+            value=value.strip(' \t')
+
+            cookies.append(Cookie(name, value))
+
+    return cookies
+
+cookie_validname = "[^"+re.escape(http_tokens+http_ctls)+"]*$"
+cookie_validname_re = re.compile(cookie_validname)
+cookie_validvalue = cookie_validname+'|"([^"]|\\\\")*"$'
+cookie_validvalue_re = re.compile(cookie_validvalue)
+
+def generateCookie(cookies):
+    # There's a fundamental problem with the two cookie specifications.
+    # They both use the "Cookie" header, and the RFC Cookie header only allows
+    # one version to be specified. Thus, when you have a collection of V0 and
+    # V1 cookies, you have to either send them all as V0 or send them all as
+    # V1.
+
+    # I choose to send them all as V1.
+
+    # You might think converting a V0 cookie to a V1 cookie would be lossless,
+    # but you'd be wrong. If you do the conversion, and a V0 parser tries to
+    # read the cookie, it will see a modified form of the cookie, in cases
+    # where quotes must be added to conform to proper V1 syntax.
+    # (as a real example: "Cookie: cartcontents=oid:94680,qty:1,auto:0,esp:y")
+
+    # However, that is what we will do, anyways. It has a high probability of
+    # breaking applications that only handle oldstyle cookies, where some other
+    # application set a newstyle cookie that is applicable over for site
+    # (or host), AND where the oldstyle cookie uses a value which is invalid
+    # syntax in a newstyle cookie.
+
+    # Also, the cookie name *cannot* be quoted in V1, so some cookies just
+    # cannot be converted at all. (e.g. "Cookie: phpAds_capAd[32]=2"). These
+    # are just dicarded during conversion.
+
+    # As this is an unsolvable problem, I will pretend I can just say
+    # OH WELL, don't do that, or else upgrade your old applications to have
+    # newstyle cookie parsers.
+
+    # I will note offhandedly that there are *many* sites which send V0 cookies
+    # that are not valid V1 cookie syntax. About 20% for my cookies file.
+    # However, they do not generally mix them with V1 cookies, so this isn't
+    # an issue, at least right now. I have not tested to see how many of those
+    # webapps support RFC2965 V1 cookies. I suspect not many.
+
+    max_version = max([cookie.version for cookie in cookies])
+
+    if max_version == 0:
+        # no quoting or anything.
+        return ';'.join(["%s=%s" % (cookie.name, cookie.value) for cookie in cookies])
+    else:
+        str_cookies = ['$Version="1"']
+        for cookie in cookies:
+            if cookie.version == 0:
+                # Version 0 cookie: we make sure the name and value are valid
+                # V1 syntax.
+
+                # If they are, we use them as is. This means in *most* cases,
+                # the cookie will look literally the same on output as it did
+                # on input.
+                # If it isn't a valid name, ignore the cookie.
+                # If it isn't a valid value, quote it and hope for the best on
+                # the other side.
+
+                if cookie_validname_re.match(cookie.name) is None:
+                    continue
+
+                value=cookie.value
+                if cookie_validvalue_re.match(cookie.value) is None:
+                    value = quoteString(value)
+
+                str_cookies.append("%s=%s" % (cookie.name, value))
+            else:
+                # V1 cookie, nice and easy
+                str_cookies.append("%s=%s" % (cookie.name, quoteString(cookie.value)))
+
+            if cookie.path:
+                str_cookies.append("$Path=%s" % quoteString(cookie.path))
+            if cookie.domain:
+                str_cookies.append("$Domain=%s" % quoteString(cookie.domain))
+            if cookie.ports is not None:
+                if len(cookie.ports) == 0:
+                    str_cookies.append("$Port")
+                else:
+                    str_cookies.append("$Port=%s" % quoteString(",".join([str(x) for x in cookie.ports])))
+        return ';'.join(str_cookies)
+
+def parseSetCookie(headers):
+    setCookies = []
+    for header in headers:
+        try:
+            parts = header.split(';')
+            l = []
+
+            for part in parts:
+                namevalue = part.split('=',1)
+                if len(namevalue) == 1:
+                    name=namevalue[0]
+                    value=None
+                else:
+                    name,value=namevalue
+                    value=value.strip(' \t')
+
+                name=name.strip(' \t')
+
+                l.append((name, value))
+
+            setCookies.append(makeCookieFromList(l, True))
+        except ValueError:
+            # If we can't parse one Set-Cookie, ignore it,
+            # but not the rest of Set-Cookies.
+            pass
+    return setCookies
+
+def parseSetCookie2(toks):
+    outCookies = []
+    for cookie in [[parseKeyValue(x) for x in split(y, Token(';'))]
+                   for y in split(toks, Token(','))]:
+        try:
+            outCookies.append(makeCookieFromList(cookie, False))
+        except ValueError:
+            # Again, if we can't handle one cookie -- ignore it.
+            pass
+    return outCookies
+
+def makeCookieFromList(tup, netscapeFormat):
+    name, value = tup[0]
+    if name is None or value is None:
+        raise ValueError("Cookie has missing name or value")
+    if name.startswith("$"):
+        raise ValueError("Invalid cookie name: %r, starts with '$'." % name)
+    cookie = Cookie(name, value)
+    hadMaxAge = False
+
+    for name,value in tup[1:]:
+        name = name.lower()
+
+        if value is None:
+            if name in ("discard", "secure"):
+                # Boolean attrs
+                value = True
+            elif name != "port":
+                # Can be either boolean or explicit
+                continue
+
+        if name in ("comment", "commenturl", "discard", "domain", "path", "secure"):
+            # simple cases
+            setattr(cookie, name, value)
+        elif name == "expires" and not hadMaxAge:
+            if netscapeFormat and value[0] == '"' and value[-1] == '"':
+                value = value[1:-1]
+            cookie.expires = parseDateTime(value)
+        elif name == "max-age":
+            hadMaxAge = True
+            cookie.expires = int(value) + time.time()
+        elif name == "port":
+            if value is None:
+                cookie.ports = ()
+            else:
+                if netscapeFormat and value[0] == '"' and value[-1] == '"':
+                    value = value[1:-1]
+                cookie.ports = tuple([int(s) for s in value.split(',')])
+        elif name == "version":
+            cookie.version = int(value)
+
+    return cookie
+
+
+def generateSetCookie(cookies):
+    setCookies = []
+    for cookie in cookies:
+        out = ["%s=%s" % (cookie.name, cookie.value)]
+        if cookie.expires:
+            out.append("expires=%s" % generateDateTime(cookie.expires))
+        if cookie.path:
+            out.append("path=%s" % cookie.path)
+        if cookie.domain:
+            out.append("domain=%s" % cookie.domain)
+        if cookie.secure:
+            out.append("secure")
+
+        setCookies.append('; '.join(out))
+    return setCookies
+
+def generateSetCookie2(cookies):
+    setCookies = []
+    for cookie in cookies:
+        out = ["%s=%s" % (cookie.name, quoteString(cookie.value))]
+        if cookie.comment:
+            out.append("Comment=%s" % quoteString(cookie.comment))
+        if cookie.commenturl:
+            out.append("CommentURL=%s" % quoteString(cookie.commenturl))
+        if cookie.discard:
+            out.append("Discard")
+        if cookie.domain:
+            out.append("Domain=%s" % quoteString(cookie.domain))
+        if cookie.expires:
+            out.append("Max-Age=%s" % (cookie.expires - time.time()))
+        if cookie.path:
+            out.append("Path=%s" % quoteString(cookie.path))
+        if cookie.ports is not None:
+            if len(cookie.ports) == 0:
+                out.append("Port")
+            else:
+                out.append("Port=%s" % quoteString(",".join([str(x) for x in cookie.ports])))
+        if cookie.secure:
+            out.append("Secure")
+        out.append('Version="1"')
+        setCookies.append('; '.join(out))
+    return setCookies
+
+def parseDepth(depth):
+    if depth not in ("0", "1", "infinity"):
+        raise ValueError("Invalid depth header value: %s" % (depth,))
+    return depth
+
+def parseOverWrite(overwrite):
+    if overwrite == "F":
+        return False
+    elif overwrite == "T":
+        return True
+    raise ValueError("Invalid overwrite header value: %s" % (overwrite,))
+
+def generateOverWrite(overwrite):
+    if overwrite:
+        return "T"
+    else:
+        return "F"
+
+##### Random stuff that looks useful.
+# def sortMimeQuality(s):
+#     def sorter(item1, item2):
+#         if item1[0] == '*':
+#             if item2[0] == '*':
+#                 return 0
+
+
+# def sortQuality(s):
+#     def sorter(item1, item2):
+#         if item1[1] < item2[1]:
+#             return -1
+#         if item1[1] < item2[1]:
+#             return 1
+#         if item1[0] == item2[0]:
+#             return 0
+
+
+# def getMimeQuality(mimeType, accepts):
+#     type,args = parseArgs(mimeType)
+#     type=type.split(Token('/'))
+#     if len(type) != 2:
+#         raise ValueError, "MIME Type "+s+" invalid."
+
+#     for accept in accepts:
+#         accept,acceptQual=accept
+#         acceptType=accept[0:1]
+#         acceptArgs=accept[2]
+
+#         if ((acceptType == type or acceptType == (type[0],'*') or acceptType==('*','*')) and
+#             (args == acceptArgs or len(acceptArgs) == 0)):
+#             return acceptQual
+
+# def getQuality(type, accepts):
+#     qual = accepts.get(type)
+#     if qual is not None:
+#         return qual
+
+#     return accepts.get('*')
+
+# Headers object
+class __RecalcNeeded(object):
+    def __repr__(self):
+        return "<RecalcNeeded>"
+
+_RecalcNeeded = __RecalcNeeded()
+
+class Headers(object):
+    """This class stores the HTTP headers as both a parsed representation and
+    the raw string representation. It converts between the two on demand."""
+
+    def __init__(self, headers=None, rawHeaders=None, handler=DefaultHTTPHandler):
+        self._raw_headers = {}
+        self._headers = {}
+        self.handler = handler
+        if headers is not None:
+            for key, value in headers.iteritems():
+                self.setHeader(key, value)
+        if rawHeaders is not None:
+            for key, value in rawHeaders.iteritems():
+                self.setRawHeaders(key, value)
+
+    def _setRawHeaders(self, headers):
+        self._raw_headers = headers
+        self._headers = {}
+
+    def _toParsed(self, name):
+        r = self._raw_headers.get(name, None)
+        h = self.handler.parse(name, r)
+        if h is not None:
+            self._headers[name] = h
+        return h
+
+    def _toRaw(self, name):
+        h = self._headers.get(name, None)
+        r = self.handler.generate(name, h)
+        if r is not None:
+            self._raw_headers[name] = r
+        return r
+
+    def hasHeader(self, name):
+        """Does a header with the given name exist?"""
+        name=name.lower()
+        return self._raw_headers.has_key(name)
+
+    def getRawHeaders(self, name, default=None):
+        """Returns a list of headers matching the given name as the raw string given."""
+
+        name=name.lower()
+        raw_header = self._raw_headers.get(name, default)
+        if raw_header is not _RecalcNeeded:
+            return raw_header
+
+        return self._toRaw(name)
+
+    def getHeader(self, name, default=None):
+        """Ret9urns the parsed representation of the given header.
+        The exact form of the return value depends on the header in question.
+
+        If no parser for the header exists, raise ValueError.
+
+        If the header doesn't exist, return default (or None if not specified)
+        """
+        name=name.lower()
+        parsed = self._headers.get(name, default)
+        if parsed is not _RecalcNeeded:
+            return parsed
+        return self._toParsed(name)
+
+    def setRawHeaders(self, name, value):
+        """Sets the raw representation of the given header.
+        Value should be a list of strings, each being one header of the
+        given name.
+        """
+        name=name.lower()
+        self._raw_headers[name] = value
+        self._headers[name] = _RecalcNeeded
+
+    def setHeader(self, name, value):
+        """Sets the parsed representation of the given header.
+        Value should be a list of objects whose exact form depends
+        on the header in question.
+        """
+        name=name.lower()
+        self._raw_headers[name] = _RecalcNeeded
+        self._headers[name] = value
+
+    def addRawHeader(self, name, value):
+        """
+        Add a raw value to a header that may or may not already exist.
+        If it exists, add it as a separate header to output; do not
+        replace anything.
+        """
+        name=name.lower()
+        raw_header = self._raw_headers.get(name)
+        if raw_header is None:
+            # No header yet
+            raw_header = []
+            self._raw_headers[name] = raw_header
+        elif raw_header is _RecalcNeeded:
+            raw_header = self._toRaw(name)
+
+        raw_header.append(value)
+        self._headers[name] = _RecalcNeeded
+
+    def removeHeader(self, name):
+        """Removes the header named."""
+
+        name=name.lower()
+        if self._raw_headers.has_key(name):
+            del self._raw_headers[name]
+            del self._headers[name]
+
+    def __repr__(self):
+        return '<Headers: Raw: %s Parsed: %s>'% (self._raw_headers, self._headers)
+
+    def canonicalNameCaps(self, name):
+        """Return the name with the canonical capitalization, if known,
+        otherwise, Caps-After-Dashes"""
+        return header_case_mapping.get(name) or dashCapitalize(name)
+
+    def getAllRawHeaders(self):
+        """Return an iterator of key,value pairs of all headers
+        contained in this object, as strings. The keys are capitalized
+        in canonical capitalization."""
+        for k,v in self._raw_headers.iteritems():
+            if v is _RecalcNeeded:
+                v = self._toRaw(k)
+            yield self.canonicalNameCaps(k), v
+
+    def makeImmutable(self):
+        """Make this header set immutable. All mutating operations will
+        raise an exception."""
+        self.setHeader = self.setRawHeaders = self.removeHeader = self._mutateRaise
+
+    def _mutateRaise(self, *args):
+        raise AttributeError("This header object is immutable as the headers have already been sent.")
+
+
+"""The following dicts are all mappings of header to list of operations
+   to perform. The first operation should generally be 'tokenize' if the
+   header can be parsed according to the normal tokenization rules. If
+   it cannot, generally the first thing you want to do is take only the
+   last instance of the header (in case it was sent multiple times, which
+   is strictly an error, but we're nice.).
+   """
+
+iteritems = lambda x: x.iteritems()
+
+
+parser_general_headers = {
+    'Cache-Control':(tokenize, listParser(parseCacheControl), dict),
+    'Connection':(tokenize,filterTokens),
+    'Date':(last,parseDateTime),
+#    'Pragma':tokenize
+#    'Trailer':tokenize
+    'Transfer-Encoding':(tokenize,filterTokens),
+#    'Upgrade':tokenize
+#    'Via':tokenize,stripComment
+#    'Warning':tokenize
+}
+
+generator_general_headers = {
+    'Cache-Control':(iteritems, listGenerator(generateCacheControl), singleHeader),
+    'Connection':(generateList,singleHeader),
+    'Date':(generateDateTime,singleHeader),
+#    'Pragma':
+#    'Trailer':
+    'Transfer-Encoding':(generateList,singleHeader),
+#    'Upgrade':
+#    'Via':
+#    'Warning':
+}
+
+parser_request_headers = {
+    'Accept': (tokenize, listParser(parseAccept), dict),
+    'Accept-Charset': (tokenize, listParser(parseAcceptQvalue), dict, addDefaultCharset),
+    'Accept-Encoding':(tokenize, listParser(parseAcceptQvalue), dict, addDefaultEncoding),
+    'Accept-Language':(tokenize, listParser(parseAcceptQvalue), dict),
+    'Authorization': (last, parseAuthorization),
+    'Cookie':(parseCookie,),
+    'Expect':(tokenize, listParser(parseExpect), dict),
+    'From':(last,),
+    'Host':(last,),
+    'If-Match':(tokenize, listParser(parseStarOrETag), list),
+    'If-Modified-Since':(last, parseIfModifiedSince),
+    'If-None-Match':(tokenize, listParser(parseStarOrETag), list),
+    'If-Range':(parseIfRange,),
+    'If-Unmodified-Since':(last,parseDateTime),
+    'Max-Forwards':(last,int),
+#    'Proxy-Authorization':str, # what is "credentials"
+    'Range':(tokenize, parseRange),
+    'Referer':(last,str), # TODO: URI object?
+    'TE':(tokenize, listParser(parseAcceptQvalue), dict),
+    'User-Agent':(last,str),
+}
+
+generator_request_headers = {
+    'Accept': (iteritems,listGenerator(generateAccept),singleHeader),
+    'Accept-Charset': (iteritems, listGenerator(generateAcceptQvalue),singleHeader),
+    'Accept-Encoding': (iteritems, removeDefaultEncoding, listGenerator(generateAcceptQvalue),singleHeader),
+    'Accept-Language': (iteritems, listGenerator(generateAcceptQvalue),singleHeader),
+    'Authorization': (generateAuthorization,), # what is "credentials"
+    'Cookie':(generateCookie,singleHeader),
+    'Expect':(iteritems, listGenerator(generateExpect), singleHeader),
+    'From':(str,singleHeader),
+    'Host':(str,singleHeader),
+    'If-Match':(listGenerator(generateStarOrETag), singleHeader),
+    'If-Modified-Since':(generateDateTime,singleHeader),
+    'If-None-Match':(listGenerator(generateStarOrETag), singleHeader),
+    'If-Range':(generateIfRange, singleHeader),
+    'If-Unmodified-Since':(generateDateTime,singleHeader),
+    'Max-Forwards':(str, singleHeader),
+#    'Proxy-Authorization':str, # what is "credentials"
+    'Range':(generateRange,singleHeader),
+    'Referer':(str,singleHeader),
+    'TE': (iteritems, listGenerator(generateAcceptQvalue),singleHeader),
+    'User-Agent':(str,singleHeader),
+}
+
+parser_response_headers = {
+    'Accept-Ranges':(tokenize, filterTokens),
+    'Age':(last,int),
+    'ETag':(tokenize, ETag.parse),
+    'Location':(last,), # TODO: URI object?
+#    'Proxy-Authenticate'
+    'Retry-After':(last, parseRetryAfter),
+    'Server':(last,),
+    'Set-Cookie':(parseSetCookie,),
+    'Set-Cookie2':(tokenize, parseSetCookie2),
+    'Vary':(tokenize, filterTokens),
+    'WWW-Authenticate': (lambda h: tokenize(h, foldCase=False),
+                         parseWWWAuthenticate,)
+}
+
+generator_response_headers = {
+    'Accept-Ranges':(generateList, singleHeader),
+    'Age':(str, singleHeader),
+    'ETag':(ETag.generate, singleHeader),
+    'Location':(str, singleHeader),
+#    'Proxy-Authenticate'
+    'Retry-After':(generateRetryAfter, singleHeader),
+    'Server':(str, singleHeader),
+    'Set-Cookie':(generateSetCookie,),
+    'Set-Cookie2':(generateSetCookie2,),
+    'Vary':(generateList, singleHeader),
+    'WWW-Authenticate':(generateWWWAuthenticate,)
+}
+
+parser_entity_headers = {
+    'Allow':(lambda str:tokenize(str, foldCase=False), filterTokens),
+    'Content-Encoding':(tokenize, filterTokens),
+    'Content-Language':(tokenize, filterTokens),
+    'Content-Length':(last, int),
+    'Content-Location':(last,), # TODO: URI object?
+    'Content-MD5':(last, parseContentMD5),
+    'Content-Range':(last, parseContentRange),
+    'Content-Type':(lambda str:tokenize(str, foldCase=False), parseContentType),
+    'Expires':(last, parseExpires),
+    'Last-Modified':(last, parseDateTime),
+    }
+
+generator_entity_headers = {
+    'Allow':(generateList, singleHeader),
+    'Content-Encoding':(generateList, singleHeader),
+    'Content-Language':(generateList, singleHeader),
+    'Content-Length':(str, singleHeader),
+    'Content-Location':(str, singleHeader),
+    'Content-MD5':(base64.encodestring, lambda x: x.strip("\n"), singleHeader),
+    'Content-Range':(generateContentRange, singleHeader),
+    'Content-Type':(generateContentType, singleHeader),
+    'Expires':(generateDateTime, singleHeader),
+    'Last-Modified':(generateDateTime, singleHeader),
+    }
+
+DefaultHTTPHandler.updateParsers(parser_general_headers)
+DefaultHTTPHandler.updateParsers(parser_request_headers)
+DefaultHTTPHandler.updateParsers(parser_response_headers)
+DefaultHTTPHandler.updateParsers(parser_entity_headers)
+
+DefaultHTTPHandler.updateGenerators(generator_general_headers)
+DefaultHTTPHandler.updateGenerators(generator_request_headers)
+DefaultHTTPHandler.updateGenerators(generator_response_headers)
+DefaultHTTPHandler.updateGenerators(generator_entity_headers)
+
+
+# casemappingify(DefaultHTTPParsers)
+# casemappingify(DefaultHTTPGenerators)
+
+# lowerify(DefaultHTTPParsers)
+# lowerify(DefaultHTTPGenerators)
--- a/web/httpcache.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/web/httpcache.py	Tue Apr 06 19:08:07 2010 +0200
@@ -131,8 +131,5 @@
 # max-age=0 to actually force revalidation when needed
 viewmod.View.cache_max_age = 0
 
-
-viewmod.EntityView.http_cache_manager = EntityHTTPCacheManager
-
 viewmod.StartupView.http_cache_manager = MaxAgeHTTPCacheManager
 viewmod.StartupView.cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
--- a/web/request.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/web/request.py	Tue Apr 06 19:08:07 2010 +0200
@@ -31,6 +31,7 @@
 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE_NOEXT
 from cubicweb.web import (INTERNAL_FIELD_VALUE, LOGGER, NothingToEdit,
                           RequestError, StatusResponse)
+from cubicweb.web.http_headers import Headers
 
 _MARKER = object()
 
@@ -88,6 +89,8 @@
         self.pageid = None
         self.datadir_url = self._datadir_url()
         self._set_pageid()
+        # prepare output header
+        self.headers_out = Headers()
 
     def _set_pageid(self):
         """initialize self.pageid
@@ -657,17 +660,26 @@
         """
         raise NotImplementedError()
 
-    def set_header(self, header, value):
+    def set_header(self, header, value, raw=True):
         """set an output HTTP header"""
-        raise NotImplementedError()
+        if raw:
+            # adding encoded header is important, else page content
+            # will be reconverted back to unicode and apart unefficiency, this
+            # may cause decoding problem (e.g. when downloading a file)
+            self.headers_out.setRawHeaders(header, [str(value)])
+        else:
+            self.headers_out.setHeader(header, value)
 
     def add_header(self, header, value):
         """add an output HTTP header"""
-        raise NotImplementedError()
+        # adding encoded header is important, else page content
+        # will be reconverted back to unicode and apart unefficiency, this
+        # may cause decoding problem (e.g. when downloading a file)
+        self.headers_out.addRawHeader(header, str(value))
 
     def remove_header(self, header):
         """remove an output HTTP header"""
-        raise NotImplementedError()
+        self.headers_out.removeHeader(header)
 
     def header_authorization(self):
         """returns a couple (auth-type, auth-value)"""
--- a/web/views/basecontrollers.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/web/views/basecontrollers.py	Tue Apr 06 19:08:07 2010 +0200
@@ -22,10 +22,11 @@
 from cubicweb.utils import CubicWebJsonEncoder
 from cubicweb.selectors import authenticated_user, match_form_params
 from cubicweb.mail import format_mail
-from cubicweb.web import ExplicitLogin, Redirect, RemoteCallFailed, json_dumps
+from cubicweb.web import ExplicitLogin, Redirect, RemoteCallFailed, DirectResponse, json_dumps
 from cubicweb.web.controller import Controller
 from cubicweb.web.views import vid_from_rset
 from cubicweb.web.views.formrenderers import FormRenderer
+
 try:
     from cubicweb.web.facet import (FilterRQLBuilder, get_facet,
                                     prepare_facets_rqlst)
@@ -283,7 +284,7 @@
             raise RemoteCallFailed(repr(exc))
         try:
             result = func(*args)
-        except RemoteCallFailed:
+        except (RemoteCallFailed, DirectResponse):
             raise
         except Exception, ex:
             self.exception('an exception occured while calling js_%s(%s): %s',
--- a/wsgi/request.py	Tue Apr 06 16:04:37 2010 +0200
+++ b/wsgi/request.py	Tue Apr 06 19:08:07 2010 +0200
@@ -38,9 +38,9 @@
         post, files = self.get_posted_data()
         super(CubicWebWsgiRequest, self).__init__(vreg, https, post)
         if files is not None:
-            for fdef in files.itervalues():
-                fdef[0] = unicode(fdef[0], self.encoding)
-            self.form.update(files)
+            for key, (name, _, stream) in files.iteritems():
+                name = unicode(name, self.encoding)
+                self.form[key] = (name, stream)
         # prepare output headers
         self.headers_out = {}