Merge 3.26
authorNicolas Chauvat <nicolas.chauvat@logilab.fr>
Fri, 18 Oct 2019 23:39:03 +0200
changeset 12749 ff63319a1730
parent 12746 5c432a7fc442 (diff)
parent 12748 9fa65579520f (current diff)
child 12750 74b473f288d5
Merge 3.26
cubicweb/cwctl.py
cubicweb/server/repository.py
--- a/MANIFEST.in	Tue Aug 27 20:16:01 2019 +0200
+++ b/MANIFEST.in	Fri Oct 18 23:39:03 2019 +0200
@@ -50,8 +50,6 @@
 recursive-include cubicweb/devtools/test/data *.py *.txt *.js *.po.ref
 recursive-include cubicweb/entities/test *.py
 recursive-include cubicweb/entities/test/data *.py
-recursive-include cubicweb/etwist/test *.py
-recursive-include cubicweb/etwist/test/data *.py
 recursive-include cubicweb/ext/test *.py
 recursive-include cubicweb/ext/test/data *.py
 recursive-include cubicweb/hooks/test *.py
@@ -68,11 +66,14 @@
 recursive-include cubicweb/server/test/data-schemaserial *.py
 include cubicweb/web/test/testutils.js
 recursive-include cubicweb/web/test *.py
+include cubicweb/web/test/data/cubicweb_file/data/file.png
+include cubicweb/web/test/data/cubicweb_file/wdoc/toc.xml
 recursive-include cubicweb/web/test/data bootstrap_cubes pouet.css *.py
 recursive-include cubicweb/web/test/data/static/jstests *.js *.html *.json
 recursive-include cubicweb/wsgi/test *.py
 
 include cubicweb/pyramid/development.ini.tmpl
+include cubicweb/pyramid/pyramid.ini.tmpl
 
 include cubicweb/web/data/jquery-treeview/*.md
 
--- a/README	Tue Aug 27 20:16:01 2019 +0200
+++ b/README	Fri Oct 18 23:39:03 2019 +0200
@@ -21,9 +21,14 @@
 
 Execute::
 
- apt-get install cubicweb cubicweb-dev cubicweb-blog
+ python3 -m venv venv
+ source venv/bin/activate
+ pip install 'cubicweb[pyramid]' cubicweb-blog
  cubicweb-ctl create blog myblog
- cubicweb-ctl start -D myblog
+ # read how to create your ~/etc/cubicweb.d/myblog/pyramid.ini file here:
+ # https://cubicweb.readthedocs.io/en/latest/book/pyramid/settings/#pyramid-settings-file
+ # then start your instance:
+ cubicweb-ctl pyramid -D myblog
  sensible-browser http://localhost:8080/
 
 Details at https://cubicweb.readthedocs.io/en/3.26/tutorials/base/blog-in-five-minutes
@@ -31,6 +36,15 @@
 You can also look at the latest builds on Logilab's jenkins:
 https://jenkins.logilab.org/
 
+Test
+----
+
+Simply run the `tox` command in the root folder of this repository:
+
+    tox
+
+How to install tox: https://tox.readthedocs.io/en/latest/install.html
+
 Documentation
 -------------
 
@@ -64,3 +78,34 @@
 Mailing list: https://lists.cubicweb.org/mailman/listinfo/cubicweb-devel
 Patchbomb extension: https://www.mercurial-scm.org/wiki/PatchbombExtension
 Good practice on sending email patches: https://www.mercurial-scm.org/wiki/ContributingChanges#Emailing_patches
+
+Full .hg/hgrc example for contributors having ssh access to ``hg.logilab.org`` :
+
+    [paths]
+    default = https://hg.logilab.org/master/cubicweb
+    default-push = ssh://hg@hg.logilab.org/review/cubicweb
+
+    [email]
+    to = cubicweb-devel@lists.cubicweb.org
+
+    [patchbomb]
+    publicurl = https://hg.logilab.org/review/cubicweb
+    flagtemplate = "{separate(' ', 'cubicweb', flags)}"
+
+    [jenkins]
+    url = https://jenkins.logilab.org/
+    job = cubicweb-default
+
+If you don't have ssh access to ``hg.logilab.org``, you can use your own Mercurial server and
+change ``default-push`` and ``publicurl`` accordingly.
+A list of services that provides hosting of Mercurial repositories is available
+on https://www.mercurial-scm.org/wiki/MercurialHosting.
+
+* ``hg pull`` will pull on master repo (public changesets).
+* ``hg push`` will push on review repo using ssh.
+* When sending an email to the list, it will add a "Available at" with command
+  to pull the draft series on the public repo.
+* Using https://hg.logilab.org/master/logilab/devtools/file/tip/hgext/jenkins.py
+  mercurial extension, ``hg show jenkins`` display jenkins build status for
+  each changeset. Read https://www.mercurial-scm.org/wiki/UsingExtensions to learn how
+  to enable a new extension in mercurial.
--- a/cubicweb.spec	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb.spec	Fri Oct 18 23:39:03 2019 +0200
@@ -21,7 +21,6 @@
 BuildArch:      noarch
 
 Requires:       %{python}
-Requires:       %{python}-six >= 1.4.0
 Requires:       %{python}-logilab-common >= 1.4.0
 Requires:       %{python}-logilab-mtconverter >= 0.8.0
 Requires:       %{python}-rql >= 0.34.0
@@ -29,8 +28,6 @@
 Requires:       %{python}-logilab-database >= 1.15.0
 Requires:       %{python}-passlib
 Requires:       %{python}-lxml
-Requires:       %{python}-unittest2 >= 0.7.0
-Requires:       %{python}-twisted-web < 16.0.0
 Requires:       %{python}-markdown
 Requires:       pytz
 # the schema view uses `dot'; at least on el5, png output requires graphviz-gd
--- a/cubicweb/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,19 +20,13 @@
 """
 
 
-import imp
 import logging
 import os
 import pickle
 import sys
-import types
 import warnings
 import zlib
 
-from six import PY2, binary_type, text_type
-from six.moves import builtins
-
-from logilab.common.deprecation import deprecated
 from logilab.common.logging_ext import set_log_methods
 from yams.constraints import BASE_CONVERTERS, BASE_CHECKERS
 from yams.schema import role_name as rname
@@ -44,11 +38,7 @@
 from yams import ValidationError
 from cubicweb._exceptions import *  # noqa
 
-if PY2:
-    # http://bugs.python.org/issue10211
-    from StringIO import StringIO as BytesIO
-else:
-    from io import BytesIO
+from io import BytesIO
 
 # ignore the pygments UserWarnings
 warnings.filterwarnings('ignore', category=UserWarning,
@@ -67,20 +57,12 @@
 
 # '_' is available to mark internationalized string but should not be used to
 # do the actual translation
-_ = text_type
-if not hasattr(builtins, '_'):
-    builtins._ = deprecated("[3.22] Use 'from cubicweb import _'")(_)
-
-
-# convert eid to the right type, raise ValueError if it's not a valid eid
-@deprecated('[3.17] typed_eid() was removed. replace it with int() when needed.')
-def typed_eid(eid):
-    return int(eid)
+_ = str
 
 
 class Binary(BytesIO):
     """class to hold binary data. Use BytesIO to prevent use of unicode data"""
-    _allowed_types = (binary_type, bytearray, buffer if PY2 else memoryview)  # noqa: F405
+    _allowed_types = (bytes, bytearray, memoryview)
 
     def __init__(self, buf=b''):
         assert isinstance(buf, self._allowed_types), \
@@ -156,7 +138,7 @@
 
 
 def check_password(eschema, value):
-    return isinstance(value, (binary_type, Binary))
+    return isinstance(value, (bytes, Binary))
 
 
 BASE_CHECKERS['Password'] = check_password
@@ -165,7 +147,7 @@
 def str_or_binary(value):
     if isinstance(value, Binary):
         return value
-    return binary_type(value)
+    return bytes(value)
 
 
 BASE_CONVERTERS['Password'] = str_or_binary
@@ -278,60 +260,3 @@
     not be processed, a memory allocation error occurred during processing,
     etc.
     """
-
-
-# Import hook for "legacy" cubes ##############################################
-
-class _CubesLoader(object):
-
-    def __init__(self, *modinfo):
-        self.modinfo = modinfo
-
-    def load_module(self, fullname):
-        try:
-            # If there is an existing module object named 'fullname' in
-            # sys.modules , the loader must use that existing module.
-            # Otherwise, the reload() builtin will not work correctly.
-            return sys.modules[fullname]
-        except KeyError:
-            pass
-        if fullname == 'cubes':
-            mod = sys.modules[fullname] = types.ModuleType(
-                fullname, doc='CubicWeb cubes')
-        else:
-            modname, file, pathname, description = self.modinfo
-            try:
-                mod = sys.modules[fullname] = imp.load_module(
-                    modname, file, pathname, description)
-            finally:
-                # https://docs.python.org/2/library/imp.html#imp.load_module
-                # Important: the caller is responsible for closing the file
-                # argument, if it was not None, even when an exception is
-                # raised. This is best done using a try ... finally statement
-                if file is not None:
-                    file.close()
-        return mod
-
-
-class _CubesImporter(object):
-    """Module finder handling redirection of import of "cubes.<name>"
-    to "cubicweb_<name>".
-    """
-
-    @classmethod
-    def install(cls):
-        if not any(isinstance(x, cls) for x in sys.meta_path):
-            self = cls()
-            sys.meta_path.append(self)
-
-    def find_module(self, fullname, path=None):
-        if fullname == 'cubes':
-            return _CubesLoader()
-        elif fullname.startswith('cubes.') and fullname.count('.') == 1:
-            modname = 'cubicweb_' + fullname.split('.', 1)[1]
-            try:
-                modinfo = imp.find_module(modname)
-            except ImportError:
-                return None
-            else:
-                return _CubesLoader(modname, *modinfo)
--- a/cubicweb/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -22,8 +22,8 @@
 
 modname = distname = "cubicweb"
 
-numversion = (3, 26, 14)
-version = '.'.join(str(num) for num in numversion)
+numversion = (3, 27, 0)
+version = '.'.join(str(num) for num in numversion) + 'a2'
 
 description = "a repository of entities / relations for knowledge management"
 author = "Logilab"
@@ -34,7 +34,7 @@
 classifiers = [
     'Environment :: Web Environment',
     'Framework :: CubicWeb',
-    'Programming Language :: Python',
+    'Programming Language :: Python :: 3',
     'Programming Language :: JavaScript',
 ]
 
--- a/cubicweb/_exceptions.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/_exceptions.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,75 +18,78 @@
 """Exceptions shared by different cubicweb packages."""
 
 
-
-from warnings import warn
+from logilab.common.decorators import cachedproperty
+from logilab.common.registry import RegistryException
 
-from six import PY2, text_type
-
-from logilab.common.decorators import cachedproperty
-
-from yams import ValidationError
+from yams import ValidationError  # noqa: F401
 
 # abstract exceptions #########################################################
 
+
 class CubicWebException(Exception):
     """base class for cubicweb server exception"""
     msg = ""
-    def __unicode__(self):
+
+    def __str__(self):
         if self.msg:
             if self.args:
                 return self.msg % tuple(self.args)
             else:
                 return self.msg
         else:
-            return u' '.join(text_type(arg) for arg in self.args)
-
-    def __str__(self):
-        res = self.__unicode__()
-        if PY2:
-            res = res.encode('utf-8')
-        return res
+            return u' '.join(str(arg) for arg in self.args)
 
 
 class ConfigurationError(CubicWebException):
     """a misconfiguration error"""
 
+
 class InternalError(CubicWebException):
     """base class for exceptions which should not occur"""
 
+
 class SecurityError(CubicWebException):
     """base class for cubicweb server security exceptions"""
 
+
 class RepositoryError(CubicWebException):
     """base class for repository exceptions"""
 
+
 class SourceException(CubicWebException):
     """base class for source exceptions"""
 
+
 class CubicWebRuntimeError(CubicWebException):
     """base class for runtime exceptions"""
 
 # repository exceptions #######################################################
 
+
 class ConnectionError(RepositoryError):
     """raised when a bad connection id is given or when an attempt to establish
     a connection failed
     """
 
+
 class AuthenticationError(ConnectionError):
     """raised when an attempt to establish a connection failed due to wrong
     connection information (login / password or other authentication token)
     """
 
+
 class BadConnectionId(ConnectionError):
     """raised when a bad connection id is given"""
 
+
 class UnknownEid(RepositoryError):
     """the eid is not defined in the system tables"""
     msg = 'No entity with eid %s in the repository'
 
+
 class UniqueTogetherError(RepositoryError):
     """raised when a unique_together constraint caused an IntegrityError"""
+
     def __init__(self, session, **kwargs):
         self.session = session
         assert 'rtypes' in kwargs or 'cstrname' in kwargs
@@ -98,23 +101,20 @@
     def rtypes(self):
         if 'rtypes' in self.kwargs:
             return self.kwargs['rtypes']
-        cstrname = text_type(self.kwargs['cstrname'])
+        cstrname = str(self.kwargs['cstrname'])
         cstr = self.session.find('CWUniqueTogetherConstraint', name=cstrname).one()
         return sorted(rtype.name for rtype in cstr.relations)
 
-    @cachedproperty
-    def args(self):
-        warn('[3.18] UniqueTogetherError.args is deprecated, just use '
-             'the .rtypes accessor.',
-             DeprecationWarning)
-        # the first argument, etype, is never used and was never garanteed anyway
-        return None, self.rtypes
-
 
 class ViolatedConstraint(RepositoryError):
-    def __init__(self, cnx, cstrname):
+    def __init__(self, cnx, cstrname, query):
         self.cnx = cnx
         self.cstrname = cstrname
+        message = (
+            "constraint '%s' is being violated by the query '%s'. "
+            "You can run the inverted constraint on the database to list the problematic rows."
+        ) % (cstrname, query)
+        super(ViolatedConstraint, self).__init__(message)
 
 
 # security exceptions #########################################################
@@ -127,7 +127,7 @@
     msg1 = u'You are not allowed to perform %s operation on %s'
     var = None
 
-    def __unicode__(self):
+    def __str__(self):
         try:
             if self.args and len(self.args) == 2:
                 return self.msg1 % self.args
@@ -135,7 +135,8 @@
                 return u' '.join(self.args)
             return self.msg
         except Exception as ex:
-            return text_type(ex)
+            return str(ex)
+
 
 class Forbidden(SecurityError):
     """raised when a user tries to perform a forbidden action
@@ -143,6 +144,7 @@
 
 # source exceptions ###########################################################
 
+
 class EidNotInSource(SourceException):
     """trying to access an object with a particular eid from a particular
     source has failed
@@ -152,31 +154,33 @@
 
 # registry exceptions #########################################################
 
-# pre 3.15 bw compat
-from logilab.common.registry import RegistryException, ObjectNotFound, NoSelectableObject
-
 class UnknownProperty(RegistryException):
     """property found in database but unknown in registry"""
 
 # query exception #############################################################
 
+
 class QueryError(CubicWebRuntimeError):
     """a query try to do something it shouldn't"""
 
+
 class NotAnEntity(CubicWebRuntimeError):
     """raised when get_entity is called for a column which doesn't contain
     a non final entity
     """
 
+
 class MultipleResultsError(CubicWebRuntimeError):
     """raised when ResultSet.one() is called on a resultset with multiple rows
     of multiple columns.
     """
 
+
 class NoResultError(CubicWebRuntimeError):
     """raised when no result is found but at least one is expected.
     """
 
+
 class UndoTransactionException(QueryError):
     """Raised when undoing a transaction could not be performed completely.
 
@@ -208,8 +212,10 @@
 
 # tools exceptions ############################################################
 
+
 class ExecutionError(Exception):
     """server execution control error (already started, not running...)"""
 
+
 # pylint: disable=W0611
-from logilab.common.clcommands import BadCommandUsage
+from logilab.common.clcommands import BadCommandUsage  # noqa: E402, F401
--- a/cubicweb/_gcdebug.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/_gcdebug.py	Fri Oct 18 23:39:03 2019 +0200
@@ -15,12 +15,8 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-from __future__ import print_function
-
 import gc, types, weakref
 
-from six import PY2
-
 from cubicweb.schema import CubicWebRelationSchema, CubicWebEntitySchema
 try:
     from cubicweb.web.request import _NeedAuthAccessMock
@@ -37,9 +33,6 @@
     types.ModuleType, types.FunctionType, types.MethodType,
     types.MemberDescriptorType, types.GetSetDescriptorType,
     )
-if PY2:
-    # weakref.WeakKeyDictionary fails isinstance check on Python 3.5.
-    IGNORE_CLASSES += (weakref.WeakKeyDictionary, )
 
 if _NeedAuthAccessMock is not None:
     IGNORE_CLASSES = IGNORE_CLASSES + (_NeedAuthAccessMock,)
--- a/cubicweb/appobject.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/appobject.py	Fri Oct 18 23:39:03 2019 +0200
@@ -31,26 +31,10 @@
 
 from logging import getLogger
 
-from logilab.common.deprecation import deprecated, class_renamed
 from logilab.common.logging_ext import set_log_methods
 
-# first line imports for bw compat
-from logilab.common.registry import (objectify_predicate, traced_selection, Predicate,
-                                     RegistrableObject, yes)
-
+from logilab.common.registry import RegistrableObject, yes
 
-objectify_selector = deprecated('[3.15] objectify_selector has been '
-                                'renamed to objectify_predicates in '
-                                'logilab.common.registry')(objectify_predicate)
-traced_selection = deprecated('[3.15] traced_selection has been '
-                              'moved to logilab.common.registry')(traced_selection)
-Selector = class_renamed('Selector', Predicate,
-                         '[3.15] Selector has been renamed to Predicate '
-                         'in logilab.common.registry')
-
-@deprecated('[3.15] lltrace decorator can now be removed')
-def lltrace(func):
-    return func
 
 # the base class for all appobjects ############################################
 
@@ -156,6 +140,3 @@
     info = warning = error = critical = exception = debug = lambda msg,*a,**kw: None
 
 set_log_methods(AppObject, getLogger('cubicweb.appobject'))
-
-# defined here to avoid warning on usage on the AppObject class
-yes = deprecated('[3.15] yes has been moved to logilab.common.registry')(yes)
--- a/cubicweb/crypto.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/crypto.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,16 +19,17 @@
 
 
 from base64 import b64encode, b64decode
+import pickle
 
-from six.moves import cPickle as pickle
-
-from Crypto.Cipher import Blowfish
+from Cryptodome.Cipher import Blowfish
 
 
 _CYPHERERS = {}
 
 
 def _cypherer(seed):
+    if isinstance(seed, str):
+        seed = seed.encode('utf-8')
     try:
         return _CYPHERERS[seed]
     except KeyError:
--- a/cubicweb/cwconfig.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/cwconfig.py	Fri Oct 18 23:39:03 2019 +0200
@@ -108,10 +108,6 @@
 
 `<CW_SOFTWARE_ROOT>` is the source checkout's ``cubicweb`` directory:
 
-* main cubes directory is `<CW_SOFTWARE_ROOT>/../../cubes`. You can specify
-  another one with :envvar:`CW_INSTANCES_DIR` environment variable or simply
-  add some other directories by using :envvar:`CW_CUBES_PATH`
-
 * cubicweb migration files are searched in `<CW_SOFTWARE_ROOT>/misc/migration`
   instead of `<INSTALL_PREFIX>/share/cubicweb/migration/`.
 
@@ -157,11 +153,6 @@
 
    Resource mode: user or system, as explained in :ref:`ResourceMode`.
 
-.. envvar:: CW_CUBES_PATH
-
-   Augments the default search path for cubes. You may specify several
-   directories using ':' as separator (';' under windows environment).
-
 .. envvar:: CW_INSTANCES_DIR
 
    Directory where cubicweb instances will be found.
@@ -175,27 +166,21 @@
    Directory where pid files will be written
 """
 
-from __future__ import print_function
-
 import importlib
 import logging
 import logging.config
 import os
-from os.path import (exists, join, expanduser, abspath, normpath,
-                     basename, isdir, dirname, splitext, realpath)
+from os.path import (exists, join, expanduser, abspath,
+                     basename, dirname, splitext, realpath)
 import pkgutil
 import pkg_resources
-import re
 from smtplib import SMTP
 import stat
 import sys
 from threading import Lock
 from warnings import filterwarnings
 
-from six import PY2, text_type
-
-from logilab.common.decorators import cached, classproperty
-from logilab.common.deprecation import deprecated
+from logilab.common.decorators import cached
 from logilab.common.logging_ext import set_log_methods, init_log
 from logilab.common.configuration import (Configuration, Method,
                                           ConfigurationMixIn, merge_options,
@@ -220,7 +205,7 @@
 
 def possible_configurations(directory):
     """return a list of installed configurations in a directory
-    according to \*-ctl files
+    according to *-ctl files
     """
     return [name for name in ('repository', 'all-in-one', 'pyramid')
             if exists(join(directory, '%s.conf' % name))]
@@ -243,15 +228,6 @@
     return cube
 
 
-def _cube_modname(cube):
-    modname = _cube_pkgname(cube)
-    loader = pkgutil.find_loader(modname)
-    if loader:
-        return modname
-    else:
-        return 'cubes.' + cube
-
-
 def _expand_modname(modname, recursive=True):
     """expand modules names `modname` if exists by recursively walking
     submodules and subpackages and yield (submodname, filepath) including
@@ -382,11 +358,6 @@
         mode = os.environ.get('CW_MODE', 'system')
     assert mode in ('system', 'user'), '"CW_MODE" should be either "user" or "system"'
 
-    _CUBES_DIR = join(_INSTALL_PREFIX, 'share', 'cubicweb', 'cubes')
-    assert _CUBES_DIR  # XXX only meaningful if CW_CUBES_DIR is not set
-    CUBES_DIR = realpath(abspath(os.environ.get('CW_CUBES_DIR', _CUBES_DIR)))
-    CUBES_PATH = os.environ.get('CW_CUBES_PATH', '').split(os.pathsep)
-
     options = (
        ('log-threshold',
          {'type' : 'string', # XXX use a dedicated type?
@@ -479,23 +450,6 @@
                                 entry_point)
                     continue
                 cubes.add(modname)
-        # Legacy cubes.
-        for directory in cls.cubes_search_path():
-            if not exists(directory):
-                cls.error('unexistant directory in cubes search path: %s'
-                          % directory)
-                continue
-            for cube in os.listdir(directory):
-                if cube == 'shared':
-                    continue
-                if not re.match('[_A-Za-z][_A-Za-z0-9]*$', cube):
-                    continue # skip invalid python package name
-                if cube == 'pyramid':
-                    cls._warn_pyramid_cube()
-                    continue
-                cubedir = join(directory, cube)
-                if isdir(cubedir) and exists(join(cubedir, '__init__.py')):
-                    cubes.add(cube)
 
         def sortkey(cube):
             """Preserve sorting with "cubicweb_" prefix."""
@@ -510,23 +464,6 @@
         return sorted(cubes, key=sortkey)
 
     @classmethod
-    def cubes_search_path(cls):
-        """return the path of directories where cubes should be searched"""
-        path = [realpath(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
-
-    @classproperty
-    def extrapath(cls):
-        extrapath = {}
-        for cubesdir in cls.cubes_search_path():
-            if cubesdir != cls.CUBES_DIR:
-                extrapath[cubesdir] = 'cubes'
-        return extrapath
-
-    @classmethod
     def cube_dir(cls, cube):
         """return the cube directory for the given cube id, raise
         `ConfigurationError` if it doesn't exist
@@ -535,15 +472,9 @@
         loader = pkgutil.find_loader(pkgname)
         if loader:
             return dirname(loader.get_filename())
-        # Legacy cubes.
-        for directory in cls.cubes_search_path():
-            cubedir = join(directory, cube)
-            if exists(cubedir):
-                return cubedir
         msg = 'no module %(pkg)s in search path nor cube %(cube)r in %(path)s'
         raise ConfigurationError(msg % {'cube': cube,
-                                        'pkg': _cube_pkgname(cube),
-                                        'path': cls.cubes_search_path()})
+                                        'pkg': _cube_pkgname(cube)})
 
     @classmethod
     def cube_migration_scripts_dir(cls, cube):
@@ -553,18 +484,9 @@
     @classmethod
     def cube_pkginfo(cls, cube):
         """return the information module for the given cube"""
+        cube = CW_MIGRATION_MAP.get(cube, cube)
         pkgname = _cube_pkgname(cube)
-        try:
-            return importlib.import_module('%s.__pkginfo__' % pkgname)
-        except ImportError:
-            cube = CW_MIGRATION_MAP.get(cube, cube)
-            try:
-                parent = __import__('cubes.%s.__pkginfo__' % cube)
-                return getattr(parent, cube).__pkginfo__
-            except Exception as ex:
-                raise ConfigurationError(
-                    'unable to find packaging information for cube %s (%s: %s)'
-                    % (cube, ex.__class__.__name__, ex))
+        return importlib.import_module('%s.__pkginfo__' % pkgname)
 
     @classmethod
     def cube_version(cls, cube):
@@ -662,16 +584,8 @@
             raise ConfigurationError(ex)
 
     @classmethod
-    def cls_adjust_sys_path(cls):
-        """update python path if necessary"""
-        from cubicweb import _CubesImporter
-        _CubesImporter.install()
-        import cubes
-        cubes.__path__ = cls.cubes_search_path()
-
-    @classmethod
     def load_available_configs(cls):
-        for confmod in ('web.webconfig',  'etwist.twconfig',
+        for confmod in ('web.webconfig',
                         'server.serverconfig', 'pyramid.config'):
             try:
                 __import__('cubicweb.%s' % confmod)
@@ -681,8 +595,7 @@
 
     @classmethod
     def load_cwctl_plugins(cls):
-        cls.cls_adjust_sys_path()
-        for ctlmod in ('web.webctl',  'etwist.twctl', 'server.serverctl',
+        for ctlmod in ('web.webctl', 'server.serverctl',
                        'devtools.devctl', 'pyramid.pyramidctl'):
             try:
                 __import__('cubicweb.%s' % ctlmod)
@@ -695,10 +608,7 @@
             cubedir = cls.cube_dir(cube)
             pluginfile = join(cubedir, 'ccplugin.py')
             initfile = join(cubedir, '__init__.py')
-            if cube.startswith('cubicweb_'):
-                pkgname = cube
-            else:
-                pkgname = 'cubes.%s' % cube
+            pkgname = _cube_pkgname(cube)
             if exists(pluginfile):
                 try:
                     __import__(pkgname + '.ccplugin')
@@ -716,7 +626,7 @@
     cubicweb_appobject_path = set(['entities'])
     cube_appobject_path = set(['entities'])
 
-    def __init__(self, debugmode=False):
+    def __init__(self, debugmode=False, log_to_file=False):
         if debugmode:
             # in python 2.7, DeprecationWarning are not shown anymore by default
             filterwarnings('default', category=DeprecationWarning)
@@ -724,10 +634,11 @@
         self._cubes = None
         super(CubicWebNoAppConfiguration, self).__init__()
         self.debugmode = debugmode
+        self.log_to_file = log_to_file
         self.adjust_sys_path()
         self.load_defaults()
         # will be properly initialized later by _gettext_init
-        self.translations = {'en': (text_type, lambda ctx, msgid: text_type(msgid) )}
+        self.translations = {'en': (str, lambda ctx, msgid: str(msgid) )}
         self._site_loaded = set()
         # don't register ReStructured Text directives by simple import, avoid pb
         # with eg sphinx.
@@ -741,7 +652,7 @@
 
     def adjust_sys_path(self):
         # overriden in CubicWebConfiguration
-        self.cls_adjust_sys_path()
+        pass
 
     def init_log(self, logthreshold=None, logfile=None, syslog=False):
         """init the log service"""
@@ -754,12 +665,12 @@
             # no logrotate on win32, so use logging rotation facilities
             # for now, hard code weekly rotation every sunday, and 52 weeks kept
             # idea: make this configurable?
-            init_log(self.debugmode, syslog, logthreshold, logfile, self.log_format,
+            init_log(not self.log_to_file, syslog, logthreshold, logfile, self.log_format,
                      rotation_parameters={'when': 'W6', # every sunday
                                           'interval': 1,
                                           'backupCount': 52})
         else:
-            init_log(self.debugmode, syslog, logthreshold, logfile, self.log_format)
+            init_log(not self.log_to_file, syslog, logthreshold, logfile, self.log_format)
         # configure simpleTal logger
         logging.getLogger('simpleTAL').setLevel(logging.ERROR)
 
@@ -769,7 +680,7 @@
             modnames.append(('cubicweb', 'cubicweb.schemas.' + name))
         for cube in reversed(self.cubes()):
             for modname, filepath in _expand_modname(
-                    '{0}.schema'.format(_cube_modname(cube)),
+                    '{0}.schema'.format(_cube_pkgname(cube)),
                     recursive=False):
                 modnames.append((cube, modname))
         if self.apphome:
@@ -805,12 +716,8 @@
     def _load_site_cubicweb(self, cube):
         """Load site_cubicweb.py from `cube` (or apphome if cube is None)."""
         if cube is not None:
-            try:
-                modname = 'cubicweb_%s' % cube
-                __import__(modname)
-            except ImportError:
-                modname = 'cubes.%s' % cube
-                __import__(modname)
+            modname = _cube_pkgname(cube)
+            __import__(modname)
             modname = modname + '.site_cubicweb'
             __import__(modname)
             return sys.modules[modname]
@@ -864,11 +771,7 @@
         self._cubes = self.reorder_cubes(cubes)
         # load cubes'__init__.py file first
         for cube in cubes:
-            try:
-                importlib.import_module(_cube_pkgname(cube))
-            except ImportError:
-                # Legacy cube.
-                __import__('cubes.%s' % cube)
+            importlib.import_module(_cube_pkgname(cube))
         self.load_site_cubicweb()
 
     def cubes(self):
@@ -980,13 +883,13 @@
         return mdir
 
     @classmethod
-    def config_for(cls, appid, config=None, debugmode=False, creating=False):
+    def config_for(cls, appid, config=None, debugmode=False, log_to_file=False, creating=False):
         """return a configuration instance for the given instance identifier
         """
         cls.load_available_configs()
         config = config or guess_configuration(cls.instance_home(appid))
         configcls = configuration_cls(config)
-        return configcls(appid, debugmode, creating)
+        return configcls(appid, debugmode, creating, log_to_file=log_to_file)
 
     @classmethod
     def possible_configurations(cls, appid):
@@ -1079,12 +982,13 @@
 
     # instance methods used to get instance specific resources #############
 
-    def __init__(self, appid, debugmode=False, creating=False):
+    def __init__(self, appid, debugmode=False, creating=False, log_to_file=False):
         self.appid = appid
         # set to true while creating an instance
         self.creating = creating
-        super(CubicWebConfiguration, self).__init__(debugmode)
-        fake_gettext = (text_type, lambda ctx, msgid: text_type(msgid))
+        super(CubicWebConfiguration, self).__init__(debugmode,
+                                                    log_to_file=log_to_file)
+        fake_gettext = (str, lambda ctx, msgid: str(msgid))
         for lang in self.available_languages():
             self.translations[lang] = fake_gettext
         self._cubes = None
@@ -1247,7 +1151,7 @@
         """return available translation for an instance, by looking for
         compiled catalog
 
-        take \*args to be usable as a vocabulary method
+        take *args to be usable as a vocabulary method
         """
         from glob import glob
         yield 'en' # ensure 'en' is yielded even if no .mo found
@@ -1287,7 +1191,7 @@
 
     def appobjects_cube_modnames(self, cube):
         modnames = []
-        cube_modname = _cube_modname(cube)
+        cube_modname = _cube_pkgname(cube)
         cube_submodnames = self._sorted_appobjects(self.cube_appobject_path)
         for name in cube_submodnames:
             for modname, filepath in _expand_modname('.'.join([cube_modname, name])):
@@ -1344,10 +1248,9 @@
                 if self.mode == 'test':
                     raise
                 return False
-            for mimedoc, recipients in msgs:
-                msg = mimedoc.as_string() if PY2 else mimedoc.as_bytes()
+            for msg, recipients in msgs:
                 try:
-                    smtp.sendmail(fromaddr, recipients, msg)
+                    smtp.sendmail(fromaddr, recipients, msg.as_bytes())
                 except Exception as ex:
                     self.exception("error sending mail to %s (%s)",
                                    recipients, ex)
@@ -1363,7 +1266,6 @@
 
 # alias to get a configuration instance from an instance id
 instance_configuration = CubicWebConfiguration.config_for
-application_configuration = deprecated('use instance_configuration')(instance_configuration)
 
 
 _EXT_REGISTERED = False
--- a/cubicweb/cwctl.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/cwctl.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,15 +18,14 @@
 """the cubicweb-ctl tool, based on logilab.common.clcommands to
 provide a pluggable commands system.
 """
-from __future__ import print_function
-
 # *ctl module should limit the number of import to be imported as quickly as
 # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
 # completion). So import locally in command helpers.
 import sys
-from warnings import warn, filterwarnings
-from os import remove, listdir, system, pathsep
-from os.path import exists, join, isdir, dirname, abspath
+import traceback
+from warnings import filterwarnings
+from os import listdir
+from os.path import exists, join, isdir
 
 try:
     from os import kill, getpgid
@@ -36,21 +35,23 @@
     def getpgid():
         """win32 getpgid implementation"""
 
-from six.moves.urllib.parse import urlparse
-
 from logilab.common.clcommands import CommandLine
 from logilab.common.shellutils import ASK
 from logilab.common.configuration import merge_options
 from logilab.common.decorators import clear_cache
 
-from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage
+from cubicweb import ConfigurationError, ExecutionError, BadCommandUsage, utils
 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg, CONFIGURATIONS
+from cubicweb.server import set_debug
 from cubicweb.toolsutils import Command, rm, create_dir, underline_title
-from cubicweb.__pkginfo__ import version
+from cubicweb.__pkginfo__ import version as cw_version
+
+LOG_LEVELS = ('debug', 'info', 'warning', 'error')
+DBG_FLAGS = ('RQL', 'SQL', 'REPO', 'HOOKS', 'OPS', 'SEC', 'MORE', 'ALL')
 
 # don't check duplicated commands, it occurs when reloading site_cubicweb
 CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.',
-                    version=version, check_duplicated_command=False)
+                    version=cw_version, check_duplicated_command=False)
 
 
 def wait_process_end(pid, maxtry=10, waittime=1):
@@ -102,86 +103,112 @@
 
 
 class InstanceCommand(Command):
-    """base class for command taking 0 to n instance id as arguments
-    (0 meaning all registered instances)
-    """
-    arguments = '[<instance>...]'
+    """base class for command taking one instance id as arguments"""
+    arguments = '<instance>'
+
+    # enforce having one instance
+    min_args = max_args = 1
+
     options = (
         ("force",
-         {'short': 'f', 'action' : 'store_true',
+         {'short': 'f', 'action': 'store_true',
           'default': False,
           'help': 'force command without asking confirmation',
           }
          ),
-        )
+        ("pdb",
+         {'action': 'store_true', 'default': False,
+          'help': 'launch pdb on exception',
+          }
+         ),
+        ("loglevel",
+         {'type': 'choice', 'default': None, 'metavar': '<log level>',
+          'choices': LOG_LEVELS, 'short': 'l',
+          'help': 'allow to specify log level for debugging (choices: %s)'
+                  % (', '.join(LOG_LEVELS)),
+          }
+         ),
+        ('dbglevel',
+         {'type': 'multiple_choice', 'metavar': '<debug level>',
+          'default': None,
+          'choices': DBG_FLAGS,
+          'help': ('Set the server debugging flags; you may choose several '
+                   'values in %s; imply "debug" loglevel if loglevel is not set' % (DBG_FLAGS,)),
+          }),
+    )
     actionverb = None
 
     def run(self, args):
         """run the <command>_method on each argument (a list of instance
         identifiers)
         """
-        if not args:
-            args = list_instances(cwcfg.instances_dir())
-            try:
-                askconfirm = not self.config.force
-            except AttributeError:
-                # no force option
-                askconfirm = False
-        else:
-            askconfirm = False
-        self.run_args(args, askconfirm)
+        appid = args[0]
+        cmdmeth = getattr(self, '%s_instance' % self.name)
+
+        traceback_ = None
+
+        # debugmode=True is to force to have a StreamHandler used instead of
+        # writting the logs into a file in /tmp
+        self.cwconfig = cwcfg.config_for(appid, debugmode=True)
 
-    def run_args(self, args, askconfirm):
-        status = 0
-        for appid in args:
-            if askconfirm:
-                print('*'*72)
-                if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
-                    continue
-            try:
-                status = max(status, self.run_arg(appid))
-            except (KeyboardInterrupt, SystemExit):
-                sys.stderr.write('%s aborted\n' % self.name)
-                return 2 # specific error code
-        sys.exit(status)
+        # by default loglevel is 'error' but we keep the default value to None
+        # because some subcommands (e.g: pyramid) can override the loglevel in
+        # certain situations if it's not explicitly set by the user and we want
+        # to detect that (the "None" case)
+        if self['loglevel'] is None:
+            # if no loglevel is set but dbglevel is here we want to set level to debug
+            if self['dbglevel']:
+                init_cmdline_log_threshold(self.cwconfig, 'debug')
+            else:
+                init_cmdline_log_threshold(self.cwconfig, 'error')
+        else:
+            init_cmdline_log_threshold(self.cwconfig, self['loglevel'])
 
-    def run_arg(self, appid):
-        cmdmeth = getattr(self, '%s_instance' % self.name)
+        if self['dbglevel']:
+            set_debug('|'.join('DBG_' + x.upper() for x in self['dbglevel']))
+
         try:
             status = cmdmeth(appid) or 0
         except (ExecutionError, ConfigurationError) as ex:
+            # we need to do extract this information here for pdb since it is
+            # now lost in python 3 once we exit the try/catch statement
+            exception_type, exception, traceback_ = sys.exc_info()
+
             sys.stderr.write('instance %s not %s: %s\n' % (
-                    appid, self.actionverb, ex))
+                appid, self.actionverb, ex))
             status = 4
         except Exception as ex:
-            import traceback
+            # idem
+            exception_type, exception, traceback_ = sys.exc_info()
+
             traceback.print_exc()
+
             sys.stderr.write('instance %s not %s: %s\n' % (
-                    appid, self.actionverb, ex))
+                appid, self.actionverb, ex))
             status = 8
-        return status
 
-class InstanceCommandFork(InstanceCommand):
-    """Same as `InstanceCommand`, but command is forked in a new environment
-    for each argument
-    """
+        except (KeyboardInterrupt, SystemExit) as ex:
+            # idem
+            exception_type, exception, traceback_ = sys.exc_info()
 
-    def run_args(self, args, askconfirm):
-        if len(args) > 1:
-            forkcmd = ' '.join(w for w in sys.argv if not w in args)
-        else:
-            forkcmd = None
-        for appid in args:
-            if askconfirm:
-                print('*'*72)
-                if not ASK.confirm('%s instance %r ?' % (self.name, appid)):
-                    continue
-            if forkcmd:
-                status = system('%s %s' % (forkcmd, appid))
-                if status:
-                    print('%s exited with status %s' % (forkcmd, status))
+            sys.stderr.write('%s aborted\n' % self.name)
+            if isinstance(ex, KeyboardInterrupt):
+                status = 2  # specific error code
             else:
-                self.run_arg(appid)
+                status = ex.code
+
+        if status != 0 and self.config.pdb:
+            pdb = utils.get_pdb()
+
+            if traceback_ is not None:
+                pdb.post_mortem(traceback_)
+            else:
+                print("WARNING: Could not access to the traceback because the command return "
+                      "code is different than 0 but the command didn't raised an exception.")
+                # we can't use "header=" of set_trace because ipdb doesn't supports it
+                pdb.set_trace()
+
+        sys.exit(status)
 
 
 # base commands ###############################################################
@@ -197,9 +224,9 @@
     arguments = '[all|cubes|configurations|instances]'
     options = (
         ('verbose',
-         {'short': 'v', 'action' : 'store_true',
+         {'short': 'v', 'action': 'store_true',
           'help': "display more information."}),
-        )
+    )
 
     def run(self, args):
         """run the command with its specific arguments"""
@@ -231,15 +258,14 @@
         if mode in ('all', 'cubes'):
             cfgpb = ConfigurationProblem(cwcfg)
             try:
-                cubesdir = pathsep.join(cwcfg.cubes_search_path())
                 cube_names = available_cube_names(cwcfg)
                 namesize = max(len(x) for x in cube_names)
             except ConfigurationError as ex:
                 print('No cubes available:', ex)
             except ValueError:
-                print('No cubes available in %s' % cubesdir)
+                print('No cubes available')
             else:
-                print('Available cubes (%s):' % cubesdir)
+                print('Available cubes:')
                 for cube in cube_names:
                     try:
                         tinfo = cwcfg.cube_pkginfo(cube)
@@ -255,7 +281,7 @@
                             if not descr:
                                 descr = tinfo.__doc__
                             if descr:
-                                print('    '+ '    \n'.join(descr.splitlines()))
+                                print('    ' + '    \n'.join(descr.splitlines()))
                         modes = detect_available_modes(cwcfg.cube_dir(cube))
                         print('    available modes: %s' % ', '.join(modes))
             print()
@@ -289,7 +315,7 @@
             # configuration management problem solving
             cfgpb.solve()
             if cfgpb.warnings:
-                print('Warnings:\n', '\n'.join('* '+txt for txt in cfgpb.warnings))
+                print('Warnings:\n', '\n'.join('* ' + txt for txt in cfgpb.warnings))
             if cfgpb.errors:
                 print('Errors:')
                 for op, cube, version, src in cfgpb.errors:
@@ -299,14 +325,18 @@
                             print(' version', version, end=' ')
                         print('is not installed, but required by %s' % src)
                     else:
-                        print('* cube %s version %s is installed, but version %s is required by %s' % (
-                            cube, cfgpb.cubes[cube], version, src))
+                        print(
+                            '* cube %s version %s is installed, but version %s is required by %s'
+                            % (cube, cfgpb.cubes[cube], version, src)
+                        )
+
 
 def check_options_consistency(config):
     if config.automatic and config.config_level > 0:
         raise BadCommandUsage('--automatic and --config-level should not be '
                               'used together')
 
+
 class CreateInstanceCommand(Command):
     """Create an instance from a cube. This is a unified
     command which can handle web / server / all-in-one installation
@@ -325,20 +355,20 @@
     min_args = max_args = 2
     options = (
         ('automatic',
-         {'short': 'a', 'action' : 'store_true',
+         {'short': 'a', 'action': 'store_true',
           'default': False,
           'help': 'automatic mode: never ask and use default answer to every '
           'question. this may require that your login match a database super '
           'user (allowed to create database & all).',
           }),
         ('config-level',
-         {'short': 'l', 'type' : 'int', 'metavar': '<level>',
+         {'short': 'l', 'type': 'int', 'metavar': '<level>',
           'default': 0,
           'help': 'configuration level (0..2): 0 will ask for essential '
           'configuration parameters only while 2 will ask for all parameters',
           }),
         ('config',
-         {'short': 'c', 'type' : 'choice', 'metavar': '<install type>',
+         {'short': 'c', 'type': 'choice', 'metavar': '<install type>',
           'choices': ('all-in-one', 'repository', 'pyramid'),
           'default': 'all-in-one',
           'help': 'installation type, telling which part of an instance '
@@ -352,7 +382,7 @@
           'default': False,
           'help': 'stop after creation and do not continue with db-create',
           }),
-        )
+    )
 
     def run(self, args):
         """run the command with its specific arguments"""
@@ -376,12 +406,12 @@
             print(', '.join(available_cube_names(cwcfg)))
             return
         # create the registry directory for this instance
-        print('\n'+underline_title('Creating the instance %s' % appid))
+        print('\n' + underline_title('Creating the instance %s' % appid))
         create_dir(config.apphome)
         # cubicweb-ctl configuration
         if not self.config.automatic:
-            print('\n'+underline_title('Configuring the instance (%s.conf)'
-                                       % configname))
+            print('\n' + underline_title('Configuring the instance (%s.conf)'
+                                         % configname))
             config.input_config('main', self.config.config_level)
         # configuration'specific stuff
         print()
@@ -397,7 +427,6 @@
                     config.input_config(section, self.config.config_level)
         # write down configuration
         config.save()
-        self._handle_win32(config, appid)
         print('-> generated config %s' % config.main_config_file())
         # handle i18n files structure
         # in the first cube given
@@ -407,12 +436,12 @@
         if errors:
             print('\n'.join(errors))
             if self.config.automatic \
-                   or not ASK.confirm('error while compiling message catalogs, '
-                                      'continue anyway ?'):
+                or not ASK.confirm('error while compiling message catalogs, '
+                                   'continue anyway ?'):
                 print('creation not completed')
                 return
         # create the additional data directory for this instance
-        if config.appdatahome != config.apphome: # true in dev mode
+        if config.appdatahome != config.apphome:  # true in dev mode
             create_dir(config.appdatahome)
         create_dir(join(config.appdatahome, 'backup'))
         if config['uid']:
@@ -424,29 +453,6 @@
         if not self.config.no_db_create:
             helper.postcreate(self.config.automatic, self.config.config_level)
 
-    def _handle_win32(self, config, appid):
-        if sys.platform != 'win32':
-            return
-        service_template = """
-import sys
-import win32serviceutil
-sys.path.insert(0, r"%(CWPATH)s")
-
-from cubicweb.etwist.service import CWService
-
-classdict = {'_svc_name_': 'cubicweb-%(APPID)s',
-             '_svc_display_name_': 'CubicWeb ' + '%(CNAME)s',
-             'instance': '%(APPID)s'}
-%(CNAME)sService = type('%(CNAME)sService', (CWService,), classdict)
-
-if __name__ == '__main__':
-    win32serviceutil.HandleCommandLine(%(CNAME)sService)
-"""
-        open(join(config.apphome, 'win32svc.py'), 'wb').write(
-            service_template % {'APPID': appid,
-                                'CNAME': appid.capitalize(),
-                                'CWPATH': abspath(join(dirname(__file__), '..'))})
-
 
 class DeleteInstanceCommand(Command):
     """Delete an instance. Will remove instance's files and
@@ -455,7 +461,6 @@
     name = 'delete'
     arguments = '<instance>'
     min_args = max_args = 1
-    options = ()
 
     def run(self, args):
         """run the command with its specific arguments"""
@@ -483,210 +488,37 @@
 
 # instance commands ########################################################
 
-class StartInstanceCommand(InstanceCommandFork):
-    """Start the given instances. If no instance is given, start them all.
-
-    <instance>...
-      identifiers of the instances to start. If no instance is
-      given, start them all.
-    """
-    name = 'start'
-    actionverb = 'started'
-    options = (
-        ("debug",
-         {'short': 'D', 'action' : 'store_true',
-          'help': 'start server in debug mode.'}),
-        ("force",
-         {'short': 'f', 'action' : 'store_true',
-          'default': False,
-          'help': 'start the instance even if it seems to be already \
-running.'}),
-        ('profile',
-         {'short': 'P', 'type' : 'string', 'metavar': '<stat file>',
-          'default': None,
-          'help': 'profile code and use the specified file to store stats',
-          }),
-        ('loglevel',
-         {'short': 'l', 'type' : 'choice', 'metavar': '<log level>',
-          'default': None, 'choices': ('debug', 'info', 'warning', 'error'),
-          'help': 'debug if -D is set, error otherwise',
-          }),
-        ('param',
-         {'short': 'p', 'type' : 'named', 'metavar' : 'key1:value1,key2:value2',
-          'default': {},
-          'help': 'override <key> configuration file option with <value>.',
-         }),
-       )
-
-    def start_instance(self, appid):
-        """start the instance's server"""
-        try:
-            import twisted  # noqa
-        except ImportError:
-            msg = (
-                "Twisted is required by the 'start' command\n"
-                "Either install it, or use one of the alternative commands:\n"
-                "- '{ctl} pyramid {appid}'\n"
-                "- '{ctl} wsgi {appid}'\n")
-            raise ExecutionError(msg.format(ctl='cubicweb-ctl', appid=appid))
-        config = cwcfg.config_for(appid, debugmode=self['debug'])
-        # override config file values with cmdline options
-        config.cmdline_options = self.config.param
-        init_cmdline_log_threshold(config, self['loglevel'])
-        if self['profile']:
-            config.global_set_option('profile', self.config.profile)
-        helper = self.config_helper(config, cmdname='start')
-        pidf = config['pid-file']
-        if exists(pidf) and not self['force']:
-            msg = "%s seems to be running. Remove %s by hand if necessary or use \
-the --force option."
-            raise ExecutionError(msg % (appid, pidf))
-        if helper.start_server(config) == 1:
-            print('instance %s started' % appid)
-
-
 def init_cmdline_log_threshold(config, loglevel):
     if loglevel is not None:
         config.global_set_option('log-threshold', loglevel.upper())
         config.init_log(config['log-threshold'], force=True)
 
 
-class StopInstanceCommand(InstanceCommand):
-    """Stop the given instances.
-
-    <instance>...
-      identifiers of the instances to stop. If no instance is
-      given, stop them all.
-    """
-    name = 'stop'
-    actionverb = 'stopped'
-
-    def stop_instance(self, appid):
-        """stop the instance's server"""
-        config = cwcfg.config_for(appid)
-        helper = self.config_helper(config, cmdname='stop')
-        helper.poststop() # do this anyway
-        pidf = config['pid-file']
-        if not exists(pidf):
-            sys.stderr.write("%s doesn't exist.\n" % pidf)
-            return
-        import signal
-        pid = int(open(pidf).read().strip())
-        try:
-            kill(pid, signal.SIGTERM)
-        except Exception:
-            sys.stderr.write("process %s seems already dead.\n" % pid)
-        else:
-            try:
-                wait_process_end(pid)
-            except ExecutionError as ex:
-                sys.stderr.write('%s\ntrying SIGKILL\n' % ex)
-                try:
-                    kill(pid, signal.SIGKILL)
-                except Exception:
-                    # probably dead now
-                    pass
-                wait_process_end(pid)
-        try:
-            remove(pidf)
-        except OSError:
-            # already removed by twistd
-            pass
-        print('instance %s stopped' % appid)
-
-
-class RestartInstanceCommand(StartInstanceCommand):
-    """Restart the given instances.
-
-    <instance>...
-      identifiers of the instances to restart. If no instance is
-      given, restart them all.
-    """
-    name = 'restart'
-    actionverb = 'restarted'
-
-    def restart_instance(self, appid):
-        StopInstanceCommand(self.logger).stop_instance(appid)
-        self.start_instance(appid)
-
-
-class ReloadConfigurationCommand(RestartInstanceCommand):
-    """Reload the given instances. This command is equivalent to a
-    restart for now.
-
-    <instance>...
-      identifiers of the instances to reload. If no instance is
-      given, reload them all.
-    """
-    name = 'reload'
-
-    def reload_instance(self, appid):
-        self.restart_instance(appid)
-
-
-class StatusCommand(InstanceCommand):
-    """Display status information about the given instances.
-
-    <instance>...
-      identifiers of the instances to status. If no instance is
-      given, get status information about all registered instances.
-    """
-    name = 'status'
-    options = ()
-
-    @staticmethod
-    def status_instance(appid):
-        """print running status information for an instance"""
-        status = 0
-        for mode in cwcfg.possible_configurations(appid):
-            config = cwcfg.config_for(appid, mode)
-            print('[%s-%s]' % (appid, mode), end=' ')
-            try:
-                pidf = config['pid-file']
-            except KeyError:
-                print('buggy instance, pid file not specified')
-                continue
-            if not exists(pidf):
-                print("doesn't seem to be running")
-                status = 1
-                continue
-            pid = int(open(pidf).read().strip())
-            # trick to guess whether or not the process is running
-            try:
-                getpgid(pid)
-            except OSError:
-                print("should be running with pid %s but the process can not be found" % pid)
-                status = 1
-                continue
-            print("running with pid %s" % (pid))
-        return status
-
-class UpgradeInstanceCommand(InstanceCommandFork):
+class UpgradeInstanceCommand(InstanceCommand):
     """Upgrade an instance after cubicweb and/or component(s) upgrade.
 
     For repository update, you will be prompted for a login / password to use
     to connect to the system database.  For some upgrades, the given user
     should have create or alter table permissions.
 
-    <instance>...
-      identifiers of the instances to upgrade. If no instance is
-      given, upgrade them all.
+    <instance>
+      identifier of the instance to upgrade.
     """
     name = 'upgrade'
     actionverb = 'upgraded'
     options = InstanceCommand.options + (
         ('force-cube-version',
-         {'short': 't', 'type' : 'named', 'metavar': 'cube1:X.Y.Z,cube2:X.Y.Z',
+         {'short': 't', 'type': 'named', 'metavar': 'cube1:X.Y.Z,cube2:X.Y.Z',
           'default': None,
           'help': 'force migration from the indicated version for the specified cube(s).'}),
 
         ('force-cubicweb-version',
-         {'short': 'e', 'type' : 'string', 'metavar': 'X.Y.Z',
+         {'short': 'e', 'type': 'string', 'metavar': 'X.Y.Z',
           'default': None,
           'help': 'force migration from the indicated cubicweb version.'}),
 
         ('fs-only',
-         {'short': 's', 'action' : 'store_true',
+         {'short': 's', 'action': 'store_true',
           'default': False,
           'help': 'only upgrade files on the file system, not the database.'}),
 
@@ -695,40 +527,34 @@
           'default': False,
           'help': 'do NOT update config file if set.'}),
 
-        ('nostartstop',
-         {'short': 'n', 'action' : 'store_true',
-          'default': False,
-          'help': 'don\'t try to stop instance before migration and to restart it after.'}),
-
         ('verbosity',
-         {'short': 'v', 'type' : 'int', 'metavar': '<0..2>',
+         {'short': 'v', 'type': 'int', 'metavar': '<0..2>',
           'default': 1,
           'help': "0: no confirmation, 1: only main commands confirmed, 2 ask \
 for everything."}),
 
         ('backup-db',
-         {'short': 'b', 'type' : 'yn', 'metavar': '<y or n>',
+         {'short': 'b', 'type': 'yn', 'metavar': '<y or n>',
           'default': None,
-          'help': "Backup the instance database before upgrade.\n"\
+          'help': "Backup the instance database before upgrade.\n"
           "If the option is ommitted, confirmation will be ask.",
           }),
 
         ('ext-sources',
-         {'short': 'E', 'type' : 'csv', 'metavar': '<sources>',
+         {'short': 'E', 'type': 'csv', 'metavar': '<sources>',
           'default': None,
           'help': "For multisources instances, specify to which sources the \
 repository should connect to for upgrading. When unspecified or 'migration' is \
 given, appropriate sources for migration will be automatically selected \
 (recommended). If 'all' is given, will connect to all defined sources.",
           }),
-        )
+    )
 
     def upgrade_instance(self, appid):
         print('\n' + underline_title('Upgrading the instance %s' % appid))
         from logilab.common.changelog import Version
         config = cwcfg.config_for(appid)
-        instance_running = exists(config['pid-file'])
-        config.repairing = True # notice we're not starting the server
+        config.repairing = True  # notice we're not starting the server
         config.verbosity = self.config.verbosity
         set_sources_mode = getattr(config, 'set_sources_mode', None)
         if set_sources_mode is not None:
@@ -750,7 +576,7 @@
                 config.error('no version information for %s' % cube)
                 continue
             if installedversion > applversion:
-                toupgrade.append( (cube, applversion, installedversion) )
+                toupgrade.append((cube, applversion, installedversion))
         cubicwebversion = config.cubicweb_version()
         if self.config.force_cubicweb_version:
             applcubicwebversion = Version(self.config.force_cubicweb_version)
@@ -759,9 +585,6 @@
             applcubicwebversion = vcconf.get('cubicweb')
         if cubicwebversion > applcubicwebversion:
             toupgrade.append(('cubicweb', applcubicwebversion, cubicwebversion))
-        # only stop once we're sure we have something to do
-        if instance_running and not self.config.nostartstop:
-            StopInstanceCommand(self.logger).stop_instance(appid)
         # run cubicweb/componants migration scripts
         if self.config.fs_only or toupgrade:
             for cube, fromversion, toversion in toupgrade:
@@ -783,13 +606,6 @@
         if helper:
             helper.postupgrade(repo)
         print('-> instance migrated.')
-        if instance_running and not self.config.nostartstop:
-            # restart instance through fork to get a proper environment, avoid
-            # uicfg pb (and probably gettext catalogs, to check...)
-            forkcmd = '%s start %s' % (sys.argv[0], appid)
-            status = system(forkcmd)
-            if status:
-                print('%s exited with status %s' % (forkcmd, status))
         print()
 
     def i18nupgrade(self, config):
@@ -819,7 +635,7 @@
     name = 'versions'
 
     def versions_instance(self, appid):
-        config = cwcfg.config_for(appid)
+        config = self.cwconfig
         # should not raise error if db versions don't match fs versions
         config.repairing = True
         # no need to load all appobjects and schema
@@ -828,7 +644,8 @@
             config.set_sources_mode(('migration',))
         vcconf = config.repository().get_versions()
         for key in sorted(vcconf):
-            print(key+': %s.%s.%s' % vcconf[key])
+            print(key + ': %s.%s.%s' % vcconf[key])
+
 
 class ShellCommand(Command):
     """Run an interactive migration shell on an instance. This is a python shell
@@ -849,17 +666,18 @@
     name = 'shell'
     arguments = '<instance> [batch command file(s)] [-- <script arguments>]'
     min_args = 1
-    options = (
+    max_args = None
+    options = merge_options((
         ('system-only',
-         {'short': 'S', 'action' : 'store_true',
+         {'short': 'S', 'action': 'store_true',
           'help': 'only connect to the system source when the instance is '
           'using multiple sources. You can\'t use this option and the '
           '--ext-sources option at the same time.',
           'group': 'local'
-         }),
+          }),
 
         ('ext-sources',
-         {'short': 'E', 'type' : 'csv', 'metavar': '<sources>',
+         {'short': 'E', 'type': 'csv', 'metavar': '<sources>',
           'help': "For multisources instances, specify to which sources the \
 repository should connect to for upgrading. When unspecified or 'all' given, \
 will connect to all defined sources. If 'migration' is given, appropriate \
@@ -868,19 +686,12 @@
           }),
 
         ('force',
-         {'short': 'f', 'action' : 'store_true',
+         {'short': 'f', 'action': 'store_true',
           'help': 'don\'t check instance is up to date.',
           'group': 'local'
           }),
 
-        ('repo-uri',
-         {'short': 'H', 'type' : 'string', 'metavar': '<protocol>://<[host][:port]>',
-          'help': 'URI of the CubicWeb repository to connect to. URI can be \
-a ZMQ URL or inmemory:// (default) use an in-memory repository. THIS OPTION IS DEPRECATED, \
-directly give URI as instance id instead',
-          'group': 'remote'
-          }),
-        )
+    ) + InstanceCommand.options)
 
     def _get_mih(self, appid):
         """ returns migration context handler & shutdown function """
@@ -899,12 +710,6 @@
 
     def run(self, args):
         appuri = args.pop(0)
-        if self.config.repo_uri:
-            warn('[3.16] --repo-uri option is deprecated, directly give the URI as instance id',
-                 DeprecationWarning)
-            if urlparse(self.config.repo_uri).scheme == 'inmemory':
-                appuri = '%s/%s' % (self.config.repo_uri.rstrip('/'), appuri)
-
         mih, shutdown_callback = self._get_mih(appuri)
         try:
             with mih.cnx:
@@ -914,8 +719,8 @@
                         # remember that usage requires instance appid as first argument
                         scripts, args = self.cmdline_parser.largs[1:], self.cmdline_parser.rargs
                         for script in scripts:
-                                mih.cmd_process_script(script, scriptargs=args)
-                                mih.commit()
+                            mih.cmd_process_script(script, scriptargs=args)
+                            mih.commit()
                     else:
                         mih.interactive_shell()
         finally:
@@ -925,17 +730,15 @@
 class RecompileInstanceCatalogsCommand(InstanceCommand):
     """Recompile i18n catalogs for instances.
 
-    <instance>...
-      identifiers of the instances to consider. If no instance is
-      given, recompile for all registered instances.
+    <instance>
+      identifier of the instance to consider.
     """
     name = 'i18ninstance'
 
-    @staticmethod
-    def i18ninstance_instance(appid):
+    def i18ninstance_instance(self, appid):
         """recompile instance's messages catalogs"""
-        config = cwcfg.config_for(appid)
-        config.quick_start = True # notify this is not a regular start
+        config = self.cwconfig
+        config.quick_start = True  # notify this is not a regular start
         repo = config.repository()
         if config._cubes is None:
             # web only config
@@ -967,106 +770,39 @@
         for cube in cwcfg.available_cubes():
             print(cube)
 
+
 class ConfigureInstanceCommand(InstanceCommand):
     """Configure instance.
 
-    <instance>...
+    <instance>
       identifier of the instance to configure.
     """
     name = 'configure'
     actionverb = 'configured'
 
-    options = merge_options(InstanceCommand.options +
-                            (('param',
-                              {'short': 'p', 'type' : 'named', 'metavar' : 'key1:value1,key2:value2',
-                               'default': None,
-                               'help': 'set <key> to <value> in configuration file.',
-                               }),
-                             ))
+    options = merge_options(
+        InstanceCommand.options + (
+            ('param',
+             {'short': 'p', 'type': 'named', 'metavar': 'key1:value1,key2:value2',
+              'default': None,
+              'help': 'set <key> to <value> in configuration file.'}),
+        ),
+    )
 
     def configure_instance(self, appid):
         if self.config.param is not None:
-            appcfg = cwcfg.config_for(appid)
+            appcfg = self.cwconfig
             for key, value in self.config.param.items():
                 try:
                     appcfg.global_set_option(key, value)
                 except KeyError:
-                    raise ConfigurationError('unknown configuration key "%s" for mode %s' % (key, appcfg.name))
+                    raise ConfigurationError(
+                        'unknown configuration key "%s" for mode %s' % (key, appcfg.name))
             appcfg.save()
 
 
-# WSGI #########
-
-WSGI_CHOICES = {}
-try:
-    from cubicweb.wsgi import server as stdlib_server
-except ImportError:
-    pass
-else:
-    WSGI_CHOICES['stdlib'] = stdlib_server
-try:
-    from cubicweb.wsgi import wz
-except ImportError:
-    pass
-else:
-    WSGI_CHOICES['werkzeug'] = wz
-try:
-    from cubicweb.wsgi import tnd
-except ImportError:
-    pass
-else:
-    WSGI_CHOICES['tornado'] = tnd
-
-
-def wsgichoices():
-    return tuple(WSGI_CHOICES)
-
-if WSGI_CHOICES:
-    class WSGIStartHandler(InstanceCommand):
-        """Start an interactive wsgi server """
-        name = 'wsgi'
-        actionverb = 'started'
-        arguments = '<instance>'
-
-        @property
-        def options(self):
-            return (
-                ("debug",
-                 {'short': 'D', 'action': 'store_true',
-                  'default': False,
-                  'help': 'start server in debug mode.'}),
-                ('method',
-                 {'short': 'm',
-                  'type': 'choice',
-                  'metavar': '<method>',
-                  'default': 'stdlib',
-                  'choices': wsgichoices(),
-                  'help': 'wsgi utility/method'}),
-                ('loglevel',
-                 {'short': 'l',
-                  'type': 'choice',
-                  'metavar': '<log level>',
-                  'default': None,
-                  'choices': ('debug', 'info', 'warning', 'error'),
-                  'help': 'debug if -D is set, error otherwise',
-              }),
-            )
-
-        def wsgi_instance(self, appid):
-            config = cwcfg.config_for(appid, debugmode=self['debug'])
-            init_cmdline_log_threshold(config, self['loglevel'])
-            assert config.name == 'all-in-one'
-            meth = self['method']
-            server = WSGI_CHOICES[meth]
-            return server.run(config)
-
-    CWCTL.register(WSGIStartHandler)
-
-
 for cmdcls in (ListCommand,
                CreateInstanceCommand, DeleteInstanceCommand,
-               StartInstanceCommand, StopInstanceCommand, RestartInstanceCommand,
-               ReloadConfigurationCommand, StatusCommand,
                UpgradeInstanceCommand,
                ListVersionsInstanceCommand,
                ShellCommand,
@@ -1077,10 +813,8 @@
     CWCTL.register(cmdcls)
 
 
-
 def run(args=sys.argv[1:]):
     """command line tool"""
-    import os
     filterwarnings('default', category=DeprecationWarning)
     cwcfg.load_cwctl_plugins()
     try:
@@ -1092,5 +826,6 @@
         print(err)
         sys.exit(2)
 
+
 if __name__ == '__main__':
     run()
--- a/cubicweb/cwgettext.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/cwgettext.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,8 +18,6 @@
 
 import gettext
 
-from six import PY3
-
 
 class cwGNUTranslations(gettext.GNUTranslations):
     # The encoding of a msgctxt and a msgid in a .mo file is
@@ -85,8 +83,7 @@
             else:
                 return msgid2
 
-    if PY3:
-        ugettext = gettext.GNUTranslations.gettext
+    ugettext = gettext.GNUTranslations.gettext
 
     def upgettext(self, context, message):
         ctxt_message_id = self.CONTEXT_ENCODING % (context, message)
@@ -97,7 +94,7 @@
             return self.ugettext(message)
             if self._fallback:
                 return self._fallback.upgettext(context, message)
-            return unicode(message)
+            return str(message)
         return tmsg
 
     def unpgettext(self, context, msgid1, msgid2, n):
@@ -108,9 +105,9 @@
             if self._fallback:
                 return self._fallback.unpgettext(context, msgid1, msgid2, n)
             if n == 1:
-                tmsg = unicode(msgid1)
+                tmsg = str(msgid1)
             else:
-                tmsg = unicode(msgid2)
+                tmsg = str(msgid2)
         return tmsg
 
 
--- a/cubicweb/cwvreg.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/cwvreg.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,12 +21,9 @@
 
 import sys
 from os.path import join, dirname, realpath
-from warnings import warn
 from datetime import datetime, date, time, timedelta
 from functools import reduce
 
-from six import text_type, binary_type
-
 from logilab.common.decorators import cached, clear_cache
 from logilab.common.deprecation import class_deprecated
 from logilab.common.modutils import clean_sys_modules
@@ -128,7 +125,7 @@
     def register(self, obj, **kwargs):
         obj = related_appobject(obj)
         oid = kwargs.get('oid') or obj.__regid__
-        if oid != 'Any' and not oid in self.schema:
+        if oid != 'Any' and oid not in self.schema:
             self.error('don\'t register %s, %s type not defined in the '
                        'schema', obj, oid)
             return
@@ -219,9 +216,9 @@
         """
         obj = self.select(oid, req, rset=rset, **kwargs)
         res = obj.render(**kwargs)
-        if isinstance(res, text_type):
+        if isinstance(res, str):
             return res.encode(req.encoding)
-        assert isinstance(res, binary_type)
+        assert isinstance(res, bytes)
         return res
 
     def possible_views(self, req, rset=None, **kwargs):
@@ -253,7 +250,7 @@
         if rset is None:
             actions = self.poss_visible_objects(req, rset=rset, **kwargs)
         else:
-            actions = rset.possible_actions(**kwargs) # cached implementation
+            actions = rset.possible_actions(**kwargs)  # cached implementation
         result = {}
         for action in actions:
             result.setdefault(action.category, []).append(action)
@@ -295,21 +292,6 @@
         return thisctxcomps
 
 
-class BwCompatCWRegistry(object):
-    def __init__(self, vreg, oldreg, redirecttoreg):
-        self.vreg = vreg
-        self.oldreg = oldreg
-        self.redirecto = redirecttoreg
-
-    def __getattr__(self, attr):
-        warn('[3.10] you should now use the %s registry instead of the %s registry'
-             % (self.redirecto, self.oldreg), DeprecationWarning, stacklevel=2)
-        return getattr(self.vreg[self.redirecto], attr)
-
-    def clear(self): pass
-    def initialization_completed(self): pass
-
-
 class CWRegistryStore(RegistryStore):
     """Central registry for the cubicweb instance, extending the generic
     RegistryStore with some cubicweb specific stuff.
@@ -366,8 +348,6 @@
             sys.path.remove(CW_SOFTWARE_ROOT)
         self.schema = None
         self.initialized = False
-        self['boxes'] = BwCompatCWRegistry(self, 'boxes', 'ctxcomponents')
-        self['contentnavigation'] = BwCompatCWRegistry(self, 'contentnavigation', 'ctxcomponents')
 
     def setdefault(self, regid):
         try:
@@ -379,12 +359,14 @@
     def items(self):
         return [item for item in super(CWRegistryStore, self).items()
                 if not item[0] in ('propertydefs', 'propertyvalues')]
+
     def iteritems(self):
         return (item for item in super(CWRegistryStore, self).items()
                 if not item[0] in ('propertydefs', 'propertyvalues'))
 
     def values(self):
         return [value for key, value in self.items()]
+
     def itervalues(self):
         return (value for key, value in self.items())
 
@@ -479,7 +461,7 @@
             # bad class reference pb after reloading
             cfg = self.config
             for cube in cfg.expand_cubes(cubes, with_recommends=True):
-                if not cube in cubes:
+                if cube not in cubes:
                     cube_modnames = cfg.appobjects_cube_modnames(cube)
                     self._cleanup_sys_modules(cube_modnames)
         self.register_modnames(modnames)
@@ -523,12 +505,6 @@
         if depends_on is not None:
             self._needs_appobject[obj] = depends_on
 
-    def register_objects(self, path):
-        """overriden to give cubicweb's extrapath (eg cubes package's __path__)
-        """
-        super(CWRegistryStore, self).register_objects(
-            path, self.config.extrapath)
-
     def initialization_completed(self):
         """cw specific code once vreg initialization is completed:
 
@@ -555,7 +531,7 @@
                                registry.objid(obj), ' or '.join(regids), regname)
                     self.unregister(obj)
         super(CWRegistryStore, self).initialization_completed()
-        if 'uicfg' in self: # 'uicfg' is not loaded in a pure repository mode
+        if 'uicfg' in self:  # 'uicfg' is not loaded in a pure repository mode
             for rtags in self['uicfg'].values():
                 for rtag in rtags:
                     # don't check rtags if we don't want to cleanup_unused_appobjects
@@ -634,8 +610,8 @@
         vocab = pdef['vocabulary']
         if vocab is not None:
             if callable(vocab):
-                vocab = vocab(None) # XXX need a req object
-            if not value in vocab:
+                vocab = vocab(None)  # XXX need a req object
+            if value not in vocab:
                 raise ValueError(_('unauthorized value'))
         return value
 
@@ -658,11 +634,11 @@
 # XXX unify with yams.constraints.BASE_CONVERTERS?
 YAMS_TO_PY = BASE_CONVERTERS.copy()
 YAMS_TO_PY.update({
-    'Bytes':      Binary,
-    'Date':       date,
-    'Datetime':   datetime,
+    'Bytes': Binary,
+    'Date': date,
+    'Datetime': datetime,
     'TZDatetime': datetime,
-    'Time':       time,
-    'TZTime':     time,
-    'Interval':   timedelta,
-    })
+    'Time': time,
+    'TZTime': time,
+    'Interval': timedelta,
+})
--- a/cubicweb/dataimport/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/dataimport/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -32,4 +32,3 @@
 from cubicweb.dataimport.stores import *
 from cubicweb.dataimport.pgstore import *
 from cubicweb.dataimport.csv import *
-from cubicweb.dataimport.deprecated import *
--- a/cubicweb/dataimport/csv.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/dataimport/csv.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,19 +16,14 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Functions to help importing CSV data"""
-from __future__ import absolute_import, print_function
-
 import codecs
 import csv as csvmod
-import warnings
-
-from six import PY2, PY3, string_types
 
 from logilab.common import shellutils
 
 
 def count_lines(stream_or_filename):
-    if isinstance(stream_or_filename, string_types):
+    if isinstance(stream_or_filename, str):
         f = open(stream_or_filename)
     else:
         f = stream_or_filename
@@ -41,16 +36,9 @@
 
 
 def ucsvreader_pb(stream_or_path, encoding='utf-8', delimiter=',', quotechar='"',
-                  skipfirst=False, withpb=True, skip_empty=True, separator=None,
-                  quote=None):
+                  skipfirst=False, withpb=True, skip_empty=True):
     """same as :func:`ucsvreader` but a progress bar is displayed as we iter on rows"""
-    if separator is not None:
-        delimiter = separator
-        warnings.warn("[3.20] 'separator' kwarg is deprecated, use 'delimiter' instead")
-    if quote is not None:
-        quotechar = quote
-        warnings.warn("[3.20] 'quote' kwarg is deprecated, use 'quotechar' instead")
-    if isinstance(stream_or_path, string_types):
+    if isinstance(stream_or_path, str):
         stream = open(stream_or_path, 'rb')
     else:
         stream = stream_or_path
@@ -68,8 +56,7 @@
 
 
 def ucsvreader(stream, encoding='utf-8', delimiter=',', quotechar='"',
-               skipfirst=False, ignore_errors=False, skip_empty=True,
-               separator=None, quote=None):
+               skipfirst=False, ignore_errors=False, skip_empty=True):
     """A csv reader that accepts files with any encoding and outputs unicode
     strings
 
@@ -77,25 +64,14 @@
     separators) will be skipped. This is useful for Excel exports which may be
     full of such lines.
     """
-    if PY3:
-        stream = codecs.getreader(encoding)(stream)
-    if separator is not None:
-        delimiter = separator
-        warnings.warn("[3.20] 'separator' kwarg is deprecated, use 'delimiter' instead")
-    if quote is not None:
-        quotechar = quote
-        warnings.warn("[3.20] 'quote' kwarg is deprecated, use 'quotechar' instead")
+    stream = codecs.getreader(encoding)(stream)
     it = iter(csvmod.reader(stream, delimiter=delimiter, quotechar=quotechar))
     if not ignore_errors:
         if skipfirst:
             next(it)
         for row in it:
-            if PY2:
-                decoded = [item.decode(encoding) for item in row]
-            else:
-                decoded = row
-            if not skip_empty or any(decoded):
-                yield decoded
+            if not skip_empty or any(row):
+                yield row
     else:
         if skipfirst:
             try:
@@ -112,9 +88,5 @@
             # Error in CSV, ignore line and continue
             except csvmod.Error:
                 continue
-            if PY2:
-                decoded = [item.decode(encoding) for item in row]
-            else:
-                decoded = row
-            if not skip_empty or any(decoded):
-                yield decoded
+            if not skip_empty or any(row):
+                yield row
--- a/cubicweb/dataimport/deprecated.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,463 +0,0 @@
-# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Old and deprecated dataimport API that provides tools to import tabular data.
-
-
-Example of use (run this with `cubicweb-ctl shell instance import-script.py`):
-
-.. sourcecode:: python
-
-  from cubicweb.dataimport import *
-  # define data generators
-  GENERATORS = []
-
-  USERS = [('Prenom', 'firstname', ()),
-           ('Nom', 'surname', ()),
-           ('Identifiant', 'login', ()),
-           ]
-
-  def gen_users(ctl):
-      for row in ctl.iter_and_commit('utilisateurs'):
-          entity = mk_entity(row, USERS)
-          entity['upassword'] = 'motdepasse'
-          ctl.check('login', entity['login'], None)
-          entity = ctl.store.prepare_insert_entity('CWUser', **entity)
-          email = ctl.store.prepare_insert_entity('EmailAddress', address=row['email'])
-          ctl.store.prepare_insert_relation(entity, 'use_email', email)
-          ctl.store.rql('SET U in_group G WHERE G name "users", U eid %(x)s', {'x': entity})
-
-  CHK = [('login', check_doubles, 'Utilisateurs Login',
-          'Deux utilisateurs ne devraient pas avoir le meme login.'),
-         ]
-
-  GENERATORS.append( (gen_users, CHK) )
-
-  # create controller
-  ctl = CWImportController(RQLObjectStore(cnx))
-  ctl.askerror = 1
-  ctl.generators = GENERATORS
-  ctl.data['utilisateurs'] = lazytable(ucsvreader(open('users.csv')))
-  # run
-  ctl.run()
-
-.. BUG file with one column are not parsable
-.. TODO rollback() invocation is not possible yet
-"""
-from __future__ import print_function
-
-import sys
-import traceback
-from io import StringIO
-
-from six import add_metaclass
-
-from logilab.common import attrdict, shellutils
-from logilab.common.date import strptime
-from logilab.common.deprecation import deprecated, class_deprecated
-
-from cubicweb import QueryError
-from cubicweb.dataimport import callfunc_every
-
-
-@deprecated('[3.21] deprecated')
-def lazytable(reader):
-    """The first row is taken to be the header of the table and
-    used to output a dict for each row of data.
-
-    >>> data = lazytable(ucsvreader(open(filename)))
-    """
-    header = next(reader)
-    for row in reader:
-        yield dict(zip(header, row))
-
-
-@deprecated('[3.21] deprecated')
-def lazydbtable(cu, table, headers, orderby=None):
-    """return an iterator on rows of a sql table. On each row, fetch columns
-    defined in headers and return values as a dictionary.
-
-    >>> data = lazydbtable(cu, 'experimentation', ('id', 'nickname', 'gps'))
-    """
-    sql = 'SELECT %s FROM %s' % (','.join(headers), table,)
-    if orderby:
-        sql += ' ORDER BY %s' % ','.join(orderby)
-    cu.execute(sql)
-    while True:
-        row = cu.fetchone()
-        if row is None:
-            break
-        yield dict(zip(headers, row))
-
-
-@deprecated('[3.21] deprecated')
-def tell(msg):
-    print(msg)
-
-
-@deprecated('[3.21] deprecated')
-def confirm(question):
-    """A confirm function that asks for yes/no/abort and exits on abort."""
-    answer = shellutils.ASK.ask(question, ('Y', 'n', 'abort'), 'Y')
-    if answer == 'abort':
-        sys.exit(1)
-    return answer == 'Y'
-
-
-@add_metaclass(class_deprecated)
-class catch_error(object):
-    """Helper for @contextmanager decorator."""
-    __deprecation_warning__ = '[3.21] deprecated'
-
-    def __init__(self, ctl, key='unexpected error', msg=None):
-        self.ctl = ctl
-        self.key = key
-        self.msg = msg
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, type, value, traceback):
-        if type is not None:
-            if issubclass(type, (KeyboardInterrupt, SystemExit)):
-                return # re-raise
-            if self.ctl.catcherrors:
-                self.ctl.record_error(self.key, None, type, value, traceback)
-                return True # silent
-
-@deprecated('[3.21] deprecated')
-def mk_entity(row, map):
-    """Return a dict made from sanitized mapped values.
-
-    ValueError can be raised on unexpected values found in checkers
-
-    >>> row = {'myname': u'dupont'}
-    >>> map = [('myname', u'name', (call_transform_method('title'),))]
-    >>> mk_entity(row, map)
-    {'name': u'Dupont'}
-    >>> row = {'myname': u'dupont', 'optname': u''}
-    >>> map = [('myname', u'name', (call_transform_method('title'),)),
-    ...        ('optname', u'MARKER', (optional,))]
-    >>> mk_entity(row, map)
-    {'name': u'Dupont', 'optname': None}
-    """
-    res = {}
-    assert isinstance(row, dict)
-    assert isinstance(map, list)
-    for src, dest, funcs in map:
-        try:
-            res[dest] = row[src]
-        except KeyError:
-            continue
-        try:
-            for func in funcs:
-                res[dest] = func(res[dest])
-                if res[dest] is None:
-                    break
-        except ValueError as err:
-            exc = ValueError('error with %r field: %s' % (src, err))
-            exc.__traceback__ = sys.exc_info()[-1]
-            raise exc
-    return res
-
-
-# base sanitizing/coercing functions ###########################################
-
-@deprecated('[3.21] deprecated')
-def optional(value):
-    """checker to filter optional field
-
-    If value is undefined (ex: empty string), return None that will
-    break the checkers validation chain
-
-    General use is to add 'optional' check in first condition to avoid
-    ValueError by further checkers
-
-    >>> MAPPER = [(u'value', 'value', (optional, int))]
-    >>> row = {'value': u'XXX'}
-    >>> mk_entity(row, MAPPER)
-    {'value': None}
-    >>> row = {'value': u'100'}
-    >>> mk_entity(row, MAPPER)
-    {'value': 100}
-    """
-    if value:
-        return value
-    return None
-
-
-@deprecated('[3.21] deprecated')
-def required(value):
-    """raise ValueError if value is empty
-
-    This check should be often found in last position in the chain.
-    """
-    if value:
-        return value
-    raise ValueError("required")
-
-
-@deprecated('[3.21] deprecated')
-def todatetime(format='%d/%m/%Y'):
-    """return a transformation function to turn string input value into a
-    `datetime.datetime` instance, using given format.
-
-    Follow it by `todate` or `totime` functions from `logilab.common.date` if
-    you want a `date`/`time` instance instead of `datetime`.
-    """
-    def coerce(value):
-        return strptime(value, format)
-    return coerce
-
-
-@deprecated('[3.21] deprecated')
-def call_transform_method(methodname, *args, **kwargs):
-    """return value returned by calling the given method on input"""
-    def coerce(value):
-        return getattr(value, methodname)(*args, **kwargs)
-    return coerce
-
-
-@deprecated('[3.21] deprecated')
-def call_check_method(methodname, *args, **kwargs):
-    """check value returned by calling the given method on input is true,
-    else raise ValueError
-    """
-    def check(value):
-        if getattr(value, methodname)(*args, **kwargs):
-            return value
-        raise ValueError('%s not verified on %r' % (methodname, value))
-    return check
-
-
-# base integrity checking functions ############################################
-
-@deprecated('[3.21] deprecated')
-def check_doubles(buckets):
-    """Extract the keys that have more than one item in their bucket."""
-    return [(k, len(v)) for k, v in buckets.items() if len(v) > 1]
-
-
-@deprecated('[3.21] deprecated')
-def check_doubles_not_none(buckets):
-    """Extract the keys that have more than one item in their bucket."""
-    return [(k, len(v)) for k, v in buckets.items()
-            if k is not None and len(v) > 1]
-
-
-@add_metaclass(class_deprecated)
-class ObjectStore(object):
-    """Store objects in memory for *faster* validation (development mode)
-
-    But it will not enforce the constraints of the schema and hence will miss some problems
-
-    >>> store = ObjectStore()
-    >>> user = store.prepare_insert_entity('CWUser', login=u'johndoe')
-    >>> group = store.prepare_insert_entity('CWUser', name=u'unknown')
-    >>> store.prepare_insert_relation(user, 'in_group', group)
-    """
-    __deprecation_warning__ = '[3.21] use the new importer API'
-
-    def __init__(self):
-        self.items = []
-        self.eids = {}
-        self.types = {}
-        self.relations = set()
-        self.indexes = {}
-
-    def prepare_insert_entity(self, etype, **data):
-        """Given an entity type, attributes and inlined relations, return an eid for the entity that
-        would be inserted with a real store.
-        """
-        data = attrdict(data)
-        data['eid'] = eid = len(self.items)
-        self.items.append(data)
-        self.eids[eid] = data
-        self.types.setdefault(etype, []).append(eid)
-        return eid
-
-    def prepare_update_entity(self, etype, eid, **kwargs):
-        """Given an entity type and eid, updates the corresponding fake entity with specified
-        attributes and inlined relations.
-        """
-        assert eid in self.types[etype], 'Trying to update with wrong type %s' % etype
-        data = self.eids[eid]
-        data.update(kwargs)
-
-    def prepare_insert_relation(self, eid_from, rtype, eid_to, **kwargs):
-        """Store into the `relations` attribute that a relation ``rtype`` exists between entities
-        with eids ``eid_from`` and ``eid_to``.
-        """
-        relation = eid_from, rtype, eid_to
-        self.relations.add(relation)
-        return relation
-
-    def flush(self):
-        """Nothing to flush for this store."""
-        pass
-
-    def commit(self):
-        """Nothing to commit for this store."""
-        return
-
-    def finish(self):
-        """Nothing to do once import is terminated for this store."""
-        pass
-
-    @property
-    def nb_inserted_entities(self):
-        return len(self.eids)
-
-    @property
-    def nb_inserted_types(self):
-        return len(self.types)
-
-    @property
-    def nb_inserted_relations(self):
-        return len(self.relations)
-
-    @deprecated('[3.21] use prepare_insert_entity instead')
-    def create_entity(self, etype, **data):
-        self.prepare_insert_entity(etype, **data)
-        return attrdict(data)
-
-    @deprecated('[3.21] use prepare_insert_relation instead')
-    def relate(self, eid_from, rtype, eid_to, **kwargs):
-        self.prepare_insert_relation(eid_from, rtype, eid_to, **kwargs)
-
-
-@add_metaclass(class_deprecated)
-class CWImportController(object):
-    """Controller of the data import process.
-
-    >>> ctl = CWImportController(store)
-    >>> ctl.generators = list_of_data_generators
-    >>> ctl.data = dict_of_data_tables
-    >>> ctl.run()
-    """
-    __deprecation_warning__ = '[3.21] use the new importer API'
-
-    def __init__(self, store, askerror=0, catcherrors=None, tell=tell,
-                 commitevery=50):
-        self.store = store
-        self.generators = None
-        self.data = {}
-        self.errors = None
-        self.askerror = askerror
-        if  catcherrors is None:
-            catcherrors = askerror
-        self.catcherrors = catcherrors
-        self.commitevery = commitevery # set to None to do a single commit
-        self._tell = tell
-
-    def check(self, type, key, value):
-        self._checks.setdefault(type, {}).setdefault(key, []).append(value)
-
-    def check_map(self, entity, key, map, default):
-        try:
-            entity[key] = map[entity[key]]
-        except KeyError:
-            self.check(key, entity[key], None)
-            entity[key] = default
-
-    def record_error(self, key, msg=None, type=None, value=None, tb=None):
-        tmp = StringIO()
-        if type is None:
-            traceback.print_exc(file=tmp)
-        else:
-            traceback.print_exception(type, value, tb, file=tmp)
-        # use a list to avoid counting a <nb lines> errors instead of one
-        errorlog = self.errors.setdefault(key, [])
-        if msg is None:
-            errorlog.append(tmp.getvalue().splitlines())
-        else:
-            errorlog.append( (msg, tmp.getvalue().splitlines()) )
-
-    def run(self):
-        self.errors = {}
-        if self.commitevery is None:
-            self.tell('Will commit all or nothing.')
-        else:
-            self.tell('Will commit every %s iterations' % self.commitevery)
-        for func, checks in self.generators:
-            self._checks = {}
-            func_name = func.__name__
-            self.tell("Run import function '%s'..." % func_name)
-            try:
-                func(self)
-            except Exception:
-                if self.catcherrors:
-                    self.record_error(func_name, 'While calling %s' % func.__name__)
-                else:
-                    self._print_stats()
-                    raise
-            for key, func, title, help in checks:
-                buckets = self._checks.get(key)
-                if buckets:
-                    err = func(buckets)
-                    if err:
-                        self.errors[title] = (help, err)
-        try:
-            txuuid = self.store.commit()
-            if txuuid is not None:
-                self.tell('Transaction commited (txuuid: %s)' % txuuid)
-        except QueryError as ex:
-            self.tell('Transaction aborted: %s' % ex)
-        self._print_stats()
-        if self.errors:
-            if self.askerror == 2 or (self.askerror and confirm('Display errors ?')):
-                from pprint import pformat
-                for errkey, error in self.errors.items():
-                    self.tell("\n%s (%s): %d\n" % (error[0], errkey, len(error[1])))
-                    self.tell(pformat(sorted(error[1])))
-
-    def _print_stats(self):
-        nberrors = sum(len(err) for err in self.errors.values())
-        self.tell('\nImport statistics: %i entities, %i types, %i relations and %i errors'
-                  % (self.store.nb_inserted_entities,
-                     self.store.nb_inserted_types,
-                     self.store.nb_inserted_relations,
-                     nberrors))
-
-    def get_data(self, key):
-        return self.data.get(key)
-
-    def index(self, name, key, value, unique=False):
-        """create a new index
-
-        If unique is set to True, only first occurence will be kept not the following ones
-        """
-        if unique:
-            try:
-                if value in self.store.indexes[name][key]:
-                    return
-            except KeyError:
-                # we're sure that one is the first occurence; so continue...
-                pass
-        self.store.indexes.setdefault(name, {}).setdefault(key, []).append(value)
-
-    def tell(self, msg):
-        self._tell(msg)
-
-    def iter_and_commit(self, datakey):
-        """iter rows, triggering commit every self.commitevery iterations"""
-        if self.commitevery is None:
-            return self.get_data(datakey)
-        else:
-            return callfunc_every(self.store.commit,
-                                  self.commitevery,
-                                  self.get_data(datakey))
--- a/cubicweb/dataimport/importer.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/dataimport/importer.py	Fri Oct 18 23:39:03 2019 +0200
@@ -30,8 +30,6 @@
 from collections import defaultdict
 import logging
 
-import six
-
 from logilab.mtconverter import xml_escape
 
 from cubicweb import Binary
@@ -74,7 +72,7 @@
         for extentity in extentities:
             if extentity.extid not in extid2eid:
                 cwuri = extentity.extid
-                if isinstance(cwuri, six.binary_type):
+                if isinstance(cwuri, bytes):
                     cwuri = cwuri.decode('utf-8')
                 extentity.values.setdefault('cwuri', set([cwuri]))
             yield extentity
--- a/cubicweb/dataimport/massive_store.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/dataimport/massive_store.py	Fri Oct 18 23:39:03 2019 +0200
@@ -22,9 +22,6 @@
 import logging
 from uuid import uuid4
 
-from six import text_type
-from six.moves import range
-
 from cubicweb.dataimport import stores, pgstore
 from cubicweb.server.schema2sql import eschema_sql_def
 
@@ -70,7 +67,7 @@
         """
         super(MassiveObjectStore, self).__init__(cnx)
 
-        self.uuid = text_type(uuid4()).replace('-', '')
+        self.uuid = str(uuid4()).replace('-', '')
         self.slave_mode = slave_mode
         if metagen is None:
             metagen = stores.MetadataGenerator(cnx)
--- a/cubicweb/dataimport/pgstore.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/dataimport/pgstore.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,19 +17,13 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Postgres specific store"""
 
-from __future__ import print_function
-
 import warnings
 import os.path as osp
 from io import StringIO
 from time import asctime
 from datetime import date, datetime, time
 from collections import defaultdict
-
-from six import string_types, integer_types, text_type, add_metaclass
-from six.moves import cPickle as pickle, range
-
-from logilab.common.deprecation import class_deprecated
+import pickle
 
 from cubicweb.utils import make_uid
 from cubicweb.server.sqlutils import SQL_PREFIX
@@ -110,7 +104,7 @@
 
 def _copyfrom_buffer_convert_number(value, **opts):
     '''Convert a number into its string representation'''
-    return text_type(value)
+    return str(value)
 
 def _copyfrom_buffer_convert_string(value, **opts):
     '''Convert string value.
@@ -142,8 +136,8 @@
 # (types, converter) list.
 _COPYFROM_BUFFER_CONVERTERS = [
     (type(None), _copyfrom_buffer_convert_None),
-    (integer_types + (float,), _copyfrom_buffer_convert_number),
-    (string_types, _copyfrom_buffer_convert_string),
+    ((int, float), _copyfrom_buffer_convert_number),
+    (str, _copyfrom_buffer_convert_string),
     (datetime, _copyfrom_buffer_convert_datetime),
     (date, _copyfrom_buffer_convert_date),
     (time, _copyfrom_buffer_convert_time),
@@ -187,7 +181,7 @@
             for types, converter in _COPYFROM_BUFFER_CONVERTERS:
                 if isinstance(value, types):
                     value = converter(value, **convert_opts)
-                    assert isinstance(value, text_type)
+                    assert isinstance(value, str)
                     break
             else:
                 raise ValueError("Unsupported value type %s" % type(value))
@@ -198,75 +192,6 @@
     return StringIO('\n'.join(rows))
 
 
-@add_metaclass(class_deprecated)
-class SQLGenObjectStore(NoHookRQLObjectStore):
-    """Controller of the data import process. This version is based
-    on direct insertions throught SQL command (COPY FROM or execute many).
-
-    >>> store = SQLGenObjectStore(cnx)
-    >>> store.create_entity('Person', ...)
-    >>> store.flush()
-    """
-    __deprecation_warning__ = '[3.23] this class is deprecated, use MassiveObjectStore instead'
-
-    def __init__(self, cnx, dump_output_dir=None, nb_threads_statement=1):
-        """
-        Initialize a SQLGenObjectStore.
-
-        Parameters:
-
-          - cnx: connection on the cubicweb instance
-          - dump_output_dir: a directory to dump failed statements
-            for easier recovery. Default is None (no dump).
-        """
-        super(SQLGenObjectStore, self).__init__(cnx)
-        ### hijack default source
-        self._system_source = SQLGenSourceWrapper(
-            self._system_source, cnx.vreg.schema,
-            dump_output_dir=dump_output_dir)
-        ### XXX This is done in super().__init__(), but should be
-        ### redone here to link to the correct source
-        self._add_relation = self._system_source.add_relation
-        self.indexes_etypes = {}
-        if nb_threads_statement != 1:
-            warnings.warn('[3.21] SQLGenObjectStore is no longer threaded', DeprecationWarning)
-
-    def flush(self):
-        """Flush data to the database"""
-        self._system_source.flush()
-
-    def relate(self, subj_eid, rtype, obj_eid, **kwargs):
-        if subj_eid is None or obj_eid is None:
-            return
-        # XXX Could subjtype be inferred ?
-        self._add_relation(self._cnx, subj_eid, rtype, obj_eid,
-                           self.rschema(rtype).inlined, **kwargs)
-        if self.rschema(rtype).symmetric:
-            self._add_relation(self._cnx, obj_eid, rtype, subj_eid,
-                               self.rschema(rtype).inlined, **kwargs)
-
-    def drop_indexes(self, etype):
-        """Drop indexes for a given entity type"""
-        if etype not in self.indexes_etypes:
-            cu = self._cnx.cnxset.cu
-            def index_to_attr(index):
-                """turn an index name to (database) attribute name"""
-                return index.replace(etype.lower(), '').replace('idx', '').strip('_')
-            indices = [(index, index_to_attr(index))
-                       for index in self._system_source.dbhelper.list_indices(cu, etype)
-                       # Do not consider 'cw_etype_pkey' index
-                       if not index.endswith('key')]
-            self.indexes_etypes[etype] = indices
-        for index, attr in self.indexes_etypes[etype]:
-            self._cnx.system_sql('DROP INDEX %s' % index)
-
-    def create_indexes(self, etype):
-        """Recreate indexes for a given entity type"""
-        for index, attr in self.indexes_etypes.get(etype, []):
-            sql = 'CREATE INDEX %s ON cw_%s(%s)' % (index, etype, attr)
-            self._cnx.system_sql(sql)
-
-
 ###########################################################################
 ## SQL Source #############################################################
 ###########################################################################
--- a/cubicweb/dataimport/stores.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/dataimport/stores.py	Fri Oct 18 23:39:03 2019 +0200
@@ -58,17 +58,14 @@
 .. autoclass:: cubicweb.dataimport.stores.MetadataGenerator
 """
 import inspect
-import warnings
 from datetime import datetime
 from copy import copy
 from itertools import count
 
-from six import add_metaclass
-
 import pytz
 
 from logilab.common.decorators import cached
-from logilab.common.deprecation import deprecated, class_deprecated
+from logilab.common.deprecation import class_deprecated
 
 from cubicweb.schema import META_RTYPES, VIRTUAL_RTYPES
 from cubicweb.server.edition import EditedEntity
@@ -115,13 +112,9 @@
     """Store that works by making RQL queries, hence with all the cubicweb's machinery activated.
     """
 
-    def __init__(self, cnx, commit=None):
-        if commit is not None:
-            warnings.warn('[3.19] commit argument should not be specified '
-                          'as the cnx object already provides it.',
-                          DeprecationWarning, stacklevel=2)
+    def __init__(self, cnx):
         self._cnx = cnx
-        self._commit = commit or cnx.commit
+        self._commit = cnx.commit
         # XXX 3.21 deprecated attributes
         self.eids = {}
         self.types = {}
@@ -149,23 +142,6 @@
     def commit(self):
         return self._commit()
 
-    @deprecated("[3.19] use cnx.find(*args, **kwargs).entities() instead")
-    def find_entities(self, *args, **kwargs):
-        return self._cnx.find(*args, **kwargs).entities()
-
-    @deprecated("[3.19] use cnx.find(*args, **kwargs).one() instead")
-    def find_one_entity(self, *args, **kwargs):
-        return self._cnx.find(*args, **kwargs).one()
-
-    @deprecated('[3.21] use prepare_insert_entity instead')
-    def create_entity(self, *args, **kwargs):
-        eid = self.prepare_insert_entity(*args, **kwargs)
-        return self._cnx.entity_from_eid(eid)
-
-    @deprecated('[3.21] use prepare_insert_relation instead')
-    def relate(self, eid_from, rtype, eid_to, **kwargs):
-        self.prepare_insert_relation(eid_from, rtype, eid_to, **kwargs)
-
 
 class NoHookRQLObjectStore(RQLObjectStore):
     """Store that works by accessing low-level CubicWeb's source API, with all hooks deactivated. It
@@ -212,7 +188,7 @@
         self._system_source.add_info(cnx, entity, entity_source)
         self._system_source.add_entity(cnx, entity)
         kwargs = dict()
-        if inspect.getargspec(self._add_relation).keywords:
+        if inspect.getfullargspec(self._add_relation).varkw:
             kwargs['subjtype'] = entity.cw_etype
         for rtype, targeteids in rels.items():
             # targeteids may be a single eid or a list of eids
@@ -241,21 +217,6 @@
             self._add_relation(self._cnx, eid_to, rtype, eid_from, rschema.inlined)
         self._nb_inserted_relations += 1
 
-    @property
-    @deprecated('[3.21] deprecated')
-    def nb_inserted_entities(self):
-        return self._nb_inserted_entities
-
-    @property
-    @deprecated('[3.21] deprecated')
-    def nb_inserted_types(self):
-        return self._nb_inserted_types
-
-    @property
-    @deprecated('[3.21] deprecated')
-    def nb_inserted_relations(self):
-        return self._nb_inserted_relations
-
 
 class MetadataGenerator(object):
     """Class responsible for generating standard metadata for imported entities. You may want to
@@ -399,8 +360,7 @@
         return self._mdgen.source
 
 
-@add_metaclass(class_deprecated)
-class MetaGenerator(object):
+class MetaGenerator(object, metaclass=class_deprecated):
     """Class responsible for generating standard metadata for imported entities. You may want to
     derive it to add application specific's metadata.
 
--- a/cubicweb/dataimport/test/test_pgstore.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/dataimport/test/test_pgstore.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,15 +20,9 @@
 
 import datetime as DT
 
-from six import PY3
 from logilab.common.testlib import TestCase, unittest_main
 
 from cubicweb.dataimport import pgstore
-from cubicweb.devtools import testlib
-
-
-if PY3:
-    long = int
 
 
 class CreateCopyFromBufferTC(TestCase):
@@ -42,7 +36,7 @@
     def test_convert_number(self):
         cnvt = pgstore._copyfrom_buffer_convert_number
         self.assertEqual(u'42', cnvt(42))
-        self.assertEqual(u'42', cnvt(long(42)))
+        self.assertEqual(u'42', cnvt(int(42)))
         self.assertEqual(u'42.42', cnvt(42.42))
 
     def test_convert_string(self):
@@ -69,9 +63,9 @@
 
     # test buffer
     def test_create_copyfrom_buffer_tuple(self):
-        data = ((42, long(42), 42.42, u'éléphant', DT.date(666, 1, 13), DT.time(6, 6, 6),
+        data = ((42, int(42), 42.42, u'éléphant', DT.date(666, 1, 13), DT.time(6, 6, 6),
                  DT.datetime(666, 6, 13, 6, 6, 6)),
-                (6, long(6), 6.6, u'babar', DT.date(2014, 1, 14), DT.time(4, 2, 1),
+                (6, int(6), 6.6, u'babar', DT.date(2014, 1, 14), DT.time(4, 2, 1),
                  DT.datetime(2014, 1, 1, 0, 0, 0)))
         results = pgstore._create_copyfrom_buffer(data)
         # all columns
@@ -94,15 +88,5 @@
         self.assertEqual(expected, results.getvalue())
 
 
-class SQLGenObjectStoreTC(testlib.CubicWebTC):
-
-    def test_prepare_insert_entity(self):
-        with self.admin_access.repo_cnx() as cnx:
-            store = pgstore.SQLGenObjectStore(cnx)
-            eid = store.prepare_insert_entity('CWUser', login=u'toto',
-                                              upassword=u'pwd')
-            self.assertIsNotNone(eid)
-
-
 if __name__ == '__main__':
     unittest_main()
--- a/cubicweb/dataimport/test/test_sqlgenstore.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-# -*- coding: utf-8 -*-
-# copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr -- mailto:contact@logilab.fr
-#
-# This program is free software: you can redistribute it and/or modify it under
-# the terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with this program. If not, see <http://www.gnu.org/licenses/>.
-"""SQL object store test case"""
-
-from cubicweb.dataimport import ucsvreader
-from cubicweb.devtools import testlib, PostgresApptestConfiguration
-from cubicweb.devtools import startpgcluster, stoppgcluster
-from cubicweb.dataimport.pgstore import SQLGenObjectStore
-
-
-def setUpModule():
-    startpgcluster(__file__)
-
-
-def tearDownModule(*args):
-    stoppgcluster(__file__)
-
-
-class SQLGenImportSimpleTC(testlib.CubicWebTC):
-    configcls = PostgresApptestConfiguration
-    appid = 'data-massimport'
-
-    def cast(self, _type, value):
-        try:
-            return _type(value)
-        except ValueError:
-            return None
-
-    def push_geonames_data(self, dumpname, store):
-        # Push timezones
-        cnx = store._cnx
-        for code, gmt, dst, raw_offset in ucsvreader(open(self.datapath('timeZones.txt'), 'rb'),
-                                                     delimiter='\t'):
-            cnx.create_entity('TimeZone', code=code, gmt=float(gmt),
-                              dst=float(dst), raw_offset=float(raw_offset))
-        timezone_code = dict(cnx.execute('Any C, X WHERE X is TimeZone, X code C'))
-        cnx.commit()
-        # Push data
-        for ind, infos in enumerate(ucsvreader(open(dumpname, 'rb'),
-                                               delimiter='\t',
-                                               ignore_errors=True)):
-            if ind > 99:
-                break
-            latitude = self.cast(float, infos[4])
-            longitude = self.cast(float, infos[5])
-            population = self.cast(int, infos[14])
-            elevation = self.cast(int, infos[15])
-            gtopo = self.cast(int, infos[16])
-            feature_class = infos[6]
-            if len(infos[6]) != 1:
-                feature_class = None
-            entity = {'name': infos[1],
-                      'asciiname': infos[2],
-                      'alternatenames': infos[3],
-                      'latitude': latitude, 'longitude': longitude,
-                      'feature_class': feature_class,
-                      'alternate_country_code': infos[9],
-                      'admin_code_3': infos[12],
-                      'admin_code_4': infos[13],
-                      'population': population, 'elevation': elevation,
-                      'gtopo30': gtopo, 'timezone': timezone_code.get(infos[17]),
-                      'cwuri': u'http://sws.geonames.org/%s/' % int(infos[0]),
-                      'geonameid': int(infos[0]),
-                      }
-            store.prepare_insert_entity('Location', **entity)
-
-    def test_autoflush_metadata(self):
-        with self.admin_access.repo_cnx() as cnx:
-            crs = cnx.system_sql('SELECT * FROM entities WHERE type=%(t)s',
-                                 {'t': 'Location'})
-            self.assertEqual(len(crs.fetchall()), 0)
-            store = SQLGenObjectStore(cnx)
-            store.prepare_insert_entity('Location', name=u'toto')
-            store.flush()
-            store.commit()
-            cnx.commit()
-        with self.admin_access.repo_cnx() as cnx:
-            crs = cnx.system_sql('SELECT * FROM entities WHERE type=%(t)s',
-                                 {'t': 'Location'})
-            self.assertEqual(len(crs.fetchall()), 1)
-
-    def test_sqlgenstore_etype_metadata(self):
-        with self.admin_access.repo_cnx() as cnx:
-            store = SQLGenObjectStore(cnx)
-            timezone_eid = store.prepare_insert_entity('TimeZone', code=u'12')
-            store.prepare_insert_entity('Location', timezone=timezone_eid)
-            store.flush()
-            store.commit()
-            eid, etname = cnx.execute('Any X, TN WHERE X timezone TZ, X is T, '
-                                      'T name TN')[0]
-            self.assertEqual(cnx.entity_from_eid(eid).cw_etype, etname)
-
-    def test_simple_insert(self):
-        with self.admin_access.repo_cnx() as cnx:
-            store = SQLGenObjectStore(cnx)
-            self.push_geonames_data(self.datapath('geonames.csv'), store)
-            store.flush()
-            store.commit()
-        with self.admin_access.repo_cnx() as cnx:
-            rset = cnx.execute('Any X WHERE X is Location')
-            self.assertEqual(len(rset), 100)
-            rset = cnx.execute('Any X WHERE X is Location, X timezone T')
-            self.assertEqual(len(rset), 100)
-
-
-if __name__ == '__main__':
-    import unittest
-    unittest.main()
--- a/cubicweb/devtools/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,8 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Test tools for cubicweb"""
 
-from __future__ import print_function
-
 import os
 import sys
 import errno
@@ -31,18 +29,16 @@
 from hashlib import sha1  # pylint: disable=E0611
 from os.path import abspath, join, exists, split, isdir, dirname
 from functools import partial
+import pickle
 
-from six import text_type
-from six.moves import cPickle as pickle
+import filelock
 
 from logilab.common.decorators import cached, clear_cache
 
 from cubicweb import ExecutionError
 from cubicweb import schema, cwconfig
 from cubicweb.server.serverconfig import ServerConfiguration
-from cubicweb.etwist.twconfig import WebConfigurationBase
-
-cwconfig.CubicWebConfiguration.cls_adjust_sys_path()
+from cubicweb.web.webconfig import WebConfigurationBase
 
 # db auto-population configuration #############################################
 
@@ -94,7 +90,7 @@
 DEFAULT_PSQL_SOURCES = DEFAULT_SOURCES.copy()
 DEFAULT_PSQL_SOURCES['system'] = DEFAULT_SOURCES['system'].copy()
 DEFAULT_PSQL_SOURCES['system']['db-driver'] = 'postgres'
-DEFAULT_PSQL_SOURCES['system']['db-user'] = text_type(getpass.getuser())
+DEFAULT_PSQL_SOURCES['system']['db-user'] = getpass.getuser()
 DEFAULT_PSQL_SOURCES['system']['db-password'] = None
 # insert a dumb value as db-host to avoid unexpected connection to local server
 DEFAULT_PSQL_SOURCES['system']['db-host'] = 'REPLACEME'
@@ -396,7 +392,7 @@
         from cubicweb.repoapi import connect
         repo = self.get_repo()
         sources = self.config.read_sources_file()
-        login = text_type(sources['admin']['login'])
+        login = sources['admin']['login']
         password = sources['admin']['password'] or 'xxx'
         cnx = connect(repo, login, password=password)
         return cnx
@@ -427,11 +423,12 @@
         raise ValueError('no initialization function for driver %r' % self.DRIVER)
 
     def has_cache(self, db_id):
-        """Check if a given database id exist in cb cache for the current config"""
-        cache_glob = self.absolute_backup_file('*', '*')
-        if cache_glob not in self.explored_glob:
-            self.discover_cached_db()
-        return self.db_cache_key(db_id) in self.db_cache
+        """Check if a given database id exist in db cache for the current config"""
+        key = self.db_cache_key(db_id)
+        if key in self.db_cache:
+            return True
+        self.discover_cached_db()
+        return key in self.db_cache
 
     def discover_cached_db(self):
         """Search available db_if for the current config"""
@@ -469,20 +466,23 @@
         ``pre_setup_func`` to setup the database.
 
         This function backup any database it build"""
-        if self.has_cache(test_db_id):
-            return  # test_db_id, 'already in cache'
-        if test_db_id is DEFAULT_EMPTY_DB_ID:
-            self.init_test_database()
-        else:
-            print('Building %s for database %s' % (test_db_id, self.dbname))
-            self.build_db_cache(DEFAULT_EMPTY_DB_ID)
-            self.restore_database(DEFAULT_EMPTY_DB_ID)
-            self.get_repo(startup=True)
-            cnx = self.get_cnx()
-            with cnx:
-                pre_setup_func(cnx, self.config)
-                cnx.commit()
-        self.backup_database(test_db_id)
+        lockfile = join(self._ensure_test_backup_db_dir(),
+                        '{}.lock'.format(test_db_id))
+        with filelock.FileLock(lockfile):
+            if self.has_cache(test_db_id):
+                return  # test_db_id, 'already in cache'
+            if test_db_id is DEFAULT_EMPTY_DB_ID:
+                self.init_test_database()
+            else:
+                print('Building %s for database %s' % (test_db_id, self.dbname))
+                self.build_db_cache(DEFAULT_EMPTY_DB_ID)
+                self.restore_database(DEFAULT_EMPTY_DB_ID)
+                self.get_repo(startup=True)
+                cnx = self.get_cnx()
+                with cnx:
+                    pre_setup_func(cnx, self.config)
+                    cnx.commit()
+            self.backup_database(test_db_id)
 
 
 class NoCreateDropDatabaseHandler(TestDataBaseHandler):
--- a/cubicweb/devtools/dataimport.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,21 +0,0 @@
-# pylint: disable=W0614,W0401
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-from warnings import warn
-warn('moved to cubicweb.dataimport', DeprecationWarning, stacklevel=2)
-from cubicweb.dataimport import *
--- a/cubicweb/devtools/devctl.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/devctl.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,8 +18,6 @@
 """additional cubicweb-ctl commands and command handlers for cubicweb and
 cubicweb's cubes development
 """
-from __future__ import print_function
-
 # *ctl module should limit the number of import to be imported as quickly as
 # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
 # completion). So import locally in command helpers.
@@ -30,12 +28,9 @@
 from datetime import datetime, date
 from os import getcwd, mkdir, chdir, path as osp
 import pkg_resources
-from warnings import warn
 
 from pytz import UTC
 
-from six.moves import input
-
 from logilab.common import STD_BLACKLIST
 from logilab.common.modutils import clean_sys_modules
 from logilab.common.fileutils import ensure_fs_mode
@@ -481,12 +476,7 @@
         print('-> extracting messages:', end=' ')
         potfiles = []
         # static messages
-        if osp.exists(osp.join('i18n', 'entities.pot')):
-            warn('entities.pot is deprecated, rename file '
-                 'to static-messages.pot (%s)'
-                 % osp.join('i18n', 'entities.pot'), DeprecationWarning)
-            potfiles.append(osp.join('i18n', 'entities.pot'))
-        elif osp.exists(osp.join('i18n', 'static-messages.pot')):
+        if osp.exists(osp.join('i18n', 'static-messages.pot')):
             potfiles.append(osp.join('i18n', 'static-messages.pot'))
         # messages from schema
         potfiles.append(self.schemapot())
@@ -728,7 +718,6 @@
             longdesc = input(
                 'Enter a long description (leave empty to reuse the short one): ')
         dependencies = {
-            'six': '>= 1.4.0',
             'cubicweb': '>= %s' % cubicwebversion,
         }
         if verbose:
@@ -802,7 +791,7 @@
                     continue
                 try:
                     rql, time = line.split('--')
-                    rql = re.sub("(\'\w+': \d*)", '', rql)
+                    rql = re.sub(r"(\'\w+': \d*)", '', rql)
                     if '{' in rql:
                         rql = rql[:rql.index('{')]
                     req = requests.setdefault(rql, [])
--- a/cubicweb/devtools/fake.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/fake.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 
 from contextlib import contextmanager
 
-from six import string_types
-
 from logilab.database import get_db_helper
 
 from cubicweb.req import RequestSessionBase
@@ -98,7 +96,7 @@
 
     def set_request_header(self, header, value, raw=False):
         """set an incoming HTTP header (for test purpose only)"""
-        if isinstance(value, string_types):
+        if isinstance(value, str):
             value = [value]
         if raw:
             # adding encoded header is important, else page content
--- a/cubicweb/devtools/fill.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/fill.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,9 +17,6 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """This modules defines func / methods for creating test repositories"""
-from __future__ import print_function
-
-
 
 import logging
 from random import randint, choice
@@ -28,9 +25,6 @@
 from decimal import Decimal
 import inspect
 
-from six import text_type, add_metaclass
-from six.moves import range
-
 from logilab.common import attrdict
 from logilab.mtconverter import xml_escape
 from yams.constraints import (SizeConstraint, StaticVocabularyConstraint,
@@ -234,7 +228,7 @@
         """
         for cst in self.eschema.rdef(attrname).constraints:
             if isinstance(cst, StaticVocabularyConstraint):
-                return text_type(choice(cst.vocabulary()))
+                return choice(cst.vocabulary())
         return None
 
     # XXX nothing to do here
@@ -264,14 +258,13 @@
         for attrname, attrvalue in classdict.items():
             if callable(attrvalue):
                 if attrname.startswith('generate_') and \
-                       len(inspect.getargspec(attrvalue).args) < 2:
+                       len(inspect.getfullargspec(attrvalue).args) < 2:
                     raise TypeError('generate_xxx must accept at least 1 argument')
                 setattr(_ValueGenerator, attrname, attrvalue)
         return type.__new__(mcs, name, bases, classdict)
 
 
-@add_metaclass(autoextend)
-class ValueGenerator(_ValueGenerator):
+class ValueGenerator(_ValueGenerator, metaclass=autoextend):
     pass
 
 
@@ -359,7 +352,7 @@
                 fmt = vreg.property_value('ui.float-format')
                 value = fmt % value
             else:
-                value = text_type(value)
+                value = value
     return entity
 
 
--- a/cubicweb/devtools/htmlparser.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/htmlparser.py	Fri Oct 18 23:39:03 2019 +0200
@@ -24,8 +24,6 @@
 
 from lxml import etree
 
-from logilab.common.deprecation import class_deprecated, class_renamed
-
 from cubicweb.view import STRICT_DOCTYPE, TRANSITIONAL_DOCTYPE
 
 STRICT_DOCTYPE = str(STRICT_DOCTYPE)
@@ -131,11 +129,6 @@
         Validator.__init__(self)
         self.parser = etree.XMLParser()
 
-SaxOnlyValidator = class_renamed('SaxOnlyValidator',
-                                 XMLValidator,
-                                 '[3.17] you should use the '
-                                 'XMLValidator class instead')
-
 
 class XMLSyntaxValidator(Validator):
     """XML syntax validator, check XML is well-formed"""
--- a/cubicweb/devtools/httptest.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/httptest.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,21 +18,14 @@
 """this module contains base classes and utilities for integration with running
 http server
 """
-from __future__ import print_function
 
-
-
+import http.client
 import random
 import threading
 import socket
-
-from six import PY3
-from six.moves import range, http_client
-from six.moves.urllib.parse import urlparse
-
+from urllib.parse import urlparse
 
 from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.devtools import ApptestConfiguration
 
 
 def get_available_port(ports_scan):
@@ -54,7 +47,7 @@
     for port in ports_scan:
         try:
             s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-            sock = s.connect(("localhost", port))
+            s.connect(("localhost", port))
         except socket.error as err:
             if err.args[0] in (111, 106):
                 return port
@@ -64,7 +57,7 @@
 
 
 class _CubicWebServerTC(CubicWebTC):
-    """Class for running a Twisted-based test web server.
+    """Base class for running a test web server.
     """
     ports_range = range(7000, 8000)
 
@@ -81,13 +74,13 @@
         If no user is provided, admin connection are used.
         """
         if user is None:
-            user  = self.admlogin
+            user = self.admlogin
             passwd = self.admpassword
         if passwd is None:
             passwd = user
         response = self.web_get("login?__login=%s&__password=%s" %
                                 (user, passwd))
-        assert response.status == http_client.SEE_OTHER, response.status
+        assert response.status == http.client.SEE_OTHER, response.status
         self._ident_cookie = response.getheader('Set-Cookie')
         assert self._ident_cookie
         return True
@@ -95,11 +88,11 @@
     def web_logout(self, user='admin', pwd=None):
         """Log out current http user"""
         if self._ident_cookie is not None:
-            response = self.web_get('logout')
+            self.web_get('logout')
         self._ident_cookie = None
 
     def web_request(self, path='', method='GET', body=None, headers=None):
-        """Return an http_client.HTTPResponse object for the specified path
+        """Return an http.client.HTTPResponse object for the specified path
 
         Use available credential if available.
         """
@@ -110,8 +103,8 @@
             headers['Cookie'] = self._ident_cookie
         self._web_test_cnx.request(method, '/' + path, headers=headers, body=body)
         response = self._web_test_cnx.getresponse()
-        response.body = response.read() # to chain request
-        response.read = lambda : response.body
+        response.body = response.read()  # to chain request
+        response.read = lambda: response.body
         return response
 
     def web_get(self, path='', body=None, headers=None):
@@ -120,7 +113,7 @@
     def setUp(self):
         super(_CubicWebServerTC, self).setUp()
         port = self.config['port'] or get_available_port(self.ports_range)
-        self.config.global_set_option('port', port) # force rewrite here
+        self.config.global_set_option('port', port)  # force rewrite here
         self.config.global_set_option('base-url', 'http://127.0.0.1:%d/' % port)
         # call load_configuration again to let the config reset its datadir_url
         self.config.load_configuration()
@@ -133,55 +126,9 @@
 
 class CubicWebServerTC(_CubicWebServerTC):
     def start_server(self):
-        if PY3:
-            self.skipTest('not using twisted on python3')
-        from twisted.internet import reactor
-        from cubicweb.etwist.server import run
-        # use a semaphore to avoid starting test while the http server isn't
-        # fully initilialized
-        semaphore = threading.Semaphore(0)
-        def safe_run(*args, **kwargs):
-            try:
-                run(*args, **kwargs)
-            finally:
-                semaphore.release()
-
-        reactor.addSystemEventTrigger('after', 'startup', semaphore.release)
-        t = threading.Thread(target=safe_run, name='cubicweb_test_web_server',
-                args=(self.config, True), kwargs={'repo': self.repo})
-        self.web_thread = t
-        t.start()
-        semaphore.acquire()
-        if not self.web_thread.isAlive():
-            # XXX race condition with actual thread death
-            raise RuntimeError('Could not start the web server')
-        #pre init utils connection
-        parseurl = urlparse(self.config['base-url'])
-        assert parseurl.port == self.config['port'], (self.config['base-url'], self.config['port'])
-        self._web_test_cnx = http_client.HTTPConnection(parseurl.hostname,
-                                                        parseurl.port)
-        self._ident_cookie = None
-
-    def stop_server(self, timeout=15):
-        """Stop the webserver, waiting for the thread to return"""
-        from twisted.internet import reactor
-        if self._web_test_cnx is None:
-            self.web_logout()
-            self._web_test_cnx.close()
-        try:
-            reactor.stop()
-            self.web_thread.join(timeout)
-            assert not self.web_thread.isAlive()
-
-        finally:
-            reactor.__init__()
-
-
-class CubicWebWsgiTC(CubicWebServerTC):
-    def start_server(self):
         from cubicweb.wsgi.handler import CubicWebWSGIApplication
         from wsgiref import simple_server
-        from six.moves import queue
+        import queue
 
         config = self.config
         port = config['port'] or 8080
@@ -214,7 +161,7 @@
             self.fail(start_flag.get())
         parseurl = urlparse(self.config['base-url'])
         assert parseurl.port == self.config['port'], (self.config['base-url'], self.config['port'])
-        self._web_test_cnx = http_client.HTTPConnection(parseurl.hostname,
+        self._web_test_cnx = http.client.HTTPConnection(parseurl.hostname,
                                                         parseurl.port)
         self._ident_cookie = None
 
--- a/cubicweb/devtools/instrument.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/instrument.py	Fri Oct 18 23:39:03 2019 +0200
@@ -14,8 +14,6 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with this program. If not, see <http://www.gnu.org/licenses/>.
 """Instrumentation utilities"""
-from __future__ import print_function
-
 import os
 
 try:
--- a/cubicweb/devtools/qunit.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/qunit.py	Fri Oct 18 23:39:03 2019 +0200
@@ -15,16 +15,13 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-from __future__ import absolute_import, print_function
-
 import os, os.path as osp
 import errno
 import shutil
-from tempfile import mkdtemp
+from queue import Queue, Empty
+from tempfile import mkdtemp, TemporaryDirectory
 from subprocess import Popen, PIPE, STDOUT
 
-from six.moves.queue import Queue, Empty
-
 # imported by default to simplify further import statements
 from logilab.common.testlib import Tags
 import webtest.http
@@ -34,7 +31,6 @@
 from cubicweb.web.controller import Controller
 from cubicweb.web.views.staticcontrollers import StaticFileController, STATIC_CONTROLLERS
 from cubicweb.devtools import webtest as cwwebtest
-from cubicweb.devtools.testlib import TemporaryDirectory
 
 
 class FirefoxHelper(object):
--- a/cubicweb/devtools/repotest.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/repotest.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,8 +19,6 @@
 
 This module contains functions to initialize a new repository.
 """
-from __future__ import print_function
-
 from contextlib import contextmanager
 from pprint import pprint
 
--- a/cubicweb/devtools/stresstester.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/stresstester.py	Fri Oct 18 23:39:03 2019 +0200
@@ -41,8 +41,6 @@
 Copyright (c) 2003-2011 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
 http://www.logilab.fr/ -- mailto:contact@logilab.fr
 """
-from __future__ import print_function
-
 import os
 import sys
 import threading
--- a/cubicweb/devtools/test/data/cubes/i18ntestcube/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-# pylint: disable=W0622
-"""cubicweb i18n test cube application packaging information"""
-
-modname = 'i18ntestcube'
-distname = 'cubicweb-i18ntestcube'
-
-numversion = (0, 1, 0)
-version = '.'.join(str(num) for num in numversion)
-
-license = 'LGPL'
-author = 'LOGILAB S.A. (Paris, FRANCE)'
-author_email = 'contact@logilab.fr'
-description = 'forum'
-web = 'http://www.cubicweb.org/project/%s' % distname
-
-__depends__ =  {'cubicweb': '>= 3.16.4',
-               }
-__recommends__ = {}
--- a/cubicweb/devtools/test/data/cubes/i18ntestcube/excludeme/somefile.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,4 +0,0 @@
-from cubicweb import _
-
-_('ignore-me')
-
--- a/cubicweb/devtools/test/data/cubes/i18ntestcube/i18n/en.po.ref	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,185 +0,0 @@
-msgid ""
-msgstr ""
-"Project-Id-Version: cubicweb 3.16.5\n"
-"PO-Revision-Date: 2008-03-28 18:14+0100\n"
-"Last-Translator: Logilab Team <contact@logilab.fr>\n"
-"Language-Team: fr <contact@logilab.fr>\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Generated-By: cubicweb-devtools\n"
-"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-
-# schema pot file, generated on 2013-07-12 16:18:12
-#
-# singular and plural forms for each entity type
-# subject and object forms for each relation type
-# (no object form for final or symmetric relation types)
-msgid "Forum"
-msgstr ""
-
-msgid "Forum_plural"
-msgstr ""
-
-msgid "This Forum"
-msgstr ""
-
-msgid "This Forum:"
-msgstr ""
-
-msgid "New Forum"
-msgstr ""
-
-msgctxt "inlined:Forum.in_forum.object"
-msgid "add a ForumThread"
-msgstr ""
-
-msgctxt "inlined:Forum.in_forum.object"
-msgid "ForumThread"
-msgstr ""
-
-msgid "add ForumThread in_forum Forum object"
-msgstr ""
-
-msgid "add a Forum"
-msgstr ""
-
-msgid "add a ForumThread"
-msgstr ""
-
-msgid "creating ForumThread (ForumThread in_forum Forum %(linkto)s)"
-msgstr ""
-
-msgid "ForumThread"
-msgstr ""
-
-msgid "ForumThread_plural"
-msgstr ""
-
-msgid "This ForumThread"
-msgstr ""
-
-msgid "This ForumThread:"
-msgstr ""
-
-msgid "New ForumThread"
-msgstr ""
-
-msgid "content"
-msgstr ""
-
-msgctxt "ForumThread"
-msgid "content"
-msgstr ""
-
-msgid "content_format"
-msgstr ""
-
-msgctxt "ForumThread"
-msgid "content_format"
-msgstr ""
-
-msgctxt "Forum"
-msgid "description"
-msgstr ""
-
-msgctxt "Forum"
-msgid "description_format"
-msgstr ""
-
-msgid "ignore-me"
-msgstr ""
-
-msgid "in_forum"
-msgstr ""
-
-msgctxt "ForumThread"
-msgid "in_forum"
-msgstr ""
-
-msgctxt "Forum"
-msgid "in_forum_object"
-msgstr ""
-
-msgid "in_forum_object"
-msgstr ""
-
-msgid "interested_in"
-msgstr ""
-
-msgctxt "CWUser"
-msgid "interested_in"
-msgstr ""
-
-msgctxt "ForumThread"
-msgid "interested_in_object"
-msgstr ""
-
-msgctxt "Forum"
-msgid "interested_in_object"
-msgstr ""
-
-msgid "interested_in_object"
-msgstr ""
-
-msgid "nosy_list"
-msgstr ""
-
-msgctxt "ForumThread"
-msgid "nosy_list"
-msgstr ""
-
-msgctxt "Forum"
-msgid "nosy_list"
-msgstr ""
-
-msgctxt "CWUser"
-msgid "nosy_list_object"
-msgstr ""
-
-msgid "nosy_list_object"
-msgstr ""
-
-msgctxt "ForumThread"
-msgid "title"
-msgstr ""
-
-msgid "topic"
-msgstr ""
-
-msgctxt "Forum"
-msgid "topic"
-msgstr ""
-
-msgid "Topic"
-msgstr ""
-
-msgid "Description"
-msgstr ""
-
-msgid "Number of threads"
-msgstr ""
-
-msgid "Last activity"
-msgstr ""
-
-msgid ""
-"a long\n"
-"tranlated line\n"
-"hop."
-msgstr ""
-
-msgid "Subject"
-msgstr ""
-
-msgid "Created"
-msgstr ""
-
-msgid "Answers"
-msgstr ""
-
-msgid "Last answered"
-msgstr ""
-
-msgid "This forum does not have any thread yet."
-msgstr ""
--- a/cubicweb/devtools/test/data/cubes/i18ntestcube/node_modules/cubes.somefile.js	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-_("hello");
-
--- a/cubicweb/devtools/test/data/cubes/i18ntestcube/schema.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,45 +0,0 @@
-# -*- coding: utf-8 -*-
-# copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr -- mailto:contact@logilab.fr
-#
-# This program is free software: you can redistribute it and/or modify it under
-# the terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with this program. If not, see <http://www.gnu.org/licenses/>.
-
-"""cubicweb-forum schema"""
-
-from yams.buildobjs import (String, RichString, EntityType,
-                            RelationDefinition, SubjectRelation)
-from yams.reader import context
-
-class Forum(EntityType):
-    topic = String(maxsize=50, required=True, unique=True)
-    description = RichString()
-
-class ForumThread(EntityType):
-    __permissions__ = {
-        'read': ('managers', 'users'),
-        'add': ('managers', 'users'),
-        'update': ('managers', 'owners'),
-        'delete': ('managers', 'owners')
-        }
-    title = String(required=True, fulltextindexed=True, maxsize=256)
-    content = RichString(required=True, fulltextindexed=True)
-    in_forum = SubjectRelation('Forum', cardinality='1*', inlined=True,
-                               composite='object')
-class interested_in(RelationDefinition):
-    subject = 'CWUser'
-    object = ('ForumThread', 'Forum')
-
-class nosy_list(RelationDefinition):
-    subject = ('Forum', 'ForumThread')
-    object = 'CWUser'
--- a/cubicweb/devtools/test/data/cubes/i18ntestcube/views.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-# copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr -- mailto:contact@logilab.fr
-#
-# This program is free software: you can redistribute it and/or modify it under
-# the terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# This program is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with this program. If not, see <http://www.gnu.org/licenses/>.
-
-"""cubicweb-forum views/forms/actions/components for web ui"""
-
-from cubicweb import view
-from cubicweb.predicates import is_instance
-from cubicweb.web.views import primary, baseviews, uicfg
-from cubicweb.web.views.uicfg import autoform_section as afs
-
-
-class MyAFS(uicfg.AutoformSectionRelationTags):
-    __select__ = is_instance('ForumThread')
-
-
-_myafs = MyAFS(__module__=__name__)
-
-_myafs.tag_object_of(('*', 'in_forum', 'Forum'), 'main', 'inlined')
-
-afs.tag_object_of(('*', 'in_forum', 'Forum'), 'main', 'inlined')
-
-
-class ForumSameETypeListView(baseviews.SameETypeListView):
-    __select__ = baseviews.SameETypeListView.__select__ & is_instance('Forum')
-
-    def call(self, **kwargs):
-        _ = self._cw._
-        _('Topic'), _('Description')
-        _('Number of threads'), _('Last activity')
-        _('''a long
-tranlated line
-hop.''')
-
-
-class ForumLastActivity(view.EntityView):
-    __regid__ = 'forum_last_activity'
-    __select__ = view.EntityView.__select__ & is_instance('Forum')
-
-
-class ForumPrimaryView(primary.PrimaryView):
-    __select__ = primary.PrimaryView.__select__ & is_instance('Forum')
-
-    def render_entity_attributes(self, entity):
-        _ = self._cw._
-        _('Subject'), _('Created'), _('Answers'),
-        _('Last answered')
-        _('This forum does not have any thread yet.')
-
-
-class ForumThreadPrimaryView(primary.PrimaryView):
-    __select__ = primary.PrimaryView.__select__ & is_instance('ForumThread')
--- a/cubicweb/devtools/test/unittest_dbfill.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/test/unittest_dbfill.py	Fri Oct 18 23:39:03 2019 +0200
@@ -23,8 +23,6 @@
 import datetime
 import io
 
-from six.moves import range
-
 from logilab.common.testlib import TestCase, unittest_main
 
 from cubicweb.devtools.fill import ValueGenerator, make_tel
--- a/cubicweb/devtools/test/unittest_devctl.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/test/unittest_devctl.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,10 +21,9 @@
 import os.path as osp
 import sys
 from subprocess import Popen, PIPE, STDOUT
+from tempfile import TemporaryDirectory
 from unittest import TestCase
 
-from cubicweb.devtools.testlib import TemporaryDirectory
-
 
 def newcube(directory, name):
     cmd = ['cubicweb-ctl', 'newcube', '--directory', directory,
--- a/cubicweb/devtools/test/unittest_httptest.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/test/unittest_httptest.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,90 +17,50 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """unittest for cubicweb.devtools.httptest module"""
 
-from six.moves import http_client
+import http.client
 
 from logilab.common.testlib import Tags
-from cubicweb.devtools.httptest import CubicWebServerTC, CubicWebWsgiTC
+from cubicweb.devtools.httptest import CubicWebServerTC
 
 
-class TwistedCWAnonTC(CubicWebServerTC):
+class WsgiCWAnonTC(CubicWebServerTC):
 
     def test_response(self):
         try:
             response = self.web_get()
-        except http_client.NotConnected as ex:
+        except http.client.NotConnected as ex:
             self.fail("Can't connection to test server: %s" % ex)
 
     def test_response_anon(self):
         response = self.web_get()
-        self.assertEqual(response.status, http_client.OK)
+        self.assertEqual(response.status, http.client.OK)
 
     def test_base_url(self):
         if self.config['base-url'] not in self.web_get().read().decode('ascii'):
             self.fail('no mention of base url in retrieved page')
 
 
-class TwistedCWIdentTC(CubicWebServerTC):
+class WsgiCWIdentTC(CubicWebServerTC):
     test_db_id = 'httptest-cwident'
     anonymous_allowed = False
     tags = CubicWebServerTC.tags | Tags(('auth',))
 
     def test_response_denied(self):
         response = self.web_get()
-        self.assertEqual(response.status, http_client.FORBIDDEN)
+        self.assertEqual(response.status, http.client.FORBIDDEN)
 
     def test_login(self):
         response = self.web_get()
-        if response.status != http_client.FORBIDDEN:
+        if response.status != http.client.FORBIDDEN:
             self.skipTest('Already authenticated, "test_response_denied" must have failed')
         # login
         self.web_login(self.admlogin, self.admpassword)
         response = self.web_get()
-        self.assertEqual(response.status, http_client.OK, response.body)
+        self.assertEqual(response.status, http.client.OK, response.body)
         # logout
         self.web_logout()
         response = self.web_get()
-        self.assertEqual(response.status, http_client.FORBIDDEN, response.body)
-
-
-class WsgiCWAnonTC(CubicWebWsgiTC):
-
-    def test_response(self):
-        try:
-            response = self.web_get()
-        except http_client.NotConnected as ex:
-            self.fail("Can't connection to test server: %s" % ex)
-
-    def test_response_anon(self):
-        response = self.web_get()
-        self.assertEqual(response.status, http_client.OK)
-
-    def test_base_url(self):
-        if self.config['base-url'] not in self.web_get().read().decode('ascii'):
-            self.fail('no mention of base url in retrieved page')
-
-
-class WsgiCWIdentTC(CubicWebWsgiTC):
-    test_db_id = 'httptest-cwident'
-    anonymous_allowed = False
-    tags = CubicWebServerTC.tags | Tags(('auth',))
-
-    def test_response_denied(self):
-        response = self.web_get()
-        self.assertEqual(response.status, http_client.FORBIDDEN)
-
-    def test_login(self):
-        response = self.web_get()
-        if response.status != http_client.FORBIDDEN:
-            self.skipTest('Already authenticated, "test_response_denied" must have failed')
-        # login
-        self.web_login(self.admlogin, self.admpassword)
-        response = self.web_get()
-        self.assertEqual(response.status, http_client.OK, response.body)
-        # logout
-        self.web_logout()
-        response = self.web_get()
-        self.assertEqual(response.status, http_client.FORBIDDEN, response.body)
+        self.assertEqual(response.status, http.client.FORBIDDEN, response.body)
 
 
 if __name__ == '__main__':
--- a/cubicweb/devtools/test/unittest_i18n.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/test/unittest_i18n.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,15 +19,13 @@
 """unit tests for i18n messages generator"""
 
 from contextlib import contextmanager
-from io import StringIO, BytesIO
+from io import StringIO
 import os
 import os.path as osp
 import sys
 from subprocess import PIPE, Popen, STDOUT
 from unittest import TestCase, main
-
-from six import PY2
-from mock import patch
+from unittest.mock import patch
 
 from cubicweb.devtools import devctl
 from cubicweb.devtools.testlib import BaseTestCase
@@ -58,7 +56,7 @@
     return msgs
 
 
-TESTCUBE_DIR = osp.join(DATADIR, 'cubes', 'i18ntestcube')
+TESTCUBE_DIR = osp.join(DATADIR, 'libpython', 'cubicweb_i18ntestcube')
 
 
 class cubePotGeneratorTC(TestCase):
@@ -74,17 +72,6 @@
         cubedir = osp.join(DATADIR, 'libpython', 'cubicweb_i18ntestcube')
         self._check(cubedir, env)
 
-    def test_i18ncube_legacy_layout(self):
-        env = os.environ.copy()
-        env['CW_CUBES_PATH'] = osp.join(DATADIR, 'cubes')
-        if 'PYTHONPATH' in env:
-            env['PYTHONPATH'] += os.pathsep
-        else:
-            env['PYTHONPATH'] = ''
-        env['PYTHONPATH'] += DATADIR
-        cubedir = osp.join(DATADIR, 'cubes', 'i18ntestcube')
-        self._check(cubedir, env)
-
     def _check(self, cubedir, env):
         cmd = [sys.executable, '-m', 'cubicweb', 'i18ncube', 'i18ntestcube']
         proc = Popen(cmd, env=env, stdout=PIPE, stderr=STDOUT)
@@ -102,7 +89,7 @@
 
 @contextmanager
 def capture_stdout():
-    stream = BytesIO() if PY2 else StringIO()
+    stream = StringIO()
     sys.stdout = stream
     yield stream
     stream.seek(0)
@@ -137,19 +124,14 @@
     @patch('pkg_resources.load_entry_point', return_value=FakeMessageExtractor)
     def test_cube_custom_extractor(self, mock_load_entry_point):
         distname = 'cubicweb_i18ntestcube'  # same for new and legacy layout
-        for cubedir in [
-            osp.join(DATADIR, 'libpython', 'cubicweb_i18ntestcube'),
-            # Legacy cubes.
-            osp.join(DATADIR, 'cubes', 'i18ntestcube'),
-        ]:
-            with self.subTest(cubedir=cubedir):
-                with capture_stdout() as stream:
-                    devctl.update_cube_catalogs(cubedir)
-                self.assertIn(u'no message catalog for cube i18ntestcube',
-                              stream.read())
-                mock_load_entry_point.assert_called_once_with(
-                    distname, 'cubicweb.i18ncube', 'i18ntestcube')
-                mock_load_entry_point.reset_mock()
+        cubedir = osp.join(DATADIR, 'libpython', 'cubicweb_i18ntestcube')
+        with capture_stdout() as stream:
+            devctl.update_cube_catalogs(cubedir)
+        self.assertIn(u'no message catalog for cube i18ntestcube',
+                      stream.read())
+        mock_load_entry_point.assert_called_once_with(
+            distname, 'cubicweb.i18ncube', 'i18ntestcube')
+        mock_load_entry_point.reset_mock()
 
 
 if __name__ == '__main__':
--- a/cubicweb/devtools/test/unittest_testlib.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/test/unittest_testlib.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 from io import BytesIO, StringIO
 from unittest import TextTestRunner
 
-from six import PY2
-
 from logilab.common.testlib import TestSuite, TestCase, unittest_main
 from logilab.common.registry import yes
 
@@ -52,7 +50,7 @@
 class WebTestTC(TestCase):
 
     def setUp(self):
-        output = BytesIO() if PY2 else StringIO()
+        output = StringIO()
         self.runner = TextTestRunner(stream=output)
 
     def test_error_raised(self):
--- a/cubicweb/devtools/test/unittest_webtest.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/test/unittest_webtest.py	Fri Oct 18 23:39:03 2019 +0200
@@ -1,4 +1,4 @@
-from six.moves import http_client
+import http.client
 
 from logilab.common.testlib import Tags
 from cubicweb.devtools.webtest import CubicWebTestTC
@@ -21,19 +21,19 @@
 
     def test_reponse_denied(self):
         res = self.webapp.get('/', expect_errors=True)
-        self.assertEqual(http_client.FORBIDDEN, res.status_int)
+        self.assertEqual(http.client.FORBIDDEN, res.status_int)
 
     def test_login(self):
         res = self.webapp.get('/', expect_errors=True)
-        self.assertEqual(http_client.FORBIDDEN, res.status_int)
+        self.assertEqual(http.client.FORBIDDEN, res.status_int)
 
         self.login(self.admlogin, self.admpassword)
         res = self.webapp.get('/')
-        self.assertEqual(http_client.OK, res.status_int)
+        self.assertEqual(http.client.OK, res.status_int)
 
         self.logout()
         res = self.webapp.get('/', expect_errors=True)
-        self.assertEqual(http_client.FORBIDDEN, res.status_int)
+        self.assertEqual(http.client.FORBIDDEN, res.status_int)
 
 
 if __name__ == '__main__':
--- a/cubicweb/devtools/testlib.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/devtools/testlib.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,21 +17,15 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Base classes and utilities for cubicweb tests"""
 
-from __future__ import print_function
-
 import sys
 import re
-import warnings
 from os.path import dirname, join, abspath
 from math import log
 from contextlib import contextmanager
 from inspect import isgeneratorfunction
 from itertools import chain
-from warnings import warn
-
-from six import binary_type, text_type, string_types, reraise
-from six.moves import range
-from six.moves.urllib.parse import urlparse, parse_qs, unquote as urlunquote
+from unittest import TestCase
+from urllib.parse import urlparse, parse_qs, unquote as urlunquote
 
 import yams.schema
 
@@ -39,7 +33,7 @@
 from logilab.common.debugger import Debugger
 from logilab.common.umessage import message_from_string
 from logilab.common.decorators import cached, classproperty, clear_cache, iclassmethod
-from logilab.common.deprecation import deprecated, class_deprecated
+from logilab.common.deprecation import class_deprecated
 from logilab.common.shellutils import getlogin
 
 from cubicweb import (ValidationError, NoSelectableObject, AuthenticationError,
@@ -54,22 +48,6 @@
 from cubicweb.devtools.fill import insert_entity_queries, make_relations_queries
 from cubicweb.web.views.authentication import Session
 
-if sys.version_info[:2] < (3, 4):
-    from unittest2 import TestCase
-    if not hasattr(TestCase, 'subTest'):
-        raise ImportError('no subTest support in available unittest2')
-    try:
-        from backports.tempfile import TemporaryDirectory  # noqa
-    except ImportError:
-        # backports.tempfile not available
-        TemporaryDirectory = None
-else:
-    from unittest import TestCase
-    from tempfile import TemporaryDirectory  # noqa
-
-# in python 2.7, DeprecationWarning are not shown anymore by default
-warnings.filterwarnings('default', category=DeprecationWarning)
-
 
 # provide a data directory for the test class ##################################
 
@@ -327,7 +305,6 @@
         """provide a new RepoAccess object for a given user
 
         The access is automatically closed at the end of the test."""
-        login = text_type(login)
         access = RepoAccess(self.repo, login, self.requestcls)
         self._open_access.add(access)
         return access
@@ -348,7 +325,7 @@
         db_handler.restore_database(self.test_db_id)
         self.repo = db_handler.get_repo(startup=True)
         # get an admin session (without actual login)
-        login = text_type(db_handler.config.default_admin_config['login'])
+        login = db_handler.config.default_admin_config['login']
         self.admin_access = self.new_access(login)
 
     # config management ########################################################
@@ -366,7 +343,7 @@
         been properly bootstrapped.
         """
         admincfg = config.default_admin_config
-        cls.admlogin = text_type(admincfg['login'])
+        cls.admlogin = admincfg['login']
         cls.admpassword = admincfg['password']
         # uncomment the line below if you want rql queries to be logged
         # config.global_set_option('query-log-file',
@@ -453,29 +430,19 @@
 
     # user / session management ###############################################
 
-    @deprecated('[3.19] explicitly use RepoAccess object in test instead')
-    def user(self, req=None):
-        """return the application schema"""
-        if req is None:
-            return self.request().user
-        else:
-            return req.user
-
     @iclassmethod  # XXX turn into a class method
     def create_user(self, req, login=None, groups=('users',), password=None,
                     email=None, commit=True, **kwargs):
         """create and return a new user entity"""
         if password is None:
             password = login
-        if login is not None:
-            login = text_type(login)
         user = req.create_entity('CWUser', login=login,
                                  upassword=password, **kwargs)
         req.execute('SET X in_group G WHERE X eid %%(x)s, G name IN(%s)'
                     % ','.join(repr(str(g)) for g in groups),
                     {'x': user.eid})
         if email is not None:
-            req.create_entity('EmailAddress', address=text_type(email),
+            req.create_entity('EmailAddress', address=email,
                               reverse_primary_email=user)
         user.cw_clear_relation_cache('in_group', 'subject')
         if commit:
@@ -530,7 +497,7 @@
         """
         torestore = []
         for erschema, etypeperms in chain(perm_overrides, perm_kwoverrides.items()):
-            if isinstance(erschema, string_types):
+            if isinstance(erschema, str):
                 erschema = self.schema[erschema]
             for action, actionperms in etypeperms.items():
                 origperms = erschema.permissions[action]
@@ -668,15 +635,6 @@
         publisher.error_handler = raise_error_handler
         return publisher
 
-    @deprecated('[3.19] use the .remote_calling method')
-    def remote_call(self, fname, *args):
-        """remote json call simulation"""
-        dump = json.dumps
-        args = [dump(arg) for arg in args]
-        req = self.request(fname=fname, pageid='123', arg=args)
-        ctrl = self.vreg['controllers'].select('ajax', req)
-        return ctrl.publish(), req
-
     @contextmanager
     def remote_calling(self, fname, *args, **kwargs):
         """remote json call simulation"""
@@ -685,19 +643,9 @@
             ctrl = self.vreg['controllers'].select('ajax', req)
             yield ctrl.publish(), req
 
-    def app_handle_request(self, req, path=None):
-        if path is not None:
-            warn('[3.24] path argument got removed from app_handle_request parameters, '
-                 'give it to the request constructor', DeprecationWarning)
-            if req.relative_path(False) != path:
-                req._url = path
+    def app_handle_request(self, req):
         return self.app.core_handle(req)
 
-    @deprecated("[3.15] app_handle_request is the new and better way"
-                " (beware of small semantic changes)")
-    def app_publish(self, *args, **kwargs):
-        return self.app_handle_request(*args, **kwargs)
-
     def ctrl_publish(self, req, ctrl='edit', rset=None):
         """call the publish method of the edit controller"""
         ctrl = self.vreg['controllers'].select(ctrl, req, appli=self.app)
@@ -748,20 +696,6 @@
             form['_cw_fields'] = ','.join(sorted(fields))
         return form
 
-    @deprecated('[3.19] use .admin_request_from_url instead')
-    def req_from_url(self, url):
-        """parses `url` and builds the corresponding CW-web request
-
-        req.form will be setup using the url's query string
-        """
-        req = self.request(url=url)
-        if isinstance(url, text_type):
-            url = url.encode(req.encoding)  # req.setup_params() expects encoded strings
-        querystring = urlparse(url)[-2]
-        params = parse_qs(querystring)
-        req.setup_params(params)
-        return req
-
     @contextmanager
     def admin_request_from_url(self, url):
         """parses `url` and builds the corresponding CW-web request
@@ -769,7 +703,7 @@
         req.form will be setup using the url's query string
         """
         with self.admin_access.web_request(url=url) as req:
-            if isinstance(url, text_type):
+            if isinstance(url, str):
                 url = url.encode(req.encoding)  # req.setup_params() expects encoded strings
             querystring = urlparse(url)[-2]
             params = parse_qs(querystring)
@@ -841,11 +775,6 @@
         location = req.get_response_header('location')
         return self._parse_location(req, location)
 
-    @deprecated("[3.15] expect_redirect_handle_request is the new and better way"
-                " (beware of small semantic changes)")
-    def expect_redirect_publish(self, *args, **kwargs):
-        return self.expect_redirect_handle_request(*args, **kwargs)
-
     def set_auth_mode(self, authmode, anonuser=None):
         self.set_option('auth-mode', authmode)
         self.set_option('anonymous-user', anonuser)
@@ -955,7 +884,7 @@
                 msg = '[%s in %s] %s' % (klass, view.__regid__, exc)
             except Exception:
                 msg = '[%s in %s] undisplayable exception' % (klass, view.__regid__)
-            reraise(AssertionError, AssertionError(msg), sys.exc_info()[-1])
+            raise AssertionError(msg).with_traceback(sys.exc_info()[-1])
         return self._check_html(output, view, template)
 
     def get_validator(self, view=None, content_type=None, output=None):
@@ -988,7 +917,7 @@
     def _check_html(self, output, view, template='main-template'):
         """raises an exception if the HTML is invalid"""
         output = output.strip()
-        if isinstance(output, text_type):
+        if isinstance(output, str):
             # XXX
             output = output.encode('utf-8')
         validator = self.get_validator(view, output=output)
@@ -1021,8 +950,8 @@
                 position = getattr(exc, "position", (0,))[0]
                 if position:
                     # define filter
-                    if isinstance(content, binary_type):
-                        content = text_type(content, sys.getdefaultencoding(), 'replace')
+                    if isinstance(content, bytes):
+                        content = str(content, sys.getdefaultencoding(), 'replace')
                     content = validator.preprocess_data(content)
                     content = content.splitlines()
                     width = int(log(len(content), 10)) + 1
--- a/cubicweb/entities/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/entities/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,14 +17,7 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """base application's entities class implementation: `AnyEntity`"""
 
-
-
-from warnings import warn
-
-from six import text_type, string_types
-
 from logilab.common.decorators import classproperty
-from logilab.common.deprecation import deprecated
 
 from cubicweb import Unauthorized
 from cubicweb.entity import Entity
@@ -39,7 +32,7 @@
     @classproperty
     def cw_etype(cls):
         """entity type as a unicode string"""
-        return text_type(cls.__regid__)
+        return cls.__regid__
 
     @classmethod
     def cw_create_url(cls, req, **kwargs):
@@ -47,36 +40,11 @@
         return req.build_url('add/%s' % cls.__regid__, **kwargs)
 
     @classmethod
-    @deprecated('[3.22] use cw_fti_index_rql_limit instead')
-    def cw_fti_index_rql_queries(cls, req):
-        """return the list of rql queries to fetch entities to FT-index
-
-        The default is to fetch all entities at once and to prefetch
-        indexable attributes but one could imagine iterating over
-        "smaller" resultsets if the table is very big or returning
-        a subset of entities that match some business-logic condition.
-        """
-        restrictions = ['X is %s' % cls.__regid__]
-        selected = ['X']
-        for attrschema in sorted(cls.e_schema.indexable_attributes()):
-            varname = attrschema.type.upper()
-            restrictions.append('X %s %s' % (attrschema, varname))
-            selected.append(varname)
-        return ['Any %s WHERE %s' % (', '.join(selected),
-                                     ', '.join(restrictions))]
-
-    @classmethod
     def cw_fti_index_rql_limit(cls, req, limit=1000):
         """generate rsets of entities to FT-index
 
         By default, each successive result set is limited to 1000 entities
         """
-        if cls.cw_fti_index_rql_queries.__func__ != AnyEntity.cw_fti_index_rql_queries.__func__:
-            warn("[3.22] cw_fti_index_rql_queries is replaced by cw_fti_index_rql_limit",
-                 DeprecationWarning)
-            for rql in cls.cw_fti_index_rql_queries(req):
-                yield req.execute(rql)
-            return
         restrictions = ['X is %s' % cls.__regid__]
         selected = ['X']
         start = 0
@@ -141,8 +109,8 @@
         if rtype is None:
             return self.dc_title().lower()
         value = self.cw_attr_value(rtype)
-        # do not restrict to `unicode` because Bytes will return a `str` value
-        if isinstance(value, string_types):
+        # do not restrict to `str` because Bytes will return a `str` value
+        if isinstance(value, str):
             return self.printable_value(rtype, format='text/plain').lower()
         return value
 
--- a/cubicweb/entities/adapters.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/entities/adapters.py	Fri Oct 18 23:39:03 2019 +0200
@@ -27,6 +27,7 @@
 
 from cubicweb import (Unauthorized, ValidationError, view, ViolatedConstraint,
                       UniqueTogetherError)
+from cubicweb.schema import constraint_name_for
 from cubicweb.predicates import is_instance, relation_possible, match_exception
 
 
@@ -474,7 +475,7 @@
         for rschema, attrschema in eschema.attribute_definitions():
             rdef = rschema.rdef(eschema, attrschema)
             for constraint in rdef.constraints:
-                if cstrname == constraint.name_for(rdef):
+                if cstrname == constraint_name_for(constraint, rdef):
                     break
             else:
                 continue
--- a/cubicweb/entities/authobjs.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/entities/authobjs.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,8 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """entity classes user and group entities"""
 
-from six import string_types, text_type
-
 from logilab.common.decorators import cached
 
 from cubicweb import Unauthorized
@@ -110,13 +108,12 @@
         return self._cw.vreg.property_value(key)
 
     def set_property(self, pkey, value):
-        value = text_type(value)
         try:
             prop = self._cw.execute(
                 'CWProperty X WHERE X pkey %(k)s, X for_user U, U eid %(u)s',
                 {'k': pkey, 'u': self.eid}).get_entity(0, 0)
         except Exception:
-            kwargs = dict(pkey=text_type(pkey), value=value)
+            kwargs = dict(pkey=pkey, value=value)
             if self.is_in_group('managers'):
                 kwargs['for_user'] = self
             self._cw.create_entity('CWProperty', **kwargs)
@@ -129,7 +126,7 @@
         :type groups: str or iterable(str)
         :param groups: a group name or an iterable on group names
         """
-        if isinstance(groups, string_types):
+        if isinstance(groups, str):
             groups = frozenset((groups,))
         elif isinstance(groups, (tuple, list)):
             groups = frozenset(groups)
--- a/cubicweb/entities/lib.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/entities/lib.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,9 +20,7 @@
 
 from warnings import warn
 from datetime import datetime
-
-from six.moves import range
-from six.moves.urllib.parse import urlsplit, urlunsplit
+from urllib.parse import urlsplit, urlunsplit
 
 from logilab.mtconverter import xml_escape
 
@@ -125,25 +123,3 @@
             return self._cw._(self._cw.vreg.property_info(self.pkey)['help'])
         except UnknownProperty:
             return u''
-
-
-class CWCache(AnyEntity):
-    """Cache"""
-    __regid__ = 'CWCache'
-    fetch_attrs, cw_fetch_order = fetch_config(['name'])
-
-    def __init__(self, *args, **kwargs):
-        warn('[3.19] CWCache entity type is going away soon. '
-             'Other caching mechanisms can be used more reliably '
-             'to the same effect.',
-             DeprecationWarning)
-        super(CWCache, self).__init__(*args, **kwargs)
-
-    def touch(self):
-        self._cw.execute('SET X timestamp %(t)s WHERE X eid %(x)s',
-                         {'t': datetime.now(), 'x': self.eid})
-
-    def valid(self, date):
-        if date:
-            return date > self.timestamp
-        return False
--- a/cubicweb/entities/sources.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/entities/sources.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,8 +21,6 @@
 from socket import gethostname
 import logging
 
-from six import text_type
-
 from logilab.common.textutils import text_to_dict
 from logilab.common.configuration import OptionError
 from logilab.mtconverter import xml_escape
--- a/cubicweb/entities/test/unittest_base.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/entities/test/unittest_base.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,7 +21,6 @@
 
 from logilab.common.testlib import unittest_main
 from logilab.common.decorators import clear_cache
-from logilab.common.registry import yes
 
 from cubicweb.devtools.testlib import CubicWebTC
 
@@ -65,33 +64,13 @@
                          {'description_format': ('format', 'description')})
 
     def test_fti_rql_method(self):
-        class EmailAddress(AnyEntity):
-            __regid__ = 'EmailAddress'
-            __select__ = AnyEntity.__select__ & yes(2)
-
-            @classmethod
-            def cw_fti_index_rql_queries(cls, req):
-                return ['EmailAddress Y']
-
         with self.admin_access.web_request() as req:
             req.create_entity('EmailAddress', address=u'foo@bar.com')
             eclass = self.vreg['etypes'].etype_class('EmailAddress')
-            # deprecated
-            self.assertEqual(['Any X, ADDRESS, ALIAS WHERE X is EmailAddress, '
-                              'X address ADDRESS, X alias ALIAS'],
-                             eclass.cw_fti_index_rql_queries(req))
-
             self.assertEqual(['Any X, ADDRESS, ALIAS ORDERBY X LIMIT 1000 WHERE X is EmailAddress, '
                               'X address ADDRESS, X alias ALIAS, X eid > 0'],
                              [rset.rql for rset in eclass.cw_fti_index_rql_limit(req)])
 
-            # test backwards compatibility with custom method
-            with self.temporary_appobjects(EmailAddress):
-                self.vreg['etypes'].clear_caches()
-                eclass = self.vreg['etypes'].etype_class('EmailAddress')
-                self.assertEqual(['EmailAddress Y'],
-                                 [rset.rql for rset in eclass.cw_fti_index_rql_limit(req)])
-
 
 class EmailAddressTC(BaseEntityTC):
 
--- a/cubicweb/entities/test/unittest_wfobjs.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/entities/test/unittest_wfobjs.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,7 +16,7 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
-from cubicweb import ValidationError
+from cubicweb import _, ValidationError
 from cubicweb.devtools.testlib import CubicWebTC
 
 def add_wf(shell, etype, name=None, default=False):
--- a/cubicweb/entities/wfobjs.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/entities/wfobjs.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,14 +21,7 @@
 * workflow history (TrInfo)
 * adapter for workflowable entities (IWorkflowableAdapter)
 """
-from __future__ import print_function
-
-
-
-from six import text_type, string_types
-
 from logilab.common.decorators import cached, clear_cache
-from logilab.common.deprecation import deprecated
 
 from cubicweb.entities import AnyEntity, fetch_config
 from cubicweb.view import EntityAdapter
@@ -99,7 +92,7 @@
     def transition_by_name(self, trname):
         rset = self._cw.execute('Any T, TN WHERE T name TN, T name %(n)s, '
                                 'T transition_of WF, WF eid %(wf)s',
-                                {'n': text_type(trname), 'wf': self.eid})
+                                {'n': trname, 'wf': self.eid})
         if rset:
             return rset.get_entity(0, 0)
         return None
@@ -116,7 +109,7 @@
 
     def add_state(self, name, initial=False, **kwargs):
         """add a state to this workflow"""
-        state = self._cw.create_entity('State', name=text_type(name), **kwargs)
+        state = self._cw.create_entity('State', name=name, **kwargs)
         self._cw.execute('SET S state_of WF WHERE S eid %(s)s, WF eid %(wf)s',
                          {'s': state.eid, 'wf': self.eid})
         if initial:
@@ -128,7 +121,7 @@
 
     def _add_transition(self, trtype, name, fromstates,
                         requiredgroups=(), conditions=(), **kwargs):
-        tr = self._cw.create_entity(trtype, name=text_type(name), **kwargs)
+        tr = self._cw.create_entity(trtype, name=name, **kwargs)
         self._cw.execute('SET T transition_of WF '
                          'WHERE T eid %(t)s, WF eid %(wf)s',
                          {'t': tr.eid, 'wf': self.eid})
@@ -258,13 +251,13 @@
         for gname in requiredgroups:
             rset = self._cw.execute('SET T require_group G '
                                     'WHERE T eid %(x)s, G name %(gn)s',
-                                    {'x': self.eid, 'gn': text_type(gname)})
+                                    {'x': self.eid, 'gn': gname})
             assert rset, '%s is not a known group' % gname
-        if isinstance(conditions, string_types):
+        if isinstance(conditions, str):
             conditions = (conditions,)
         for expr in conditions:
-            if isinstance(expr, string_types):
-                kwargs = {'expr': text_type(expr)}
+            if isinstance(expr, str):
+                kwargs = {'expr': expr}
             else:
                 assert isinstance(expr, dict)
                 kwargs = expr
@@ -416,7 +409,7 @@
         """return the default workflow for entities of this type"""
         # XXX CWEType method
         wfrset = self._cw.execute('Any WF WHERE ET default_workflow WF, '
-                                  'ET name %(et)s', {'et': text_type(self.entity.cw_etype)})
+                                  'ET name %(et)s', {'et': self.entity.cw_etype})
         if wfrset:
             return wfrset.get_entity(0, 0)
         self.warning("can't find any workflow for %s", self.entity.cw_etype)
@@ -481,7 +474,7 @@
             'Any T,TT, TN WHERE S allowed_transition T, S eid %(x)s, '
             'T type TT, T type %(type)s, '
             'T name TN, T transition_of WF, WF eid %(wfeid)s',
-            {'x': self.current_state.eid, 'type': text_type(type),
+            {'x': self.current_state.eid, 'type': type,
              'wfeid': self.current_workflow.eid})
         for tr in rset.entities():
             if tr.may_be_fired(self.entity.eid):
@@ -530,7 +523,7 @@
 
     def _get_transition(self, tr):
         assert self.current_workflow
-        if isinstance(tr, string_types):
+        if isinstance(tr, str):
             _tr = self.current_workflow.transition_by_name(tr)
             assert _tr is not None, 'not a %s transition: %s' % (
                 self.__regid__, tr)
--- a/cubicweb/entity.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/entity.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,13 +17,7 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Base class for entity objects manipulated in clients"""
 
-from warnings import warn
-
-from six import text_type, string_types, integer_types
-from six.moves import range
-
 from logilab.common.decorators import cached
-from logilab.common.deprecation import deprecated
 from logilab.common.registry import yes
 from logilab.mtconverter import TransformData, xml_escape
 
@@ -58,7 +52,6 @@
     """return True if value can be used at the end of a Rest URL path"""
     if value is None:
         return False
-    value = text_type(value)
     # the check for ?, /, & are to prevent problems when running
     # behind Apache mod_proxy
     if value == u'' or u'?' in value or u'/' in value or u'&' in value:
@@ -175,7 +168,6 @@
     # class attributes that must be set in class definition
     rest_attr = None
     fetch_attrs = None
-    skip_copy_for = () # bw compat (< 3.14), use cw_skip_copy_for instead
     cw_skip_copy_for = [('in_state', 'subject')]
     # class attributes set automatically at registration time
     e_schema = None
@@ -256,23 +248,11 @@
             select.add_sort_var(var, asc=False)
 
     @classmethod
-    def fetch_rql(cls, user, restriction=None, fetchattrs=None, mainvar='X',
+    def fetch_rql(cls, user, fetchattrs=None, mainvar='X',
                   settype=True, ordermethod='fetch_order'):
         st = cls.fetch_rqlst(user, mainvar=mainvar, fetchattrs=fetchattrs,
                              settype=settype, ordermethod=ordermethod)
-        rql = st.as_string()
-        if restriction:
-            # cannot use RQLRewriter API to insert 'X rtype %(x)s' restriction
-            warn('[3.14] fetch_rql: use of `restriction` parameter is '
-                 'deprecated, please use fetch_rqlst and supply a syntax'
-                 'tree with your restriction instead', DeprecationWarning)
-            insert = ' WHERE ' + ','.join(restriction)
-            if ' WHERE ' in rql:
-                select, where = rql.split(' WHERE ', 1)
-                rql = select + insert + ',' + where
-            else:
-                rql += insert
-        return rql
+        return st.as_string()
 
     @classmethod
     def fetch_rqlst(cls, user, select=None, mainvar='X', fetchattrs=None,
@@ -281,7 +261,7 @@
             select = Select()
             mainvar = select.get_variable(mainvar)
             select.add_selected(mainvar)
-        elif isinstance(mainvar, string_types):
+        elif isinstance(mainvar, str):
             assert mainvar in select.defined_vars
             mainvar = select.get_variable(mainvar)
         # eases string -> syntax tree test transition: please remove once stable
@@ -377,34 +357,8 @@
                 etypecls._fetch_restrictions(var, select, fetchattrs,
                                              user, None, visited=visited)
             if ordermethod is not None:
-                try:
-                    cmeth = getattr(cls, ordermethod)
-                    warn('[3.14] %s %s class method should be renamed to cw_%s'
-                         % (cls.__regid__, ordermethod, ordermethod),
-                         DeprecationWarning)
-                except AttributeError:
-                    cmeth = getattr(cls, 'cw_' + ordermethod)
-                if support_args(cmeth, 'select'):
-                    cmeth(select, attr, var)
-                else:
-                    warn('[3.14] %s should now take (select, attr, var) and '
-                         'modify the syntax tree when desired instead of '
-                         'returning something' % cmeth, DeprecationWarning)
-                    orderterm = cmeth(attr, var.name)
-                    if orderterm is not None:
-                        try:
-                            var, order = orderterm.split()
-                        except ValueError:
-                            if '(' in orderterm:
-                                cls.error('ignore %s until %s is upgraded',
-                                          orderterm, cmeth)
-                                orderterm = None
-                            elif not ' ' in orderterm.strip():
-                                var = orderterm
-                                order = 'ASC'
-                        if orderterm is not None:
-                            select.add_sort_var(select.get_variable(var),
-                                                order=='ASC')
+                cmeth = getattr(cls, 'cw_' + ordermethod)
+                cmeth(select, attr, var)
 
     @classmethod
     @cached
@@ -548,12 +502,12 @@
         return NotImplemented
 
     def __eq__(self, other):
-        if isinstance(self.eid, integer_types):
+        if isinstance(self.eid, int):
             return self.eid == other.eid
         return self is other
 
     def __hash__(self):
-        if isinstance(self.eid, integer_types):
+        if isinstance(self.eid, int):
             return self.eid
         return super(Entity, self).__hash__()
 
@@ -622,14 +576,6 @@
         """
         return self.has_eid() and self._cw_is_saved
 
-    @deprecated('[3.24] cw_metainformation is deprecated')
-    @cached
-    def cw_metainformation(self):
-        source = self.cw_source[0].name
-        return {'type': self.cw_etype,
-                'extid': self.cwuri if source != 'system' else None,
-                'source': {'uri': source}}
-
     def cw_check_perm(self, action):
         self.e_schema.check_perm(self._cw, action, eid=self.eid)
 
@@ -662,10 +608,8 @@
             kwargs['rql'] = 'Any X WHERE X eid %s' % self.eid
         return self._cw.build_url(method, **kwargs)
 
-    def rest_path(self, *args, **kwargs): # XXX cw_rest_path
+    def rest_path(self): # XXX cw_rest_path
         """returns a REST-like (relative) path for this entity"""
-        if args or kwargs:
-            warn("[3.24] rest_path doesn't take parameters anymore", DeprecationWarning)
         mainattr, needcheck = self.cw_rest_attr_info()
         etype = str(self.e_schema)
         path = etype.lower()
@@ -691,7 +635,7 @@
         if path is None:
             # fallback url: <base-url>/<eid> url is used as cw entities uri,
             # prefer it to <base-url>/<etype>/eid/<eid>
-            return text_type(value)
+            return str(value)
         return u'%s/%s' % (path, self._cw.url_quote(value))
 
     def cw_attr_metadata(self, attr, metadata):
@@ -709,7 +653,7 @@
         attr = str(attr)
         if value is _marker:
             value = getattr(self, attr)
-        if isinstance(value, string_types):
+        if isinstance(value, str):
             value = value.strip()
         if value is None or value == '': # don't use "not", 0 is an acceptable value
             return u''
@@ -759,11 +703,6 @@
         assert self.has_eid()
         execute = self._cw.execute
         skip_copy_for = {'subject': set(), 'object': set()}
-        for rtype in self.skip_copy_for:
-            skip_copy_for['subject'].add(rtype)
-            warn('[3.14] skip_copy_for on entity classes (%s) is deprecated, '
-                 'use cw_skip_copy_for instead with list of couples (rtype, role)' % self.cw_etype,
-                 DeprecationWarning)
         for rtype, role in self.cw_skip_copy_for:
             assert role in ('subject', 'object'), role
             skip_copy_for[role].add(rtype)
@@ -1343,29 +1282,6 @@
         for rqlexpr in self.e_schema.get_rqlexprs(action):
             self._cw.local_perm_cache.pop((rqlexpr.eid, (('x', self.eid),)), None)
 
-    # deprecated stuff #########################################################
-
-    @deprecated('[3.16] use cw_set() instead of set_attributes()')
-    def set_attributes(self, **kwargs): # XXX cw_set_attributes
-        if kwargs:
-            self.cw_set(**kwargs)
-
-    @deprecated('[3.16] use cw_set() instead of set_relations()')
-    def set_relations(self, **kwargs): # XXX cw_set_relations
-        """add relations to the given object. To set a relation where this entity
-        is the object of the relation, use 'reverse_'<relation> as argument name.
-
-        Values may be an entity or eid, a list of entities or eids, or None
-        (meaning that all relations of the given type from or to this object
-        should be deleted).
-        """
-        if kwargs:
-            self.cw_set(**kwargs)
-
-    @deprecated('[3.13] use entity.cw_clear_all_caches()')
-    def clear_all_caches(self):
-        return self.cw_clear_all_caches()
-
 
 # attribute and relation descriptors ##########################################
 
@@ -1381,13 +1297,6 @@
             return self
         return eobj.cw_attr_value(self._attrname)
 
-    @deprecated('[3.10] assign to entity.cw_attr_cache[attr] or entity.cw_edited[attr]')
-    def __set__(self, eobj, value):
-        if hasattr(eobj, 'cw_edited') and not eobj.cw_edited.saved:
-            eobj.cw_edited[self._attrname] = value
-        else:
-            eobj.cw_attr_cache[self._attrname] = value
-
 
 class Relation(object):
     """descriptor that controls schema relation access"""
--- a/cubicweb/etwist/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-""" CW - nevow/twisted client
-
-"""
--- a/cubicweb/etwist/http.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +0,0 @@
-"""twisted server for CubicWeb web instances
-
-:organization: Logilab
-:copyright: 2001-2011 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
-"""
-
-
-
-
-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 headers
-        for k, values in self._headers_out.getAllRawHeaders():
-            self._twreq.responseHeaders.setRawHeaders(k, values)
-        # 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):
-        # cw_failed is set on errors such as "connection aborted by client". In
-        # such cases, req.finish() was already called and calling it a twice
-        # would crash
-        if getattr(self._twreq, 'cw_failed', False):
-            return
-        # we must set code before writing anything, else it's too late
-        if self._code is not None:
-            self._twreq.setResponseCode(self._code)
-        if self._stream is not None:
-            self._twreq.write(str(self._stream))
-        self._twreq.finish()
-
-    def __repr__(self):
-        return "<%s.%s code=%d>" % (self.__module__, self.__class__.__name__, self._code)
--- a/cubicweb/etwist/request.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Twisted request handler for CubicWeb"""
-
-from six import text_type
-
-from cubicweb.web.request import CubicWebRequestBase
-
-
-class CubicWebTwistedRequestAdapter(CubicWebRequestBase):
-    """ from twisted .req to cubicweb .form
-    req.files are put into .form[<filefield>]
-    """
-    def __init__(self, req, vreg):
-        self._twreq = req
-        super(CubicWebTwistedRequestAdapter, self).__init__(
-            vreg, req.args, headers=req.received_headers)
-        for key, name_stream_list in req.files.items():
-            for name, stream in name_stream_list:
-                if name is not None:
-                    name = text_type(name, self.encoding)
-                self.form.setdefault(key, []).append((name, stream))
-            # 3.16.4 backward compat
-            if len(self.form[key]) == 1:
-                self.form[key] = self.form[key][0]
-        self.content = self._twreq.content  # stream
-
-    def http_method(self):
-        """returns 'POST', 'GET', 'HEAD', etc."""
-        return self._twreq.method
-
-    def relative_path(self, includeparams=True):
-        """return the normalized path of the request (ie at least relative to
-        the instance's root, but some other normalization may be needed so that
-        the returned path may be used to compare to generated urls
-
-        :param includeparams:
-           boolean indicating if GET form parameters should be kept in the path
-        """
-        path = self._twreq.uri[1:]  # remove the root '/'
-        if not includeparams:
-            path = path.split('?', 1)[0]
-        return path
--- a/cubicweb/etwist/server.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,297 +0,0 @@
-# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""twisted server for CubicWeb web instances"""
-
-import sys
-import traceback
-import threading
-from cgi import FieldStorage, parse_header
-from functools import partial
-import warnings
-
-from cubicweb.statsd_logger import statsd_timeit
-
-from twisted.internet import reactor, task, threads
-from twisted.web import http, server
-from twisted.web import resource
-from twisted.web.server import NOT_DONE_YET
-
-
-from logilab.mtconverter import xml_escape
-from logilab.common.decorators import monkeypatch
-
-from cubicweb import ConfigurationError, CW_EVENT_MANAGER
-from cubicweb.utils import json_dumps
-from cubicweb.web import DirectResponse
-from cubicweb.web.application import CubicWebPublisher
-from cubicweb.etwist.request import CubicWebTwistedRequestAdapter
-from cubicweb.etwist.http import HTTPResponse
-
-
-def start_task(interval, func):
-    lc = task.LoopingCall(func)
-    # wait until interval has expired to actually start the task, else we have
-    # to wait all tasks to be finished for the server to be actually started
-    lc.start(interval, now=False)
-
-
-class CubicWebRootResource(resource.Resource):
-    def __init__(self, config, repo):
-        resource.Resource.__init__(self)
-        self.config = config
-        # instantiate publisher here and not in init_publisher to get some
-        # checks done before daemonization (eg versions consistency)
-        self.appli = CubicWebPublisher(repo, config)
-        self.base_url = config['base-url']
-        global MAX_POST_LENGTH
-        MAX_POST_LENGTH = config['max-post-length']
-
-    def init_publisher(self):
-        config = self.config
-        # when we have an in-memory repository, clean unused sessions every XX
-        # seconds and properly shutdown the server
-        if config['repository-uri'] == 'inmemory://':
-            if config.mode != 'test':
-                reactor.addSystemEventTrigger('before', 'shutdown',
-                                              self.shutdown_event)
-                warnings.warn(
-                    'twisted server does not start repository looping tasks anymore; '
-                    'use the standalone "scheduler" command if needed'
-                )
-        self.set_url_rewriter()
-        CW_EVENT_MANAGER.bind('after-registry-reload', self.set_url_rewriter)
-
-    def start_service(self):
-        start_task(self.appli.session_handler.clean_sessions_interval,
-                   self.appli.session_handler.clean_sessions)
-
-    def set_url_rewriter(self):
-        self.url_rewriter = self.appli.vreg['components'].select_or_none('urlrewriter')
-
-    def shutdown_event(self):
-        """callback fired when the server is shutting down to properly
-        clean opened sessions
-        """
-        self.appli.repo.shutdown()
-
-    def getChild(self, path, request):
-        """Indicate which resource to use to process down the URL's path"""
-        return self
-
-    def on_request_finished_ko(self, request, reason):
-        # annotate the twisted request so that we're able later to check for
-        # failure without having to dig into request's internal attributes such
-        # as _disconnected
-        request.cw_failed = True
-        self.warning('request finished abnormally: %s', reason)
-
-    def render(self, request):
-        """Render a page from the root resource"""
-        finish_deferred = request.notifyFinish()
-        finish_deferred.addErrback(partial(self.on_request_finished_ko, request))
-        # reload modified files in debug mode
-        if self.config.debugmode:
-            self.config.uiprops.reload_if_needed()
-            self.appli.vreg.reload_if_needed()
-        if self.config['profile']: # default profiler don't trace threads
-            return self.render_request(request)
-        else:
-            deferred = threads.deferToThread(self.render_request, request)
-            return NOT_DONE_YET
-
-    @statsd_timeit
-    def render_request(self, request):
-        try:
-            # processing HUGE files (hundred of megabytes) in http.processReceived
-            # blocks other HTTP requests processing
-            # due to the clumsy & slow parsing algorithm of cgi.FieldStorage
-            # so we deferred that part to the cubicweb thread
-            request.process_multipart()
-            return self._render_request(request)
-        except Exception:
-            trace = traceback.format_exc()
-            return HTTPResponse(stream='<pre>%s</pre>' % xml_escape(trace),
-                                code=500, twisted_request=request)
-
-    def _render_request(self, request):
-        origpath = request.path
-        host = request.host
-        if self.url_rewriter is not None:
-            # XXX should occur before authentication?
-            path = self.url_rewriter.rewrite(host, origpath, request)
-            request.uri.replace(origpath, path, 1)
-        req = CubicWebTwistedRequestAdapter(request, self.appli.vreg)
-        try:
-            ### Try to generate the actual request content
-            content = self.appli.handle_request(req)
-        except DirectResponse as ex:
-            return ex.response
-        # at last: create twisted object
-        return HTTPResponse(code    = req.status_out,
-                            headers = req.headers_out,
-                            stream  = content,
-                            twisted_request=req._twreq)
-
-    # these are overridden by set_log_methods below
-    # only defining here to prevent pylint from complaining
-    @classmethod
-    def debug(cls, msg, *a, **kw):
-        pass
-    info = warning = error = critical = exception = debug
-
-
-JSON_PATHS = set(('json',))
-FRAME_POST_PATHS = set(('validateform',))
-
-orig_gotLength = http.Request.gotLength
-@monkeypatch(http.Request)
-def gotLength(self, length):
-    orig_gotLength(self, length)
-    if length > MAX_POST_LENGTH: # length is 0 on GET
-        path = self.channel._path.split('?', 1)[0].rstrip('/').rsplit('/', 1)[-1]
-        self.clientproto = 'HTTP/1.1' # not yet initialized
-        self.channel.persistent = 0   # force connection close on cleanup
-        self.setResponseCode(http.REQUEST_ENTITY_TOO_LARGE)
-        if path in JSON_PATHS: # XXX better json path detection
-            self.setHeader('content-type',"application/json")
-            body = json_dumps({'reason': 'request max size exceeded'})
-        elif path in FRAME_POST_PATHS: # XXX better frame post path detection
-            self.setHeader('content-type',"text/html")
-            body = ('<script type="text/javascript">'
-                    'window.parent.handleFormValidationResponse(null, null, null, %s, null);'
-                    '</script>' % json_dumps( (False, 'request max size exceeded', None) ))
-        else:
-            self.setHeader('content-type',"text/html")
-            body = ("<html><head><title>Processing Failed</title></head><body>"
-                    "<b>request max size exceeded</b></body></html>")
-        self.setHeader('content-length', str(len(body)))
-        self.write(body)
-        # see request.finish(). Done here since we get error due to not full
-        # initialized request
-        self.finished = 1
-        if not self.queued:
-            self._cleanup()
-        for d in self.notifications:
-            d.callback(None)
-        self.notifications = []
-
-@monkeypatch(http.Request)
-def requestReceived(self, command, path, version):
-    """Called by channel when all data has been received.
-
-    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:
-        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')
-    self._do_process_multipart = False
-    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))
-            self.content.seek(0)
-        elif key == 'multipart/form-data':
-            # defer this as it can be extremely time consumming
-            # with big files
-            self._do_process_multipart = True
-    self.process()
-
-@monkeypatch(http.Request)
-def process_multipart(self):
-    if not self._do_process_multipart:
-        return
-    form = FieldStorage(self.content, self.received_headers,
-                        environ={'REQUEST_METHOD': 'POST'},
-                        keep_blank_values=1,
-                        strict_parsing=1)
-    for key in form:
-        values = form[key]
-        if not isinstance(values, list):
-            values = [values]
-        for value in values:
-            if value.filename:
-                if value.done != -1: # -1 is transfer has been interrupted
-                    self.files.setdefault(key, []).append((value.filename, value.file))
-                else:
-                    self.files.setdefault(key, []).append((None, None))
-            else:
-                self.args.setdefault(key, []).append(value.value)
-
-from logging import getLogger
-from cubicweb import set_log_methods
-LOGGER = getLogger('cubicweb.twisted')
-set_log_methods(CubicWebRootResource, LOGGER)
-
-def run(config, debug=None, repo=None):
-    # repo may by passed during test.
-    #
-    # Test has already created a repo object so we should not create a new one.
-    # Explicitly passing the repo object avoid relying on the fragile
-    # config.repository() cache. We could imagine making repo a mandatory
-    # argument and receives it from the starting command directly.
-    if debug is not None:
-        config.debugmode = debug
-    config.check_writeable_uid_directory(config.appdatahome)
-    # create the site
-    if repo is None:
-        repo = config.repository()
-    root_resource = CubicWebRootResource(config, repo)
-    website = server.Site(root_resource)
-    # serve it via standard HTTP on port set in the configuration
-    port = config['port'] or 8080
-    interface = config['interface']
-    reactor.suggestThreadPoolSize(config['webserver-threadpool-size'])
-    reactor.listenTCP(port, website, interface=interface)
-    if not config.debugmode:
-        if sys.platform == 'win32':
-            raise ConfigurationError("Under windows, you must use the service management "
-                                     "commands (e.g : 'net start my_instance)'")
-        from logilab.common.daemon import daemonize
-        LOGGER.info('instance started in the background on %s', root_resource.base_url)
-        whichproc = daemonize(config['pid-file'], umask=config['umask'])
-        if whichproc: # 1 = orig process, 2 = first fork, None = second fork (eg daemon process)
-            return whichproc # parent process
-    root_resource.init_publisher() # before changing uid
-    if config['uid'] is not None:
-        from logilab.common.daemon import setugid
-        setugid(config['uid'])
-    root_resource.start_service()
-    LOGGER.info('instance started on %s', root_resource.base_url)
-    # avoid annoying warnign if not in Main Thread
-    signals = threading.currentThread().getName() == 'MainThread'
-    if config['profile']:
-        import cProfile
-        cProfile.runctx('reactor.run(installSignalHandlers=%s)' % signals,
-                        globals(), locals(), config['profile'])
-    else:
-        reactor.run(installSignalHandlers=signals)
--- a/cubicweb/etwist/service.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,100 +0,0 @@
-# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-from __future__ import print_function
-
-import os
-import sys
-
-try:
-    import win32serviceutil
-    import win32service
-except ImportError:
-    print('Win32 extensions for Python are likely not installed.')
-    sys.exit(3)
-
-from os.path import join
-
-from cubicweb.etwist.server import (CubicWebRootResource, reactor, server)
-
-from logilab.common.shellutils import rm
-
-import logging
-from logging import getLogger, handlers
-from cubicweb import set_log_methods
-from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
-
-
-def _check_env(env):
-    env_vars = ('CW_INSTANCES_DIR', 'CW_INSTANCES_DATA_DIR', 'CW_RUNTIME_DIR')
-    for var in env_vars:
-        if var not in env:
-            raise Exception('The environment variables %s must be set.' %
-                            ', '.join(env_vars))
-    if not env.get('USERNAME'):
-        env['USERNAME'] = 'cubicweb'
-
-
-class CWService(object, win32serviceutil.ServiceFramework):
-    _svc_name_ = None
-    _svc_display_name_ = None
-    instance = None
-
-    def __init__(self, *args, **kwargs):
-        win32serviceutil.ServiceFramework.__init__(self, *args, **kwargs)
-        cwcfg.load_cwctl_plugins()
-        logger = getLogger('cubicweb')
-        set_log_methods(CubicWebRootResource, logger)
-
-    def SvcStop(self):
-        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
-        logger = getLogger('cubicweb.twisted')
-        logger.info('stopping %s service' % self.instance)
-        reactor.stop()
-        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
-
-    def SvcDoRun(self):
-        self.ReportServiceStatus(win32service.SERVICE_START_PENDING)
-        logger = getLogger('cubicweb.twisted')
-        handler = handlers.NTEventLogHandler('cubicweb')
-        handler.setLevel(logging.INFO)
-        logger.addHandler(handler)
-        logger.info('starting %s service' % self.instance)
-        try:
-            _check_env(os.environ)
-            # create the site
-            config = cwcfg.config_for(self.instance)
-            config.init_log(force=True)
-            config.debugmode = False
-            logger.info('starting cubicweb instance %s ', self.instance)
-            config.info('clear ui caches')
-            rm(join(config.appdatahome, 'uicache', '*'))
-            root_resource = CubicWebRootResource(config, config.repository())
-            website = server.Site(root_resource)
-            # serve it via standard HTTP on port set in the configuration
-            port = config['port'] or 8080
-            logger.info('listening on port %s' % port)
-            reactor.listenTCP(port, website)
-            root_resource.init_publisher()
-            root_resource.start_service()
-            logger.info('instance started on %s', root_resource.base_url)
-            self.ReportServiceStatus(win32service.SERVICE_RUNNING)
-            reactor.run()
-        except Exception as e:
-            logger.error('service %s stopped (cause: %s)' % (self.instance, e))
-            logger.exception('what happened ...')
-        self.ReportServiceStatus(win32service.SERVICE_STOPPED)
--- a/cubicweb/etwist/test/data/views.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-# copyright 2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""only for unit tests !"""
-
-from cubicweb.view import View
-from cubicweb.predicates import match_http_method
-
-class PutView(View):
-    __regid__ = 'put'
-    __select__ = match_http_method('PUT') | match_http_method('POST')
-    binary = True
-
-    def call(self):
-        self.w(self._cw.content.read())
--- a/cubicweb/etwist/test/unittest_server.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-
-import os, os.path as osp, glob
-import urllib
-
-from cubicweb.devtools.httptest import CubicWebServerTC
-
-
-class ETwistHTTPTC(CubicWebServerTC):
-    def test_put_content(self):
-        data = {'hip': 'hop'}
-        headers = {'Content-Type': 'application/x-www-form-urlencoded'}
-        body = urllib.urlencode(data)
-        response = self.web_request('?vid=put', method='PUT', body=body)
-        self.assertEqual(body, response.body)
-        response = self.web_request('?vid=put', method='POST', body=body,
-                                    headers=headers)
-        self.assertEqual(body, response.body)
-
-if __name__ == '__main__':
-    from logilab.common.testlib import unittest_main
-    unittest_main()
--- a/cubicweb/etwist/twconfig.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,110 +0,0 @@
-# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""twisted server configurations:
-
-* the "all-in-one" configuration to get a web instance running in a twisted
-  web server integrating a repository server in the same process (only available
-  if the repository part of the software is installed
-"""
-
-
-from os.path import join
-
-from logilab.common.configuration import Method, merge_options
-
-from cubicweb.cwconfig import CONFIGURATIONS
-from cubicweb.server.serverconfig import ServerConfiguration
-from cubicweb.web.webconfig import WebConfiguration
-
-
-class WebConfigurationBase(WebConfiguration):
-    """web instance (in a twisted web server) client of a RQL server"""
-
-    options = merge_options((
-        # ctl configuration
-        ('port',
-         {'type' : 'int',
-          'default': None,
-          'help': 'http server port number (default to 8080)',
-          'group': 'web', 'level': 0,
-          }),
-        ('interface',
-         {'type' : 'string',
-          'default': '0.0.0.0',
-          'help': 'http server address on which to listen (default to everywhere)',
-          'group': 'web', 'level': 1,
-          }),
-        ('max-post-length',
-         {'type' : 'bytes',
-          'default': '100MB',
-          'help': 'maximum length of HTTP request. Default to 100 MB.',
-          'group': 'web', 'level': 1,
-          }),
-        ('profile',
-         {'type' : 'string',
-          'default': None,
-          'help': 'profile code and use the specified file to store stats if this option is set',
-          'group': 'web', 'level': 3,
-          }),
-        ('host',
-         {'type' : 'string',
-          'default': None,
-          'help': 'host name if not correctly detectable through gethostname',
-          'group': 'main', 'level': 1,
-          }),
-        ('pid-file',
-         {'type' : 'string',
-          'default': Method('default_pid_file'),
-          'help': 'repository\'s pid file',
-          'group': 'main', 'level': 2,
-          }),
-        ('uid',
-         {'type' : 'string',
-          'default': None,
-          'help': 'unix user, if this option is set, use the specified user to start \
-the repository rather than the user running the command',
-          'group': 'main', 'level': WebConfiguration.mode == 'system'
-          }),
-        ('webserver-threadpool-size',
-         {'type': 'int',
-          'default': 4,
-          'help': "size of twisted's reactor threadpool. It should probably be not too \
-much greater than connection-poolsize",
-          'group': 'web', 'level': 3,
-          }),
-        ) + WebConfiguration.options)
-
-    def server_file(self):
-        return join(self.apphome, '%s-%s.py' % (self.appid, self.name))
-
-    def default_base_url(self):
-        from socket import getfqdn
-        return 'http://%s:%s/' % (self['host'] or getfqdn().lower(), self['port'] or 8080)
-
-
-class AllInOneConfiguration(WebConfigurationBase, ServerConfiguration):
-    """repository and web instance in the same twisted process"""
-    name = 'all-in-one'
-    options = merge_options(WebConfigurationBase.options
-                            + ServerConfiguration.options)
-
-    cubicweb_appobject_path = WebConfigurationBase.cubicweb_appobject_path | ServerConfiguration.cubicweb_appobject_path
-    cube_appobject_path = WebConfigurationBase.cube_appobject_path | ServerConfiguration.cube_appobject_path
-
-
-CONFIGURATIONS.append(AllInOneConfiguration)
--- a/cubicweb/etwist/twctl.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb-clt handlers for twisted"""
-
-from cubicweb.toolsutils import CommandHandler
-from cubicweb.web.webctl import WebCreateHandler, WebUpgradeHandler
-from cubicweb.server import serverctl
-
-# trigger configuration registration
-import cubicweb.etwist.twconfig # pylint: disable=W0611
-
-class TWCreateHandler(WebCreateHandler):
-    cfgname = 'twisted'
-
-class TWStartHandler(CommandHandler):
-    cmdname = 'start'
-    cfgname = 'twisted'
-
-    def start_server(self, config):
-        from cubicweb.etwist import server
-        return server.run(config)
-
-class TWStopHandler(CommandHandler):
-    cmdname = 'stop'
-    cfgname = 'twisted'
-
-    def poststop(self):
-        pass
-
-class TWUpgradeHandler(WebUpgradeHandler):
-    cfgname = 'twisted'
-
-
-class AllInOneCreateHandler(serverctl.RepositoryCreateHandler,
-                            TWCreateHandler):
-    """configuration to get an instance running in a twisted web server
-    integrating a repository server in the same process
-    """
-    cfgname = 'all-in-one'
-
-    def bootstrap(self, cubes, automatic=False, inputlevel=0):
-        """bootstrap this configuration"""
-        serverctl.RepositoryCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
-        TWCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
-
-class AllInOneStartHandler(TWStartHandler):
-    cmdname = 'start'
-    cfgname = 'all-in-one'
-    subcommand = 'cubicweb-twisted'
-
-class AllInOneStopHandler(CommandHandler):
-    cmdname = 'stop'
-    cfgname = 'all-in-one'
-    subcommand = 'cubicweb-twisted'
-
-    def poststop(self):
-        pass
-
-class AllInOneUpgradeHandler(TWUpgradeHandler):
-    cfgname = 'all-in-one'
--- a/cubicweb/ext/rest.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/ext/rest.py	Fri Oct 18 23:39:03 2019 +0200
@@ -38,9 +38,7 @@
 from itertools import chain
 from logging import getLogger
 from os.path import join
-
-from six import text_type
-from six.moves.urllib.parse import urlsplit
+from urllib.parse import urlsplit
 
 from docutils import statemachine, nodes, utils, io
 from docutils.core import Publisher
@@ -403,7 +401,7 @@
       the data formatted as HTML or the original data if an error occurred
     """
     req = context._cw
-    if isinstance(data, text_type):
+    if isinstance(data, str):
         encoding = 'utf-8'
         # remove unprintable characters unauthorized in xml
         data = data.translate(ESC_UCAR_TABLE)
@@ -448,8 +446,8 @@
         return res
     except BaseException:
         LOGGER.exception('error while publishing ReST text')
-        if not isinstance(data, text_type):
-            data = text_type(data, encoding, 'replace')
+        if not isinstance(data, str):
+            data = data.encode(encoding, 'replace')
         return xml_escape(req._('error while publishing ReST text')
                            + '\n\n' + data)
 
--- a/cubicweb/ext/test/unittest_rest.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/ext/test/unittest_rest.py	Fri Oct 18 23:39:03 2019 +0200
@@ -15,7 +15,6 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-from six import PY3
 
 from logilab.common.testlib import unittest_main
 from cubicweb.devtools.testlib import CubicWebTC
@@ -93,8 +92,7 @@
             context = self.context(req)
             out = rest_publish(context, ':rql:`Any X WHERE X is CWUser:toto`')
             self.assertTrue(out.startswith("<p>an error occurred while interpreting this "
-                                           "rql directive: ObjectNotFound(%s'toto'" %
-                                           ('' if PY3 else 'u')),
+                                           "rql directive: ObjectNotFound('toto'"),
                             out)
 
     def test_rql_role_without_vid(self):
--- a/cubicweb/hooks/integrity.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/hooks/integrity.py	Fri Oct 18 23:39:03 2019 +0200
@@ -23,8 +23,6 @@
 
 from threading import Lock
 
-from six import text_type
-
 from cubicweb import validation_error, neg_role
 from cubicweb.schema import (META_RTYPES, WORKFLOW_RTYPES,
                              RQLConstraint, RQLUniqueConstraint)
@@ -276,7 +274,7 @@
                     value = edited[attr]
                 except KeyError:
                     continue # no text to tidy
-                if isinstance(value, text_type): # filter out None and Binary
+                if isinstance(value, str): # filter out None and Binary
                     if getattr(entity, str(metaattr)) == 'text/html':
                         edited[attr] = soup2xhtml(value, self._cw.encoding)
 
--- a/cubicweb/hooks/notification.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/hooks/notification.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,7 +20,6 @@
 
 
 from logilab.common.textutils import normalize_text
-from logilab.common.deprecation import deprecated
 
 from cubicweb import RegistryNotFound
 from cubicweb.predicates import is_instance
@@ -28,11 +27,6 @@
 from cubicweb.sobjects.supervising import SupervisionMailOp
 
 
-@deprecated('[3.17] use notify_on_commit instead')
-def RenderAndSendNotificationView(cnx, view, viewargs=None):
-    notify_on_commit(cnx, view, viewargs)
-
-
 def notify_on_commit(cnx, view, viewargs=None):
     """register a notification view (see
     :class:`~cubicweb.sobjects.notification.NotificationView`) to be sent at
@@ -76,6 +70,12 @@
                 # to prevent them all.
                 self.exception('Notification failed')
 
+                if self.cnx.vreg.config.mode == "test":
+                    # reraise in testing context because we actually want to
+                    # have those exceptions here and that self.exception is
+                    # filtered in test context
+                    raise
+
 
 class NotificationHook(hook.Hook):
     __abstract__ = True
--- a/cubicweb/hooks/syncschema.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/hooks/syncschema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -33,10 +33,11 @@
 from logilab.common.decorators import clear_cache
 
 from cubicweb import _
-from cubicweb import validation_error
+from cubicweb import validation_error, ETYPE_NAME_MAP
 from cubicweb.predicates import is_instance
 from cubicweb.schema import (SCHEMA_TYPES, META_RTYPES, VIRTUAL_RTYPES,
-                             CONSTRAINTS, UNIQUE_CONSTRAINTS, ETYPE_NAME_MAP)
+                             CONSTRAINTS, UNIQUE_CONSTRAINTS)
+from cubicweb.schema import constraint_name_for
 from cubicweb.server import hook, schemaserial as ss, schema2sql as y2sql
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.hooks.synccomputed import RecomputeAttributeOperation
@@ -758,7 +759,7 @@
                           'IntervalBoundConstraint',
                           'StaticVocabularyConstraint'):
             cnx.system_sql('ALTER TABLE %s%s DROP CONSTRAINT %s'
-                           % (SQL_PREFIX, rdef.subject, self.oldcstr.name_for(rdef)))
+                           % (SQL_PREFIX, rdef.subject, constraint_name_for(self.oldcstr, rdef)))
 
     def revertprecommit_event(self):
         # revert changes on in memory schema
@@ -812,7 +813,7 @@
             # oldcstr is the new constraint when the attribute is being added in the same
             # transaction or when constraint value is updated. So we've to take care...
             if oldcstr is not None:
-                oldcstrname = self.oldcstr.name_for(rdef)
+                oldcstrname = constraint_name_for(self.oldcstr, rdef)
                 if oldcstrname != cstrname:
                     cnx.system_sql('ALTER TABLE %s%s DROP CONSTRAINT %s'
                                    % (SQL_PREFIX, rdef.subject, oldcstrname))
--- a/cubicweb/hooks/test/data-computed/schema.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/hooks/test/data-computed/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,6 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 from yams.buildobjs import EntityType, String, Int, SubjectRelation, RelationDefinition
 
+from cubicweb import _
+
 THISYEAR = 2014
 
 class Person(EntityType):
--- a/cubicweb/hooks/test/unittest_hooks.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/hooks/test/unittest_hooks.py	Fri Oct 18 23:39:03 2019 +0200
@@ -24,8 +24,6 @@
 
 from datetime import datetime
 
-from six import text_type
-
 from pytz import utc
 
 from cubicweb import ValidationError
@@ -211,7 +209,7 @@
             with self.assertRaises(ValidationError) as cm:
                 cnx.execute('INSERT CWUser X: X login "admin", X upassword "admin"')
             ex = cm.exception
-            ex.translate(text_type)
+            ex.translate(str)
             self.assertIsInstance(ex.entity, int)
             self.assertEqual(ex.errors,
                              {'': u'some relations violate a unicity constraint',
--- a/cubicweb/hooks/test/unittest_syncsession.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/hooks/test/unittest_syncsession.py	Fri Oct 18 23:39:03 2019 +0200
@@ -22,8 +22,6 @@
   syncschema.py hooks are mostly tested in server/test/unittest_migrations.py
 """
 
-from six import text_type
-
 from cubicweb import ValidationError
 from cubicweb.devtools.testlib import CubicWebTC
 
@@ -35,13 +33,13 @@
             with self.assertRaises(ValidationError) as cm:
                 req.execute('INSERT CWProperty X: X pkey "bla.bla", '
                             'X value "hop", X for_user U')
-            cm.exception.translate(text_type)
+            cm.exception.translate(str)
             self.assertEqual(cm.exception.errors,
                              {'pkey-subject': 'unknown property key bla.bla'})
 
             with self.assertRaises(ValidationError) as cm:
                 req.execute('INSERT CWProperty X: X pkey "bla.bla", X value "hop"')
-            cm.exception.translate(text_type)
+            cm.exception.translate(str)
             self.assertEqual(cm.exception.errors,
                              {'pkey-subject': 'unknown property key bla.bla'})
 
--- a/cubicweb/i18n.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/i18n.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,17 +16,11 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Some i18n/gettext utilities."""
-from __future__ import print_function
-
-
-
 import re
 import os
 from os.path import join, basename, splitext, exists
 from glob import glob
 
-from six import PY2
-
 from cubicweb.toolsutils import create_dir
 
 def extract_from_tal(files, output_file):
@@ -42,11 +36,7 @@
 
 def add_msg(w, msgid, msgctx=None):
     """write an empty pot msgid definition"""
-    if PY2 and isinstance(msgid, unicode):
-        msgid = msgid.encode('utf-8')
     if msgctx:
-        if PY2 and isinstance(msgctx, unicode):
-            msgctx = msgctx.encode('utf-8')
         w('msgctxt "%s"\n' % msgctx)
     msgid = msgid.replace('"', r'\"').splitlines()
     if len(msgid) > 1:
--- a/cubicweb/i18n/de.po	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/i18n/de.po	Fri Oct 18 23:39:03 2019 +0200
@@ -259,12 +259,6 @@
 msgid "CWAttribute_plural"
 msgstr "Attribute"
 
-msgid "CWCache"
-msgstr "Cache"
-
-msgid "CWCache_plural"
-msgstr "Caches"
-
 msgid "CWComputedRType"
 msgstr ""
 
@@ -546,9 +540,6 @@
 msgid "New CWAttribute"
 msgstr "Neue finale Relationsdefinition"
 
-msgid "New CWCache"
-msgstr "Neuer Anwendungs-Cache"
-
 msgid "New CWComputedRType"
 msgstr ""
 
@@ -767,9 +758,6 @@
 msgid "This CWAttribute:"
 msgstr "diese finale Relationsdefinition:"
 
-msgid "This CWCache:"
-msgstr "Dieser Anwendungs-Cache:"
-
 msgid "This CWComputedRType:"
 msgstr ""
 
@@ -974,13 +962,6 @@
 msgid "a number (in seconds) or 20s, 10min, 24h or 4d are expected"
 msgstr ""
 
-msgid ""
-"a simple cache entity characterized by a name and a validity date. The "
-"target application is responsible for updating timestamp when necessary to "
-"invalidate the cache (typically in hooks). Also, checkout the AppObject."
-"get_cache() method."
-msgstr ""
-
 msgid "abstract base class for transitions"
 msgstr "abstrakte Basisklasse für Übergänge"
 
@@ -1107,9 +1088,6 @@
 msgid "add a CWAttribute"
 msgstr ""
 
-msgid "add a CWCache"
-msgstr ""
-
 msgid "add a CWComputedRType"
 msgstr ""
 
@@ -2663,9 +2641,6 @@
 msgid "gc"
 msgstr ""
 
-msgid "generic plot"
-msgstr "generischer Plot"
-
 msgid "generic relation to link one entity to another"
 msgstr "generische Relation zur Verbindung einer Entität mit einer anderen"
 
@@ -3202,10 +3177,6 @@
 msgid "name"
 msgstr "Name"
 
-msgctxt "CWCache"
-msgid "name"
-msgstr "Name"
-
 msgctxt "CWComputedRType"
 msgid "name"
 msgstr ""
@@ -3250,9 +3221,6 @@
 msgid "name"
 msgstr "Name"
 
-msgid "name of the cache"
-msgstr "Name des Caches"
-
 msgid ""
 "name of the main variables which should be used in the selection if "
 "necessary (comma separated)"
@@ -3317,9 +3285,6 @@
 msgid "no related entity"
 msgstr "keine verknüpfte Entität"
 
-msgid "no repository sessions found"
-msgstr "keine Datenbank-Sitzung gefunden"
-
 msgid "no selected entities"
 msgstr "keine Entitäten ausgewählt"
 
@@ -3372,9 +3337,6 @@
 msgid "open all"
 msgstr "alle öffnen"
 
-msgid "opened sessions"
-msgstr "offene Sitzungen"
-
 msgid "opened web sessions"
 msgstr "offene Web-Sitzungen"
 
@@ -3782,6 +3744,9 @@
 msgid "severity"
 msgstr ""
 
+msgid "should css be compiled and store in uicache"
+msgstr ""
+
 msgid ""
 "should html fields being edited using fckeditor (a HTML WYSIWYG editor).  "
 "You should also select text/html as default text format to actually get "
@@ -3855,6 +3820,9 @@
 msgid "specifying %s is mandatory"
 msgstr ""
 
+msgid "specifying an URL is mandatory"
+msgstr ""
+
 msgid ""
 "start timestamp of the currently in synchronization, or NULL when no "
 "synchronization in progress."
@@ -4045,9 +4013,6 @@
 msgid "the prefered email"
 msgstr "primäre E-Mail-Adresse"
 
-msgid "the system source has its configuration stored on the file-system"
-msgstr ""
-
 msgid "there is no next page"
 msgstr ""
 
@@ -4070,13 +4035,6 @@
 msgid "thursday"
 msgstr "Donnerstag"
 
-msgid "timestamp"
-msgstr "Datum"
-
-msgctxt "CWCache"
-msgid "timestamp"
-msgstr "gültig seit"
-
 msgid "timetable"
 msgstr "Zeitplan"
 
@@ -4576,9 +4534,21 @@
 #~ msgid "Browse by category"
 #~ msgstr "nach Kategorien navigieren"
 
+#~ msgid "CWCache"
+#~ msgstr "Cache"
+
+#~ msgid "CWCache_plural"
+#~ msgstr "Caches"
+
+#~ msgid "New CWCache"
+#~ msgstr "Neuer Anwendungs-Cache"
+
 #~ msgid "No account? Try public access at %s"
 #~ msgstr "Kein Konto? Zur öffentlichen Website: %s"
 
+#~ msgid "This CWCache:"
+#~ msgstr "Dieser Anwendungs-Cache:"
+
 #~ msgid "anonymous"
 #~ msgstr "anonym"
 
@@ -4599,9 +4569,25 @@
 #~ "Fehler beim Zugriff auf Quelle %s, möglicherweise sind die Daten "
 #~ "unvollständig."
 
+#~ msgid "generic plot"
+#~ msgstr "generischer Plot"
+
+#~ msgctxt "CWCache"
+#~ msgid "name"
+#~ msgstr "Name"
+
+#~ msgid "name of the cache"
+#~ msgstr "Name des Caches"
+
 #~ msgid "no edited fields specified for entity %s"
 #~ msgstr "kein Eingabefeld spezifiziert Für Entität %s"
 
+#~ msgid "no repository sessions found"
+#~ msgstr "keine Datenbank-Sitzung gefunden"
+
+#~ msgid "opened sessions"
+#~ msgstr "offene Sitzungen"
+
 #~ msgid "the value \"%s\" is already used, use another one"
 #~ msgstr ""
 #~ "Der Wert \"%s\" wird bereits benutzt, bitte verwenden Sie einen anderen "
@@ -4609,3 +4595,10 @@
 
 #~ msgid "timeline"
 #~ msgstr "Zeitleiste"
+
+#~ msgid "timestamp"
+#~ msgstr "Datum"
+
+#~ msgctxt "CWCache"
+#~ msgid "timestamp"
+#~ msgstr "gültig seit"
--- a/cubicweb/i18n/en.po	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/i18n/en.po	Fri Oct 18 23:39:03 2019 +0200
@@ -248,12 +248,6 @@
 msgid "CWAttribute_plural"
 msgstr "Attributes"
 
-msgid "CWCache"
-msgstr "CubicWeb Cache"
-
-msgid "CWCache_plural"
-msgstr "CubicWeb Caches"
-
 msgid "CWComputedRType"
 msgstr "Virtual relation"
 
@@ -524,9 +518,6 @@
 msgid "New CWAttribute"
 msgstr "New attribute"
 
-msgid "New CWCache"
-msgstr "New cache"
-
 msgid "New CWComputedRType"
 msgstr "New virtual relation"
 
@@ -618,7 +609,8 @@
 msgstr "Passwords"
 
 msgid "Persistent session. Used by cubicweb.pyramid to store the session data."
-msgstr "Persistent session. Used by cubicweb.pyramid to store the session data."
+msgstr ""
+"Persistent session. Used by cubicweb.pyramid to store the session data."
 
 msgid "Please note that this is only a shallow copy"
 msgstr ""
@@ -743,9 +735,6 @@
 msgid "This CWAttribute:"
 msgstr "This attribute:"
 
-msgid "This CWCache:"
-msgstr "This cache:"
-
 msgid "This CWComputedRType:"
 msgstr "This virtual relation:"
 
@@ -936,13 +925,6 @@
 msgid "a number (in seconds) or 20s, 10min, 24h or 4d are expected"
 msgstr ""
 
-msgid ""
-"a simple cache entity characterized by a name and a validity date. The "
-"target application is responsible for updating timestamp when necessary to "
-"invalidate the cache (typically in hooks). Also, checkout the AppObject."
-"get_cache() method."
-msgstr ""
-
 msgid "abstract base class for transitions"
 msgstr ""
 
@@ -1069,9 +1051,6 @@
 msgid "add a CWAttribute"
 msgstr ""
 
-msgid "add a CWCache"
-msgstr ""
-
 msgid "add a CWComputedRType"
 msgstr ""
 
@@ -2612,9 +2591,6 @@
 msgid "gc"
 msgstr "memory leak"
 
-msgid "generic plot"
-msgstr ""
-
 msgid "generic relation to link one entity to another"
 msgstr ""
 
@@ -3122,10 +3098,6 @@
 msgid "name"
 msgstr "name"
 
-msgctxt "CWCache"
-msgid "name"
-msgstr "name"
-
 msgctxt "CWComputedRType"
 msgid "name"
 msgstr "name"
@@ -3170,9 +3142,6 @@
 msgid "name"
 msgstr "name"
 
-msgid "name of the cache"
-msgstr ""
-
 msgid ""
 "name of the main variables which should be used in the selection if "
 "necessary (comma separated)"
@@ -3235,9 +3204,6 @@
 msgid "no related entity"
 msgstr ""
 
-msgid "no repository sessions found"
-msgstr ""
-
 msgid "no selected entities"
 msgstr ""
 
@@ -3290,9 +3256,6 @@
 msgid "open all"
 msgstr ""
 
-msgid "opened sessions"
-msgstr ""
-
 msgid "opened web sessions"
 msgstr ""
 
@@ -3696,6 +3659,9 @@
 msgid "severity"
 msgstr ""
 
+msgid "should css be compiled and store in uicache"
+msgstr ""
+
 msgid ""
 "should html fields being edited using fckeditor (a HTML WYSIWYG editor).  "
 "You should also select text/html as default text format to actually get "
@@ -3762,6 +3728,9 @@
 msgid "specifying %s is mandatory"
 msgstr ""
 
+msgid "specifying an URL is mandatory"
+msgstr ""
+
 msgid ""
 "start timestamp of the currently in synchronization, or NULL when no "
 "synchronization in progress."
@@ -3948,9 +3917,6 @@
 msgid "the prefered email"
 msgstr ""
 
-msgid "the system source has its configuration stored on the file-system"
-msgstr ""
-
 msgid "there is no next page"
 msgstr ""
 
@@ -3973,13 +3939,6 @@
 msgid "thursday"
 msgstr ""
 
-msgid "timestamp"
-msgstr ""
-
-msgctxt "CWCache"
-msgid "timestamp"
-msgstr "timestamp"
-
 msgid "timetable"
 msgstr ""
 
@@ -4459,3 +4418,23 @@
 
 msgid "you should probably delete that property"
 msgstr ""
+
+#~ msgid "CWCache"
+#~ msgstr "CubicWeb Cache"
+
+#~ msgid "CWCache_plural"
+#~ msgstr "CubicWeb Caches"
+
+#~ msgid "New CWCache"
+#~ msgstr "New cache"
+
+#~ msgid "This CWCache:"
+#~ msgstr "This cache:"
+
+#~ msgctxt "CWCache"
+#~ msgid "name"
+#~ msgstr "name"
+
+#~ msgctxt "CWCache"
+#~ msgid "timestamp"
+#~ msgstr "timestamp"
--- a/cubicweb/i18n/es.po	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/i18n/es.po	Fri Oct 18 23:39:03 2019 +0200
@@ -262,12 +262,6 @@
 msgid "CWAttribute_plural"
 msgstr "Atributos"
 
-msgid "CWCache"
-msgstr "Cache"
-
-msgid "CWCache_plural"
-msgstr "Caches"
-
 msgid "CWComputedRType"
 msgstr ""
 
@@ -550,9 +544,6 @@
 msgid "New CWAttribute"
 msgstr "Nueva definición de relación final"
 
-msgid "New CWCache"
-msgstr "Agregar Caché"
-
 msgid "New CWComputedRType"
 msgstr ""
 
@@ -772,9 +763,6 @@
 msgid "This CWAttribute:"
 msgstr "Esta definición de relación final:"
 
-msgid "This CWCache:"
-msgstr "Este Caché:"
-
 msgid "This CWComputedRType:"
 msgstr ""
 
@@ -983,18 +971,6 @@
 msgid "a number (in seconds) or 20s, 10min, 24h or 4d are expected"
 msgstr "se espera un número (en segundos) ó 20s, 10min, 24h ó 4d "
 
-msgid ""
-"a simple cache entity characterized by a name and a validity date. The "
-"target application is responsible for updating timestamp when necessary to "
-"invalidate the cache (typically in hooks). Also, checkout the AppObject."
-"get_cache() method."
-msgstr ""
-"un caché simple caracterizado por un nombre y una fecha de validez. Es\n"
-"el código de la instancia quién es responsable de actualizar la fecha de\n"
-"validez mientras el caché debe ser invalidado (en general en un hook).\n"
-"Para recuperar un caché, hace falta utilizar el método\n"
-"get_cache(cachename)."
-
 msgid "abstract base class for transitions"
 msgstr "Clase de base abstracta para la transiciones"
 
@@ -1121,9 +1097,6 @@
 msgid "add a CWAttribute"
 msgstr ""
 
-msgid "add a CWCache"
-msgstr ""
-
 msgid "add a CWComputedRType"
 msgstr ""
 
@@ -2712,9 +2685,6 @@
 msgid "gc"
 msgstr "fuga de memoria"
 
-msgid "generic plot"
-msgstr "Gráfica Genérica"
-
 msgid "generic relation to link one entity to another"
 msgstr "Relación genérica para ligar entidades"
 
@@ -3248,10 +3218,6 @@
 msgid "name"
 msgstr "Nombre"
 
-msgctxt "CWCache"
-msgid "name"
-msgstr "Nombre"
-
 msgctxt "CWComputedRType"
 msgid "name"
 msgstr ""
@@ -3296,9 +3262,6 @@
 msgid "name"
 msgstr "Nombre"
 
-msgid "name of the cache"
-msgstr "Nombre del Caché"
-
 msgid ""
 "name of the main variables which should be used in the selection if "
 "necessary (comma separated)"
@@ -3363,9 +3326,6 @@
 msgid "no related entity"
 msgstr "No posee entidad asociada"
 
-msgid "no repository sessions found"
-msgstr "Ninguna sesión encontrada"
-
 msgid "no selected entities"
 msgstr "No hay entidades seleccionadas"
 
@@ -3418,9 +3378,6 @@
 msgid "open all"
 msgstr "Abrir todos"
 
-msgid "opened sessions"
-msgstr "Sesiones abiertas"
-
 msgid "opened web sessions"
 msgstr "Sesiones Web abiertas"
 
@@ -3831,6 +3788,9 @@
 msgid "severity"
 msgstr "severidad"
 
+msgid "should css be compiled and store in uicache"
+msgstr ""
+
 msgid ""
 "should html fields being edited using fckeditor (a HTML WYSIWYG editor).  "
 "You should also select text/html as default text format to actually get "
@@ -3904,6 +3864,9 @@
 msgid "specifying %s is mandatory"
 msgstr "especificar %s es obligatorio"
 
+msgid "specifying an URL is mandatory"
+msgstr ""
+
 msgid ""
 "start timestamp of the currently in synchronization, or NULL when no "
 "synchronization in progress."
@@ -4096,10 +4059,6 @@
 msgid "the prefered email"
 msgstr "Dirección principal de email"
 
-msgid "the system source has its configuration stored on the file-system"
-msgstr ""
-"el sistema fuente tiene su configuración almacenada en el sistema de archivos"
-
 msgid "there is no next page"
 msgstr "no existe página siguiente"
 
@@ -4122,13 +4081,6 @@
 msgid "thursday"
 msgstr "Jueves"
 
-msgid "timestamp"
-msgstr "Fecha"
-
-msgctxt "CWCache"
-msgid "timestamp"
-msgstr "Válido desde"
-
 msgid "timetable"
 msgstr "Tablero de tiempos"
 
@@ -4655,6 +4607,12 @@
 #~ msgid "Browse by category"
 #~ msgstr "Busca por categoría"
 
+#~ msgid "CWCache"
+#~ msgstr "Cache"
+
+#~ msgid "CWCache_plural"
+#~ msgstr "Caches"
+
 #~ msgid "CWSourceSchemaConfig"
 #~ msgstr "Configuraciones de Esquema de Fuente"
 
@@ -4667,18 +4625,36 @@
 #~ msgid "Entity and relation supported by this source"
 #~ msgstr "Entidades y relaciones aceptadas por esta fuente"
 
+#~ msgid "New CWCache"
+#~ msgstr "Agregar Caché"
+
 #~ msgid "New CWSourceSchemaConfig"
 #~ msgstr "Nueva parte de mapeo de fuente"
 
 #~ msgid "No account? Try public access at %s"
 #~ msgstr "No esta registrado? Use el acceso público en %s"
 
+#~ msgid "This CWCache:"
+#~ msgstr "Este Caché:"
+
 #~ msgid "This CWSourceSchemaConfig:"
 #~ msgstr "Esta parte de mapeo de fuente:"
 
 #~ msgid "You can't change this relation"
 #~ msgstr "Usted no puede modificar esta relación"
 
+#~ msgid ""
+#~ "a simple cache entity characterized by a name and a validity date. The "
+#~ "target application is responsible for updating timestamp when necessary "
+#~ "to invalidate the cache (typically in hooks). Also, checkout the "
+#~ "AppObject.get_cache() method."
+#~ msgstr ""
+#~ "un caché simple caracterizado por un nombre y una fecha de validez. Es\n"
+#~ "el código de la instancia quién es responsable de actualizar la fecha de\n"
+#~ "validez mientras el caché debe ser invalidado (en general en un hook).\n"
+#~ "Para recuperar un caché, hace falta utilizar el método\n"
+#~ "get_cache(cachename)."
+
 #~ msgid "allowed options depends on the source type"
 #~ msgstr "las opciones permitidas dependen del tipo de fuente"
 
@@ -4764,14 +4740,30 @@
 #~ "Un error ha ocurrido al interrogar  %s, es posible que los \n"
 #~ "datos visibles se encuentren incompletos"
 
+#~ msgid "generic plot"
+#~ msgstr "Gráfica Genérica"
+
 #~ msgid "inlined relation %(rtype)s of %(etype)s should be supported"
 #~ msgstr ""
 #~ "la relación %(rtype)s del tipo de entidad %(etype)s debe ser aceptada "
 #~ "('inlined')"
 
+#~ msgctxt "CWCache"
+#~ msgid "name"
+#~ msgstr "Nombre"
+
+#~ msgid "name of the cache"
+#~ msgstr "Nombre del Caché"
+
 #~ msgid "no edited fields specified for entity %s"
 #~ msgstr "Ningún campo editable especificado para la entidad %s"
 
+#~ msgid "no repository sessions found"
+#~ msgstr "Ninguna sesión encontrada"
+
+#~ msgid "opened sessions"
+#~ msgstr "Sesiones abiertas"
+
 #~ msgctxt "CWSourceSchemaConfig"
 #~ msgid "options"
 #~ msgstr "opciones"
@@ -4797,6 +4789,11 @@
 #~ "la relación %s es aceptada pero ninguna de sus definiciones corresponden "
 #~ "a los tipos de entidades aceptadas"
 
+#~ msgid "the system source has its configuration stored on the file-system"
+#~ msgstr ""
+#~ "el sistema fuente tiene su configuración almacenada en el sistema de "
+#~ "archivos"
+
 #~ msgid "the value \"%s\" is already used, use another one"
 #~ msgstr "El valor \"%s\" ya esta en uso, favor de utilizar otro"
 
@@ -4809,6 +4806,13 @@
 #~ msgid "timeline"
 #~ msgstr "Escala de Tiempo"
 
+#~ msgid "timestamp"
+#~ msgstr "Fecha"
+
+#~ msgctxt "CWCache"
+#~ msgid "timestamp"
+#~ msgstr "Válido desde"
+
 #~ msgid "unknown option(s): %s"
 #~ msgstr "opcion(es) desconocida(s): %s"
 
--- a/cubicweb/i18n/fr.po	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/i18n/fr.po	Fri Oct 18 23:39:03 2019 +0200
@@ -202,7 +202,9 @@
 msgstr "Permissions des attributs"
 
 msgid "Authentication failed. Please check your credentials."
-msgstr "Échec de l'authentification. Veuillez vérifier vos identifiant et mot de passe."
+msgstr ""
+"Échec de l'authentification. Veuillez vérifier vos identifiant et mot de "
+"passe."
 
 # schema pot file, generated on 2009-09-16 16:46:55
 #
@@ -256,12 +258,6 @@
 msgid "CWAttribute_plural"
 msgstr "Attributs"
 
-msgid "CWCache"
-msgstr "Cache applicatif"
-
-msgid "CWCache_plural"
-msgstr "Caches applicatifs"
-
 msgid "CWComputedRType"
 msgstr "Relation virtuelle"
 
@@ -546,9 +542,6 @@
 msgid "New CWAttribute"
 msgstr "Nouvelle définition de relation finale"
 
-msgid "New CWCache"
-msgstr "Nouveau cache applicatif"
-
 msgid "New CWComputedRType"
 msgstr "Nouvelle relation virtuelle"
 
@@ -640,7 +633,9 @@
 msgstr "Mots de passe"
 
 msgid "Persistent session. Used by cubicweb.pyramid to store the session data."
-msgstr "Session persistante. Utilisée par cubicweb.pyramid pour stocker les données de session."
+msgstr ""
+"Session persistante. Utilisée par cubicweb.pyramid pour stocker les données "
+"de session."
 
 msgid "Please note that this is only a shallow copy"
 msgstr "Attention, cela n'effectue qu'une copie de surface"
@@ -770,9 +765,6 @@
 msgid "This CWAttribute:"
 msgstr "Cette définition de relation finale :"
 
-msgid "This CWCache:"
-msgstr "Ce cache applicatif :"
-
 msgid "This CWComputedRType:"
 msgstr "Cette relation virtuelle :"
 
@@ -981,18 +973,6 @@
 msgid "a number (in seconds) or 20s, 10min, 24h or 4d are expected"
 msgstr "un nombre (en seconde) ou 20s, 10min, 24h ou 4d sont attendus"
 
-msgid ""
-"a simple cache entity characterized by a name and a validity date. The "
-"target application is responsible for updating timestamp when necessary to "
-"invalidate the cache (typically in hooks). Also, checkout the AppObject."
-"get_cache() method."
-msgstr ""
-"un cache simple caractérisé par un nom et une date de validité. C'est\n"
-"le code de l'instance qui est responsable de mettre à jour la date de\n"
-"validité lorsque le cache doit être invalidé (en général dans un hook).\n"
-"Pour récupérer un cache, il faut utiliser utiliser la méthode\n"
-"get_cache(cachename)."
-
 msgid "abstract base class for transitions"
 msgstr "classe de base abstraite pour les transitions"
 
@@ -1119,9 +1099,6 @@
 msgid "add a CWAttribute"
 msgstr ""
 
-msgid "add a CWCache"
-msgstr ""
-
 msgid "add a CWComputedRType"
 msgstr ""
 
@@ -2711,9 +2688,6 @@
 msgid "gc"
 msgstr "fuite mémoire"
 
-msgid "generic plot"
-msgstr "tracé de courbes standard"
-
 msgid "generic relation to link one entity to another"
 msgstr "relation générique pour lier une entité à une autre"
 
@@ -3247,10 +3221,6 @@
 msgid "name"
 msgstr "nom"
 
-msgctxt "CWCache"
-msgid "name"
-msgstr "nom"
-
 msgctxt "CWComputedRType"
 msgid "name"
 msgstr "nom"
@@ -3295,9 +3265,6 @@
 msgid "name"
 msgstr "nom"
 
-msgid "name of the cache"
-msgstr "nom du cache applicatif"
-
 msgid ""
 "name of the main variables which should be used in the selection if "
 "necessary (comma separated)"
@@ -3362,9 +3329,6 @@
 msgid "no related entity"
 msgstr "pas d'entité liée"
 
-msgid "no repository sessions found"
-msgstr "aucune session trouvée"
-
 msgid "no selected entities"
 msgstr "pas d'entité sélectionnée"
 
@@ -3417,9 +3381,6 @@
 msgid "open all"
 msgstr "tout ouvrir"
 
-msgid "opened sessions"
-msgstr "sessions ouvertes"
-
 msgid "opened web sessions"
 msgstr "sessions web ouvertes"
 
@@ -3833,6 +3794,9 @@
 msgid "severity"
 msgstr "sévérité"
 
+msgid "should css be compiled and store in uicache"
+msgstr ""
+
 msgid ""
 "should html fields being edited using fckeditor (a HTML WYSIWYG editor).  "
 "You should also select text/html as default text format to actually get "
@@ -3906,6 +3870,9 @@
 msgid "specifying %s is mandatory"
 msgstr "spécifier %s est obligatoire"
 
+msgid "specifying an URL is mandatory"
+msgstr ""
+
 msgid ""
 "start timestamp of the currently in synchronization, or NULL when no "
 "synchronization in progress."
@@ -4097,9 +4064,6 @@
 msgid "the prefered email"
 msgstr "l'adresse électronique principale"
 
-msgid "the system source has its configuration stored on the file-system"
-msgstr "la source système a sa configuration stockée sur le système de fichier"
-
 msgid "there is no next page"
 msgstr "Il n'y a pas de page suivante"
 
@@ -4123,13 +4087,6 @@
 msgid "thursday"
 msgstr "jeudi"
 
-msgid "timestamp"
-msgstr "date"
-
-msgctxt "CWCache"
-msgid "timestamp"
-msgstr "valide depuis"
-
 msgid "timetable"
 msgstr "emploi du temps"
 
@@ -4628,3 +4585,54 @@
 
 msgid "you should probably delete that property"
 msgstr "vous devriez probablement supprimer cette propriété"
+
+#~ msgid "CWCache"
+#~ msgstr "Cache applicatif"
+
+#~ msgid "CWCache_plural"
+#~ msgstr "Caches applicatifs"
+
+#~ msgid "New CWCache"
+#~ msgstr "Nouveau cache applicatif"
+
+#~ msgid "This CWCache:"
+#~ msgstr "Ce cache applicatif :"
+
+#~ msgid ""
+#~ "a simple cache entity characterized by a name and a validity date. The "
+#~ "target application is responsible for updating timestamp when necessary "
+#~ "to invalidate the cache (typically in hooks). Also, checkout the "
+#~ "AppObject.get_cache() method."
+#~ msgstr ""
+#~ "un cache simple caractérisé par un nom et une date de validité. C'est\n"
+#~ "le code de l'instance qui est responsable de mettre à jour la date de\n"
+#~ "validité lorsque le cache doit être invalidé (en général dans un hook).\n"
+#~ "Pour récupérer un cache, il faut utiliser utiliser la méthode\n"
+#~ "get_cache(cachename)."
+
+#~ msgid "generic plot"
+#~ msgstr "tracé de courbes standard"
+
+#~ msgctxt "CWCache"
+#~ msgid "name"
+#~ msgstr "nom"
+
+#~ msgid "name of the cache"
+#~ msgstr "nom du cache applicatif"
+
+#~ msgid "no repository sessions found"
+#~ msgstr "aucune session trouvée"
+
+#~ msgid "opened sessions"
+#~ msgstr "sessions ouvertes"
+
+#~ msgid "the system source has its configuration stored on the file-system"
+#~ msgstr ""
+#~ "la source système a sa configuration stockée sur le système de fichier"
+
+#~ msgid "timestamp"
+#~ msgstr "date"
+
+#~ msgctxt "CWCache"
+#~ msgid "timestamp"
+#~ msgstr "valide depuis"
--- a/cubicweb/mail.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/mail.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,8 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Common utilies to format / send emails."""
 
-
-
 from base64 import b64encode, b64decode
 from time import time
 from email.mime.multipart import MIMEMultipart
@@ -28,21 +26,13 @@
 from email.utils import formatdate
 from socket import gethostname
 
-from six import PY2, PY3, text_type
-
 
 def header(ustring):
-    if PY3:
-        return Header(ustring, 'utf-8')
-    return Header(ustring.encode('UTF-8'), 'UTF-8')
+    return Header(ustring, 'utf-8')
 
-def addrheader(uaddr, uname=None):
+def addrheader(addr, uname=None):
     # even if an email address should be ascii, encode it using utf8 since
     # automatic tests may generate non ascii email address
-    if PY2:
-        addr = uaddr.encode('UTF-8')
-    else:
-        addr = uaddr
     if uname:
         val = '%s <%s>' % (header(uname).encode(), addr)
     else:
@@ -86,7 +76,7 @@
     to_addrs and cc_addrs are expected to be a list of email address without
     name
     """
-    assert isinstance(content, text_type), repr(content)
+    assert isinstance(content, str), repr(content)
     msg = MIMEText(content.encode('UTF-8'), 'plain', 'UTF-8')
     # safety: keep only the first newline
     try:
@@ -97,13 +87,13 @@
     if uinfo.get('email'):
         email = uinfo['email']
     elif config and config['sender-addr']:
-        email = text_type(config['sender-addr'])
+        email = config['sender-addr']
     else:
         email = u''
     if uinfo.get('name'):
         name = uinfo['name']
     elif config and config['sender-name']:
-        name = text_type(config['sender-name'])
+        name = config['sender-name']
     else:
         name = u''
     msg['From'] = addrheader(email, name)
--- a/cubicweb/md5crypt.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/md5crypt.py	Fri Oct 18 23:39:03 2019 +0200
@@ -43,9 +43,6 @@
 
 from hashlib import md5 # pylint: disable=E0611
 
-from six import text_type, indexbytes
-from six.moves import range
-
 
 def to64 (v, n):
     ret = bytearray()
@@ -56,9 +53,9 @@
     return ret
 
 def crypt(pw, salt):
-    if isinstance(pw, text_type):
+    if isinstance(pw, str):
         pw = pw.encode('utf-8')
-    if isinstance(salt, text_type):
+    if isinstance(salt, str):
         salt = salt.encode('ascii')
     # Take care of the magic string if present
     if salt.startswith(MAGIC):
@@ -102,20 +99,20 @@
         final = md5(ctx1).digest()
     # Final xform
     passwd = b''
-    passwd += to64((indexbytes(final, 0) << 16)
-                   |(indexbytes(final, 6) << 8)
-                   |(indexbytes(final, 12)),4)
-    passwd += to64((indexbytes(final, 1) << 16)
-                   |(indexbytes(final, 7) << 8)
-                   |(indexbytes(final, 13)), 4)
-    passwd += to64((indexbytes(final, 2) << 16)
-                   |(indexbytes(final, 8) << 8)
-                   |(indexbytes(final, 14)), 4)
-    passwd += to64((indexbytes(final, 3) << 16)
-                   |(indexbytes(final, 9) << 8)
-                   |(indexbytes(final, 15)), 4)
-    passwd += to64((indexbytes(final, 4) << 16)
-                   |(indexbytes(final, 10) << 8)
-                   |(indexbytes(final, 5)), 4)
-    passwd += to64((indexbytes(final, 11)), 2)
+    passwd += to64((final[0] << 16)
+                   |(final[6] << 8)
+                   |(final[12]),4)
+    passwd += to64((final[1] << 16)
+                   |(final[7] << 8)
+                   |(final[13]), 4)
+    passwd += to64((final[2] << 16)
+                   |(final[8] << 8)
+                   |(final[14]), 4)
+    passwd += to64((final[3] << 16)
+                   |(final[9] << 8)
+                   |(final[15]), 4)
+    passwd += to64((final[4] << 16)
+                   |(final[10] << 8)
+                   |(final[5]), 4)
+    passwd += to64((final[11]), 2)
     return passwd
--- a/cubicweb/migration.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/migration.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,28 +16,23 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """utilities for instances migration"""
-from __future__ import print_function
-
-
 
 import sys
 import os
+import string
 import logging
 import tempfile
+import itertools
 from os.path import exists, join, basename, splitext
 from itertools import chain
-from warnings import warn
-
-from six import string_types
 
 from logilab.common import IGNORED_EXTENSIONS
 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 logilab.common.deprecation import deprecated
 
-from cubicweb import ConfigurationError, ExecutionError
+from cubicweb import ConfigurationError, ExecutionError, utils
 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
 from cubicweb.toolsutils import show_diffs
 
@@ -205,7 +200,7 @@
             return meth(*args, **kwargs)
 
     def confirm(self, question, # pylint: disable=E0202
-                shell=True, abort=True, retry=False, pdb=False, default='y'):
+                shell=True, abort=True, retry=False, pdb=False, default='y', traceback=None):
         """ask for confirmation and return true on positive answer
 
         if `retry` is true the r[etry] answer may return 2
@@ -231,11 +226,14 @@
             raise SystemExit(1)
         if answer == 'shell':
             self.interactive_shell()
-            return self.confirm(question, shell, abort, retry, pdb, default)
+            return self.confirm(question, shell, abort, retry, pdb, default, traceback)
         if answer == 'pdb':
-            import pdb
-            pdb.set_trace()
-            return self.confirm(question, shell, abort, retry, pdb, default)
+            pdb = utils.get_pdb()
+            if traceback:
+                pdb.post_mortem(traceback)
+            else:
+                pdb.set_trace()
+            return self.confirm(question, shell, abort, retry, pdb, default, traceback)
         return True
 
     def interactive_shell(self):
@@ -269,11 +267,20 @@
         banner = """entering the migration python shell
 just type migration commands or arbitrary python code and type ENTER to execute it
 type "exit" or Ctrl-D to quit the shell and resume operation"""
-        interact(banner, local=local_ctx)
+
+        # use ipython if available
         try:
-            readline.write_history_file(histfile)
-        except IOError:
-            pass
+            from IPython import start_ipython
+            print(banner)
+            start_ipython(argv=[], user_ns=local_ctx)
+        except ImportError:
+            interact(banner, local=local_ctx)
+
+            try:
+                readline.write_history_file(histfile)
+            except IOError:
+                pass
+
         # delete instance's confirm attribute to avoid questions
         del self.confirm
         self.need_wrap = True
@@ -349,13 +356,7 @@
             scriptlocals['__name__'] = pyname
             with open(migrscript, 'rb') as fobj:
                 fcontent = fobj.read()
-            try:
-                code = compile(fcontent, migrscript, 'exec')
-            except SyntaxError:
-                # try without print_function
-                code = compile(fcontent, migrscript, 'exec', 0, True)
-                warn('[3.22] script %r should be updated to work with print_function'
-                     % migrscript, DeprecationWarning)
+            code = compile(fcontent, migrscript, 'exec')
             exec(code, scriptlocals)
             if funcname is not None:
                 try:
@@ -406,7 +407,7 @@
         """modify the list of used cubes in the in-memory config
         returns newly inserted cubes, including dependencies
         """
-        if isinstance(cubes, string_types):
+        if isinstance(cubes, str):
             cubes = (cubes,)
         origcubes = self.config.cubes()
         newcubes = [p for p in self.config.expand_cubes(cubes)
@@ -415,10 +416,6 @@
             self.config.add_cubes(newcubes)
         return newcubes
 
-    @deprecated('[3.20] use drop_cube() instead of remove_cube()')
-    def cmd_remove_cube(self, cube, removedeps=False):
-        return self.cmd_drop_cube(cube, removedeps)
-
     def cmd_drop_cube(self, cube, removedeps=False):
         if removedeps:
             toremove = self.config.expand_cubes([cube])
@@ -474,6 +471,14 @@
 def max_version(a, b):
     return str(max(Version(a), Version(b)))
 
+
+def split_constraint(constraint):
+    oper = itertools.takewhile(lambda x: x in "<>=", constraint)
+    version = itertools.dropwhile(lambda x: x not in string.digits + ".", constraint)
+
+    return "".join(oper), "".join(version)
+
+
 class ConfigurationProblem(object):
     """Each cube has its own list of dependencies on other cubes/versions.
 
@@ -507,7 +512,7 @@
                 self.reverse_dependencies.setdefault(name,set())
                 if constraint:
                     try:
-                        oper, version = constraint.split()
+                        oper, version = split_constraint(constraint)
                         self.reverse_dependencies[name].add( (oper, version, cube) )
                     except Exception:
                         self.warnings.append(
--- a/cubicweb/misc/migration/3.10.0_Any.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/misc/migration/3.10.0_Any.py	Fri Oct 18 23:39:03 2019 +0200
@@ -1,5 +1,3 @@
-from six import text_type
-
 add_entity_type('CWSource')
 add_relation_definition('CWSource', 'cw_source', 'CWSource')
 add_entity_type('CWSourceHostConfig')
@@ -18,7 +16,7 @@
         continue
     config = u'\n'.join('%s=%s' % (key, value) for key, value in cfg.items()
                         if key != 'adapter' and value is not None)
-    create_entity('CWSource', name=text_type(uri), type=text_type(cfg['adapter']),
+    create_entity('CWSource', name=uri, type=cfg['adapter'],
                   config=config)
 commit()
 
--- a/cubicweb/misc/migration/3.10.9_Any.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/misc/migration/3.10.9_Any.py	Fri Oct 18 23:39:03 2019 +0200
@@ -7,7 +7,6 @@
 
 if confirm('fix existing cwuri?'):
     from logilab.common.shellutils import progress
-    from cubicweb.server.session import hooks_control
     rset = rql('Any X, XC WHERE X cwuri XC, X cwuri ~= "%/eid/%"')
     title = "%i entities to fix" % len(rset)
     nbops = rset.rowcount
--- a/cubicweb/misc/migration/3.13.8_Any.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/misc/migration/3.13.8_Any.py	Fri Oct 18 23:39:03 2019 +0200
@@ -1,4 +1,3 @@
-change_attribute_type('CWCache', 'timestamp', 'TZDatetime')
 change_attribute_type('CWUser', 'last_login_time', 'TZDatetime')
 change_attribute_type('CWSource', 'latest_retrieval', 'TZDatetime')
 drop_attribute('CWSource', 'synchronizing')
--- a/cubicweb/misc/migration/3.15.0_Any.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/misc/migration/3.15.0_Any.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,7 +16,7 @@
             sconfig.set_option(opt, val)
         except OptionError:
             continue
-    cfgstr = text_type(generate_source_config(sconfig), source._cw.encoding)
+    cfgstr = str(generate_source_config(sconfig), source._cw.encoding)
     source.cw_set(config=cfgstr)
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/misc/migration/3.27.0_Any.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,4 @@
+option_removed('host')
+option_removed('uid')
+option_removed('webserver-threadpool-size')
+drop_entity_type('CWCache')
--- a/cubicweb/misc/migration/bootstrapmigration_repository.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/misc/migration/bootstrapmigration_repository.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,13 +19,7 @@
 
 it should only include low level schema changes
 """
-
-from __future__ import print_function
-
-from six import text_type
-
 from cubicweb import ConfigurationError
-from cubicweb.server.session import hooks_control
 from cubicweb.server import schemaserial as ss
 
 applcubicwebversion, cubicwebversion = versions_map['cubicweb']
@@ -120,7 +114,6 @@
                 default = yams.DATE_FACTORY_MAP[atype](default)
         else:
             assert atype == 'String', atype
-            default = text_type(default)
         return Binary.zpickle(default)
 
     dbh = repo.system_source.dbhelper
@@ -267,7 +260,7 @@
     session.set_cnxset()
     permsdict = ss.deserialize_ertype_permissions(session)
 
-    with hooks_control(session, session.HOOKS_ALLOW_ALL, 'integrity'):
+    with session.allow_all_hooks_but('integrity'):
         for rschema in repo.schema.relations():
             rpermsdict = permsdict.get(rschema.eid, {})
             for rdef in rschema.rdefs.values():
--- a/cubicweb/misc/migration/postcreate.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/misc/migration/postcreate.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,19 +16,15 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """cubicweb post creation script, set user's workflow"""
-from __future__ import print_function
-
-from six import text_type
-
 from cubicweb import _
 
 
 # insert versions
 create_entity('CWProperty', pkey=u'system.version.cubicweb',
-              value=text_type(config.cubicweb_version()))
+              value=str(config.cubicweb_version()))
 for cube in config.cubes():
     create_entity('CWProperty', pkey=u'system.version.%s' % cube.lower(),
-                  value=text_type(config.cube_version(cube)))
+                  value=str(config.cube_version(cube)))
 
 # some entities have been added before schema entities, add their missing 'is' and
 # 'is_instance_of' relations
@@ -56,7 +52,7 @@
         print('Hopefully this is not a production instance...')
     elif anonlogin:
         from cubicweb.server import create_user
-        create_user(session, text_type(anonlogin), anonpwd, u'guests')
+        create_user(session, anonlogin, anonpwd, u'guests')
 
 # need this since we already have at least one user in the database (the default admin)
 for user in rql('Any X WHERE X is CWUser').entities():
--- a/cubicweb/misc/scripts/migration_helper.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/misc/scripts/migration_helper.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,9 +19,6 @@
 """Helper functions for migrations that aren't reliable enough or too dangerous
 to be available in the standard migration environment
 """
-from __future__ import print_function
-
-
 
 def drop_entity_types_fast(*etypes, **kwargs):
     """drop an entity type bypassing all hooks
--- a/cubicweb/misc/scripts/repair_file_1-9_migration.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-"""execute this script if you've migration to file >= 1.9.0 with cubicweb <= 3.9.2
-
-FYI, this migration occurred :
-* on our intranet on July 07 2010
-* on our extranet on July 16 2010
-"""
-from __future__ import print_function
-
-try:
-    backupinstance, = __args__
-except ValueError:
-    print('USAGE: cubicweb-ctl shell <instance> repair_file_1-9_migration.py -- <backup instance id>')
-    print()
-    print('you should restored the backup on a new instance, accessible through pyro')
-
-from cubicweb import cwconfig, dbapi
-from cubicweb.server.session import hooks_control
-
-defaultadmin = repo.config.default_admin_config
-backupcfg = cwconfig.instance_configuration(backupinstance)
-backupcfg.repairing = True
-backuprepo, backupcnx = dbapi.in_memory_repo_cnx(backupcfg, defaultadmin['login'],
-                                                 password=defaultadmin['password'],
-                                                 host='localhost')
-backupcu = backupcnx.cursor()
-
-with hooks_control(session, session.HOOKS_DENY_ALL):
-    rql('SET X is Y WHERE X is File, Y name "File", NOT X is Y')
-    rql('SET X is_instance_of Y WHERE X is File, Y name "File", NOT X is_instance_of Y')
-    for rtype, in backupcu.execute('DISTINCT Any RTN WHERE X relation_type RT, RT name RTN,'
-                                   'X from_entity Y, Y name "Image", X is CWRelation, '
-                                   'EXISTS(XX is CWRelation, XX relation_type RT, '
-                                   'XX from_entity YY, YY name "File")'):
-        if rtype in ('is', 'is_instance_of'):
-            continue
-        print(rtype)
-        for feid, xeid in backupcu.execute('Any F,X WHERE F %s X, F is IN (File,Image)' % rtype):
-            print('restoring relation %s between file %s and %s' % (rtype, feid, xeid), end=' ')
-            print(rql('SET F %s X WHERE F eid %%(f)s, X eid %%(x)s, NOT F %s X' % (rtype, rtype),
-                      {'f': feid, 'x': xeid}))
-
-    for rtype, in backupcu.execute('DISTINCT Any RTN WHERE X relation_type RT, RT name RTN,'
-                                   'X to_entity Y, Y name "Image", X is CWRelation, '
-                                   'EXISTS(XX is CWRelation, XX relation_type RT, '
-                                   'XX to_entity YY, YY name "File")'):
-        print(rtype)
-        for feid, xeid in backupcu.execute('Any F,X WHERE X %s F, F is IN (File,Image)' % rtype):
-            print('restoring relation %s between %s and file %s' % (rtype, xeid, feid), end=' ')
-            print(rql('SET X %s F WHERE F eid %%(f)s, X eid %%(x)s, NOT X %s F' % (rtype, rtype),
-                      {'f': feid, 'x': xeid}))
-
-commit()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/misc/source_highlight.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,21 @@
+"""This module provide syntaxe highlight functions"""
+
+from logilab.common.logging_ext import _colorable_terminal
+
+try:
+    from pygments import highlight as pygments_highlight
+    from pygments.lexers import get_lexer_by_name
+    from pygments.formatters.terminal import TerminalFormatter
+    has_pygments = True
+except ImportError:
+    has_pygments = False
+
+
+def highlight(code, language):
+    if not has_pygments:
+        return code
+
+    if not _colorable_terminal():
+        return code
+
+    return pygments_highlight(code, get_lexer_by_name(language), TerminalFormatter())
--- a/cubicweb/multipart.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/multipart.py	Fri Oct 18 23:39:03 2019 +0200
@@ -37,16 +37,12 @@
 __version__ = '0.1'
 __license__ = 'MIT'
 
+from io import BytesIO
 from tempfile import TemporaryFile
+from urllib.parse import parse_qs
 from wsgiref.headers import Headers
 import re, sys
-try:
-    from io import BytesIO
-except ImportError: # pragma: no cover (fallback for Python 2.5)
-    from StringIO import StringIO as BytesIO
 
-from six import PY3, text_type
-from six.moves.urllib.parse import parse_qs
 
 ##############################################################################
 ################################ Helper & Misc ################################
@@ -54,7 +50,7 @@
 # Some of these were copied from bottle: http://bottle.paws.de/
 
 try:
-    from collections import MutableMapping as DictMixin
+    from collections.abc import MutableMapping as DictMixin
 except ImportError: # pragma: no cover (fallback for Python 2.5)
     from UserDict import DictMixin
 
@@ -88,7 +84,7 @@
                 yield key, value
 
 def tob(data, enc='utf8'): # Convert strings to bytes (py2 and py3)
-    return data.encode(enc) if isinstance(data, text_type) else data
+    return data.encode(enc) if isinstance(data, str) else data
 
 def copy_file(stream, target, maxread=-1, buffer_size=2*16):
     ''' Read from :stream and write to :target until :maxread or EOF. '''
@@ -105,10 +101,10 @@
 ##############################################################################
 
 _special = re.escape('()<>@,;:\\"/[]?={} \t')
-_re_special = re.compile('[%s]' % _special)
+_re_special = re.compile(r'[%s]' % _special)
 _qstr = '"(?:\\\\.|[^"])*"' # Quoted string
 _value = '(?:[^%s]+|%s)' % (_special, _qstr) # Save or quoted string
-_option = '(?:;|^)\s*([^%s]+)\s*=\s*(%s)' % (_special, _value)
+_option = r'(?:;|^)\s*([^%s]+)\s*=\s*(%s)' % (_special, _value)
 _re_option = re.compile(_option) # key=value part of an Content-Type like header
 
 def header_quote(val):
@@ -400,15 +396,11 @@
             data = stream.read(mem_limit)
             if stream.read(1): # These is more that does not fit mem_limit
                 raise MultipartError("Request too big. Increase MAXMEM.")
-            if PY3:
-                data = data.decode('ascii')
+            data = data.decode('ascii')
             data = parse_qs(data, keep_blank_values=True)
             for key, values in data.items():
                 for value in values:
-                    if PY3:
-                        forms[key] = value
-                    else:
-                        forms[key.decode(charset)] = value.decode(charset)
+                    forms[key] = value
         else:
             raise MultipartError("Unsupported content type.")
     except MultipartError:
--- a/cubicweb/predicates.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/predicates.py	Fri Oct 18 23:39:03 2019 +0200
@@ -24,11 +24,7 @@
 from warnings import warn
 from operator import eq
 
-from six import string_types, integer_types
-from six.moves import range
-
-from logilab.common.deprecation import deprecated
-from logilab.common.registry import Predicate, objectify_predicate, yes
+from logilab.common.registry import Predicate, objectify_predicate
 
 from yams.schema import BASE_TYPES, role_name
 from rql.nodes import Function
@@ -38,8 +34,6 @@
 from cubicweb.uilib import eid_param
 from cubicweb.schema import split_expression
 
-yes = deprecated('[3.15] import yes() from use logilab.common.registry')(yes)
-
 
 # abstract predicates / mixin helpers ###########################################
 
@@ -85,12 +79,7 @@
       - `accept_none` is False and some cell in the column has a None value
         (this may occurs with outer join)
     """
-    def __init__(self, once_is_enough=None, accept_none=True, mode='all'):
-        if once_is_enough is not None:
-            warn("[3.14] once_is_enough is deprecated, use mode='any'",
-                 DeprecationWarning, stacklevel=2)
-            if once_is_enough:
-                mode = 'any'
+    def __init__(self, accept_none=True, mode='all'):
         assert mode in ('any', 'all'), 'bad mode %s' % mode
         self.once_is_enough = mode == 'any'
         self.accept_none = accept_none
@@ -618,7 +607,7 @@
         super(is_instance, self).__init__(**kwargs)
         self.expected_etypes = expected_etypes
         for etype in self.expected_etypes:
-            assert isinstance(etype, string_types), etype
+            assert isinstance(etype, str), etype
 
     def __str__(self):
         return '%s(%s)' % (self.__class__.__name__,
@@ -672,13 +661,13 @@
     See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity
     lookup / score rules according to the input context.
     """
-    def __init__(self, scorefunc, once_is_enough=None, mode='all'):
-        super(score_entity, self).__init__(mode=mode, once_is_enough=once_is_enough)
+    def __init__(self, scorefunc, mode='all'):
+        super(score_entity, self).__init__(mode=mode)
         def intscore(*args, **kwargs):
             score = scorefunc(*args, **kwargs)
             if not score:
                 return 0
-            if isinstance(score, integer_types):
+            if isinstance(score, int):
                 return score
             return 1
         self.score_entity = intscore
@@ -690,8 +679,8 @@
     You can give 'image/' to match any image for instance, or 'image/png' to match
     only PNG images.
     """
-    def __init__(self, mimetype, once_is_enough=None, mode='all'):
-        super(has_mimetype, self).__init__(mode=mode, once_is_enough=once_is_enough)
+    def __init__(self, mimetype, mode='all'):
+        super(has_mimetype, self).__init__(mode=mode)
         self.mimetype = mimetype
 
     def score_entity(self, entity):
@@ -995,8 +984,8 @@
     See :class:`~cubicweb.predicates.EntityPredicate` documentation for entity
     lookup / score rules according to the input context.
     """
-    def __init__(self, expression, once_is_enough=None, mode='all', user_condition=False):
-        super(rql_condition, self).__init__(mode=mode, once_is_enough=once_is_enough)
+    def __init__(self, expression, mode='all', user_condition=False):
+        super(rql_condition, self).__init__(mode=mode)
         self.user_condition = user_condition
         if user_condition:
             rql = 'Any COUNT(U) WHERE U eid %%(u)s, %s' % expression
@@ -1084,7 +1073,7 @@
                            ','.join(str(s) for s in self.expected))
 
 
-def on_fire_transition(etype, tr_names, from_state_name=None):
+def on_fire_transition(etype, tr_names):
     """Return 1 when entity of the type `etype` is going through transition of
     a name included in `tr_names`.
 
@@ -1096,9 +1085,7 @@
 
     See :class:`cubicweb.entities.wfobjs.TrInfo` for more information.
     """
-    if from_state_name is not None:
-        warn("on_fire_transition's from_state_name argument is unused", DeprecationWarning)
-    if isinstance(tr_names, string_types):
+    if isinstance(tr_names, str):
         tr_names = set((tr_names,))
     def match_etype_and_transition(trinfo):
         # take care trinfo.transition is None when calling change_state
@@ -1298,7 +1285,7 @@
             raise ValueError("match_form_params() can't be called with both "
                              "positional and named arguments")
         if expected:
-            if len(expected) == 1 and not isinstance(expected[0], string_types):
+            if len(expected) == 1 and not isinstance(expected[0], str):
                 raise ValueError("match_form_params() positional arguments "
                                  "must be strings")
             super(match_form_params, self).__init__(*expected)
@@ -1391,8 +1378,8 @@
      is_instance('Version') & (match_transition('ready') |
                                attribute_edited('publication_date'))
     """
-    def __init__(self, attribute, once_is_enough=None, mode='all'):
-        super(attribute_edited, self).__init__(mode=mode, once_is_enough=once_is_enough)
+    def __init__(self, attribute, mode='all'):
+        super(attribute_edited, self).__init__(mode=mode)
         self._attribute = attribute
 
     def score_entity(self, entity):
--- a/cubicweb/pyramid/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/pyramid/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,10 +21,10 @@
 """Pyramid interface to CubicWeb"""
 
 import atexit
+from configparser import ConfigParser
 import os
 import warnings
 
-from six.moves.configparser import SafeConfigParser
 import wsgicors
 
 from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
@@ -72,7 +72,7 @@
 
     for fname in settings_filenames:
         if os.path.exists(fname):
-            cp = SafeConfigParser()
+            cp = ConfigParser()
             cp.read(fname)
             settings.update(cp.items('main'))
             break
--- a/cubicweb/pyramid/bwcompat.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/pyramid/bwcompat.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,6 +21,7 @@
 """Backward compatibility layer for CubicWeb to run as a Pyramid application."""
 
 import sys
+import inspect
 import logging
 
 from pyramid import security
@@ -88,7 +89,12 @@
                     try:
                         controller = vreg['controllers'].select(
                             ctrlid, req, appli=self.appli)
+                        log.info("REQUEST [%s] '%s' selected controller %s at %s:%s",
+                                 ctrlid, req.path, controller,
+                                 inspect.getsourcefile(controller.__class__),
+                                 inspect.getsourcelines(controller.__class__)[1])
                     except cubicweb.NoSelectableObject:
+                        log.warn("WARNING '%s' unauthorized request", req.path)
                         raise httpexceptions.HTTPUnauthorized(
                             req._('not authorized'))
 
--- a/cubicweb/pyramid/config.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/pyramid/config.py	Fri Oct 18 23:39:03 2019 +0200
@@ -26,7 +26,7 @@
 from cubicweb.cwconfig import CONFIGURATIONS
 from cubicweb.server.serverconfig import ServerConfiguration
 from cubicweb.toolsutils import fill_templated_file
-from cubicweb.web.webconfig import BaseWebConfiguration
+from cubicweb.web.webconfig import BaseWebConfiguration, WebConfigurationBase
 
 
 def get_random_secret_key():
@@ -69,3 +69,29 @@
 
 
 CONFIGURATIONS.append(CubicWebPyramidConfiguration)
+
+
+class AllInOneConfiguration(WebConfigurationBase, ServerConfiguration):
+    """repository and web instance in the same Pyramid process"""
+    name = 'all-in-one'
+    options = merge_options((
+        ('profile',
+         {'type': 'string',
+          'default': None,
+          'help': 'profile code and use the specified file to store stats if this option is set',
+          'group': 'web', 'level': 3,
+          }),
+    ) + WebConfigurationBase.options + ServerConfiguration.options
+    )
+
+    cubicweb_appobject_path = (
+        WebConfigurationBase.cubicweb_appobject_path
+        | ServerConfiguration.cubicweb_appobject_path
+    )
+    cube_appobject_path = (
+        WebConfigurationBase.cube_appobject_path
+        | ServerConfiguration.cube_appobject_path
+    )
+
+
+CONFIGURATIONS.append(AllInOneConfiguration)
--- a/cubicweb/pyramid/core.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/pyramid/core.py	Fri Oct 18 23:39:03 2019 +0200
@@ -23,7 +23,6 @@
 import itertools
 
 from contextlib import contextmanager
-from warnings import warn
 from cgi import FieldStorage
 
 import rql
@@ -124,11 +123,6 @@
         assert 300 <= ex.status < 400
         raise httpexceptions.status_map[ex.status](
             ex.location, headers=cw_headers(request))
-    except cubicweb.web.StatusResponse as ex:
-        warn('[3.16] StatusResponse is deprecated use req.status_out',
-             DeprecationWarning, stacklevel=2)
-        request.body = ex.content
-        request.status_int = ex.status
     except cubicweb.web.Unauthorized:
         raise httpexceptions.HTTPForbidden(
             request.cw_request._(
@@ -177,15 +171,6 @@
                 val = (val.filename, val.file)
             if param == '_cwmsgid':
                 self.set_message_id(val)
-            elif param == '__message':
-                warn('[3.13] __message in request parameter is deprecated '
-                     '(may only be given to .build_url). Seeing this message '
-                     'usualy means your application hold some <form> where '
-                     'you should replace use of __message hidden input by '
-                     'form.set_message, so new _cwmsgid mechanism is properly '
-                     'used',
-                     DeprecationWarning)
-                self.set_message(val)
             else:
                 self.form[param] = val
 
@@ -413,12 +398,7 @@
 
     cwcfg = config.registry['cubicweb.config']
     for cube in cwcfg.cubes():
-        try:
-            pkgname = 'cubicweb_{}'.format(cube)
-            mod = __import__(pkgname)
-        except ImportError:
-            pkgname = 'cubes.{}'.format(cube)
-            mod = __import__(pkgname)
-            mod = getattr(mod, cube)
+        pkgname = 'cubicweb_{}'.format(cube)
+        mod = __import__(pkgname)
         if hasattr(mod, 'includeme'):
             config.include(pkgname)
--- a/cubicweb/pyramid/profile.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/pyramid/profile.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,7 +21,6 @@
 """ Tools for profiling.
 
 See :ref:`profiling`."""
-from __future__ import print_function
 
 import cProfile
 import itertools
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/pyramid/pyramid.ini.tmpl	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,7 @@
+[main]
+
+cubicweb.session.secret = %(secret_1)s
+cubicweb.auth.authtkt.session.secret = %(secret_2)s
+cubicweb.auth.authtkt.persistent.secret = %(secret_3)s
+cubicweb.auth.authtkt.session.secure = no
+cubicweb.auth.authtkt.persistent.secure = no
--- a/cubicweb/pyramid/pyramidctl.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/pyramid/pyramidctl.py	Fri Oct 18 23:39:03 2019 +0200
@@ -25,31 +25,36 @@
 the pyramid script 'pserve'.
 """
 
-from __future__ import print_function
-
-import atexit
-import errno
 import os
 import signal
 import sys
 import time
 import threading
 import subprocess
-import warnings
 
-from cubicweb import ExecutionError
-from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
+from logilab.common.configuration import merge_options
+
 from cubicweb.cwctl import CWCTL, InstanceCommand, init_cmdline_log_threshold
 from cubicweb.pyramid import wsgi_application_from_cwconfig
-from cubicweb.server import serverctl, set_debug
+from cubicweb.pyramid.config import get_random_secret_key
+from cubicweb.server import serverctl
 from cubicweb.web.webctl import WebCreateHandler
+from cubicweb.toolsutils import fill_templated_file
 
 import waitress
 
 MAXFD = 1024
 
-DBG_FLAGS = ('RQL', 'SQL', 'REPO', 'HOOKS', 'OPS', 'SEC', 'MORE', 'ALL')
-LOG_LEVELS = ('debug', 'info', 'warning', 'error')
+
+def _generate_pyramid_ini_file(pyramid_ini_path):
+    """Write a 'pyramid.ini' file into apphome."""
+    template_fpath = os.path.join(os.path.dirname(__file__), 'pyramid.ini.tmpl')
+    context = {
+        'secret_1': get_random_secret_key(),
+        'secret_2': get_random_secret_key(),
+        'secret_3': get_random_secret_key(),
+    }
+    fill_templated_file(template_fpath, pyramid_ini_path, context)
 
 
 class PyramidCreateHandler(serverctl.RepositoryCreateHandler,
@@ -63,6 +68,20 @@
         self.config.write_development_ini(cubes)
 
 
+class AllInOneCreateHandler(serverctl.RepositoryCreateHandler,
+                            WebCreateHandler):
+    """configuration to get an instance running in a Pyramid web server
+    integrating a repository server in the same process
+    """
+    cfgname = 'all-in-one'
+
+    def bootstrap(self, cubes, automatic=False, inputlevel=0):
+        """bootstrap this configuration"""
+        serverctl.RepositoryCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
+        WebCreateHandler.bootstrap(self, cubes, automatic, inputlevel)
+        _generate_pyramid_ini_file(os.path.join(self.config.apphome, "pyramid.ini"))
+
+
 class PyramidStartHandler(InstanceCommand):
     """Start an interactive pyramid server.
 
@@ -72,37 +91,20 @@
     name = 'pyramid'
     actionverb = 'started'
 
-    options = (
-        ('no-daemon',
-         {'action': 'store_true',
-          'help': 'Run the server in the foreground.'}),
+    options = merge_options((
         ('debug-mode',
          {'action': 'store_true',
           'help': 'Activate the repository debug mode ('
-                  'logs in the console and the debug toolbar).'
-                  ' Implies --no-daemon'}),
+                  'logs in the console and the debug toolbar).'}),
         ('debug',
          {'short': 'D', 'action': 'store_true',
-          'help': 'Equals to "--debug-mode --no-daemon --reload"'}),
+          'help': 'Equals to "--debug-mode --reload"'}),
         ('reload',
          {'action': 'store_true',
           'help': 'Restart the server if any source file is changed'}),
         ('reload-interval',
          {'type': 'int', 'default': 1,
           'help': 'Interval, in seconds, between file modifications checks'}),
-        ('loglevel',
-         {'short': 'l', 'type': 'choice', 'metavar': '<log level>',
-          'default': None, 'choices': LOG_LEVELS,
-          'help': 'debug if -D is set, error otherwise; '
-                  'one of %s' % (LOG_LEVELS,),
-          }),
-        ('dbglevel',
-         {'type': 'multiple_choice', 'metavar': '<dbg level>',
-          'default': None,
-          'choices': DBG_FLAGS,
-          'help': ('Set the server debugging flags; you may choose several '
-                   'values in %s; imply "debug" loglevel' % (DBG_FLAGS,)),
-          }),
         ('profile',
          {'action': 'store_true',
           'default': False,
@@ -123,7 +125,7 @@
           'metavar': 'key1:value1,key2:value2',
           'default': {},
           'help': 'override <key> configuration file option with <value>.'}),
-    )
+    ) + InstanceCommand.options)
 
     _reloader_environ_key = 'CW_RELOADER_SHOULD_RUN'
 
@@ -153,92 +155,6 @@
         arg = win32api.GetShortPathName(arg)
         return arg
 
-    def _remove_pid_file(self, written_pid, filename):
-        current_pid = os.getpid()
-        if written_pid != current_pid:
-            # A forked process must be exiting, not the process that
-            # wrote the PID file
-            return
-        if not os.path.exists(filename):
-            return
-        with open(filename) as f:
-            content = f.read().strip()
-        try:
-            pid_in_file = int(content)
-        except ValueError:
-            pass
-        else:
-            if pid_in_file != current_pid:
-                msg = "PID file %s contains %s, not expected PID %s"
-                self.out(msg % (filename, pid_in_file, current_pid))
-                return
-        self.info("Removing PID file %s" % filename)
-        try:
-            os.unlink(filename)
-            return
-        except OSError as e:
-            # Record, but don't give traceback
-            self.out("Cannot remove PID file: (%s)" % e)
-        # well, at least lets not leave the invalid PID around...
-        try:
-            with open(filename, 'w') as f:
-                f.write('')
-        except OSError as e:
-            self.out('Stale PID left in file: %s (%s)' % (filename, e))
-        else:
-            self.out('Stale PID removed')
-
-    def record_pid(self, pid_file):
-        pid = os.getpid()
-        self.debug('Writing PID %s to %s' % (pid, pid_file))
-        with open(pid_file, 'w') as f:
-            f.write(str(pid))
-        atexit.register(
-            self._remove_pid_file, pid, pid_file)
-
-    def daemonize(self, pid_file):
-        pid = live_pidfile(pid_file)
-        if pid:
-            raise ExecutionError(
-                "Daemon is already running (PID: %s from PID file %s)"
-                % (pid, pid_file))
-
-        self.debug('Entering daemon mode')
-        pid = os.fork()
-        if pid:
-            # The forked process also has a handle on resources, so we
-            # *don't* want proper termination of the process, we just
-            # want to exit quick (which os._exit() does)
-            os._exit(0)
-        # Make this the session leader
-        os.setsid()
-        # Fork again for good measure!
-        pid = os.fork()
-        if pid:
-            os._exit(0)
-
-        # @@: Should we set the umask and cwd now?
-
-        import resource  # Resource usage information.
-        maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
-        if (maxfd == resource.RLIM_INFINITY):
-            maxfd = MAXFD
-        # Iterate through and close all file descriptors.
-        for fd in range(0, maxfd):
-            try:
-                os.close(fd)
-            except OSError:  # ERROR, fd wasn't open to begin with (ignored)
-                pass
-
-        if (hasattr(os, "devnull")):
-            REDIRECT_TO = os.devnull
-        else:
-            REDIRECT_TO = "/dev/null"
-        os.open(REDIRECT_TO, os.O_RDWR)  # standard input (0)
-        # Duplicate standard input to standard output and standard error.
-        os.dup2(0, 1)  # standard output (1)
-        os.dup2(0, 2)  # standard error (2)
-
     def restart_with_reloader(self, filelist_path):
         self.debug('Starting subprocess with file monitor')
 
@@ -312,14 +228,16 @@
     def pyramid_instance(self, appid):
         self._needreload = False
 
-        debugmode = self['debug-mode'] or self['debug']
         autoreload = self['reload'] or self['debug']
-        daemonize = not (self['no-daemon'] or debugmode or autoreload)
 
-        cwconfig = cwcfg.config_for(appid, debugmode=debugmode)
+        cwconfig = self.cwconfig
         filelist_path = os.path.join(cwconfig.apphome,
                                      '.pyramid-reload-files.list')
 
+        pyramid_ini_path = os.path.join(cwconfig.apphome, "pyramid.ini")
+        if not os.path.exists(pyramid_ini_path):
+            _generate_pyramid_ini_file(pyramid_ini_path)
+
         if autoreload and not os.environ.get(self._reloader_environ_key):
             return self.restart_with_reloader(filelist_path)
 
@@ -333,14 +251,9 @@
                 self['reload-interval'], extra_files,
                 filelist_path=filelist_path)
 
-        if daemonize:
-            self.daemonize(cwconfig['pid-file'])
-            self.record_pid(cwconfig['pid-file'])
-
-        if self['dbglevel']:
-            self['loglevel'] = 'debug'
-            set_debug('|'.join('DBG_' + x.upper() for x in self['dbglevel']))
-        init_cmdline_log_threshold(cwconfig, self['loglevel'])
+        # if no loglevel is specified and --debug is here, set log level at debug
+        if self['loglevel'] is None and self['debug']:
+            init_cmdline_log_threshold(self.cwconfig, 'debug')
 
         app = wsgi_application_from_cwconfig(
             cwconfig, profile=self['profile'],
@@ -353,12 +266,9 @@
         url_scheme = ('https' if cwconfig['base-url'].startswith('https')
                       else 'http')
         repo = app.application.registry['cubicweb.repository']
-        warnings.warn(
-            'the "pyramid" command does not start repository "looping tasks" '
-            'anymore; use the standalone "scheduler" command if needed'
-        )
         try:
-            waitress.serve(app, host=host, port=port, url_scheme=url_scheme)
+            waitress.serve(app, host=host, port=port, url_scheme=url_scheme,
+                           clear_untrusted_proxy_headers=True)
         finally:
             repo.shutdown()
         if self._needreload:
@@ -369,35 +279,6 @@
 CWCTL.register(PyramidStartHandler)
 
 
-def live_pidfile(pidfile):  # pragma: no cover
-    """(pidfile:str) -> int | None
-    Returns an int found in the named file, if there is one,
-    and if there is a running process with that process id.
-    Return None if no such process exists.
-    """
-    pid = read_pidfile(pidfile)
-    if pid:
-        try:
-            os.kill(int(pid), 0)
-            return pid
-        except OSError as e:
-            if e.errno == errno.EPERM:
-                return pid
-    return None
-
-
-def read_pidfile(filename):
-    if os.path.exists(filename):
-        try:
-            with open(filename) as f:
-                content = f.read()
-            return int(content.strip())
-        except (ValueError, IOError):
-            return None
-    else:
-        return None
-
-
 def _turn_sigterm_into_systemexit():
     """Attempts to turn a SIGTERM exception into a SystemExit exception."""
     try:
--- a/cubicweb/pyramid/resources.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/pyramid/resources.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 
 """Pyramid resource definitions for CubicWeb."""
 
-from six import text_type
-
 from rql import TypeResolverException
 
 from pyramid.decorator import reify
@@ -62,7 +60,7 @@
                 # conflicting eid/type
                 raise HTTPNotFound()
         else:
-            rset = req.execute(st.as_string(), {'x': text_type(self.value)})
+            rset = req.execute(st.as_string(), {'x': self.value})
         return rset
 
 
--- a/cubicweb/pyramid/session.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/pyramid/session.py	Fri Oct 18 23:39:03 2019 +0200
@@ -84,12 +84,11 @@
    redis) when using redis as backend.
 """
 
-import warnings
 import logging
 from contextlib import contextmanager
 
 from pyramid.compat import pickle
-from pyramid.session import SignedCookieSessionFactory
+from pyramid.session import SignedCookieSessionFactory, JSONSerializer, PickleSerializer
 
 from cubicweb import (
     Binary,
@@ -129,6 +128,24 @@
             yield cnx
 
 
+class JSONSerializerWithPickleFallback(object):
+    def __init__(self):
+        self.json = JSONSerializer()
+        self.pickle = PickleSerializer()
+
+    def dumps(self, value):
+        # maybe catch serialization errors here and keep using pickle
+        # while finding spots in your app that are not storing
+        # JSON-serializable objects, falling back to pickle
+        return self.json.dumps(value)
+
+    def loads(self, value):
+        try:
+            return self.json.loads(value)
+        except ValueError:
+            return self.pickle.loads(value)
+
+
 def CWSessionFactory(
         secret,
         cookie_name='session',
@@ -177,7 +194,7 @@
         reissue_time=reissue_time,
         hashalg=hashalg,
         salt=salt,
-        serializer=serializer)
+        serializer=serializer if serializer else JSONSerializerWithPickleFallback())
 
     class CWSession(SignedCookieSession):
         def __init__(self, request):
@@ -261,23 +278,6 @@
 
     See also :ref:`defaults_module`
     """
-    settings = config.registry.settings
-    try:
-        secret = settings['cubicweb.session.secret']
-    except KeyError:
-        secret = 'notsosecret'
-        if config.registry['cubicweb.config'].mode != 'test':
-            warnings.warn('''
-
-                !! WARNING !! !! WARNING !!
-
-                The session cookies are signed with a static secret key.
-                To put your own secret key, edit your pyramid.ini file
-                and set the 'cubicweb.session.secret' key.
-
-                YOU SHOULD STOP THIS INSTANCE unless your really know what you
-                are doing !!
-
-            ''')
+    secret = config.registry.settings['cubicweb.session.secret']
     session_factory = CWSessionFactory(secret)
     config.set_session_factory(session_factory)
--- a/cubicweb/pyramid/test/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/pyramid/test/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -15,7 +15,10 @@
     def setUp(self):
         # Skip CubicWebTestTC setUp
         super(CubicWebTestTC, self).setUp()
-        settings = {'cubicweb.bwcompat': False}
+        settings = {
+            'cubicweb.bwcompat': False,
+            'cubicweb.session.secret': 'test',
+        }
         settings.update(self.settings)
         config = Configurator(settings=settings)
         config.registry['cubicweb.repository'] = self.repo
--- a/cubicweb/pyramid/test/test_config.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/pyramid/test/test_config.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,13 +19,13 @@
 
 import os
 from os import path
+from tempfile import TemporaryDirectory
 from unittest import TestCase
+from unittest.mock import patch
 
-from mock import patch
-
-from cubicweb.devtools.testlib import TemporaryDirectory
 
 from cubicweb.pyramid import config
+from cubicweb.pyramid.pyramidctl import _generate_pyramid_ini_file
 
 
 class PyramidConfigTC(TestCase):
@@ -36,6 +36,27 @@
         self.assertEqual(patched_choice.call_count, 50)
         self.assertEqual(secret, '0' * 50)
 
+    def test_write_pyramid_ini(self):
+        with TemporaryDirectory() as instancedir:
+            pyramid_ini_path = path.join(instancedir, "pyramid.ini")
+            with patch('random.SystemRandom.choice', return_value='0') as patched_choice:
+                _generate_pyramid_ini_file(pyramid_ini_path)
+            with open(path.join(instancedir, 'pyramid.ini')) as f:
+                lines = f.readlines()
+
+        self.assertEqual(patched_choice.call_count, 50 * 3)
+
+        secret = '0' * 50
+
+        for option in ('cubicweb.session.secret',
+                       'cubicweb.auth.authtkt.persistent.secret',
+                       'cubicweb.auth.authtkt.session.secret'):
+            self.assertIn('{} = {}\n'.format(option, secret), lines)
+
+        for option in ('cubicweb.auth.authtkt.persistent.secure',
+                       'cubicweb.auth.authtkt.session.secure'):
+            self.assertIn('{} = {}\n'.format(option, "no"), lines)
+
     def test_write_development_ini(self):
         with TemporaryDirectory() as instancedir:
             appid = 'pyramid-instance'
--- a/cubicweb/pyramid/test/test_hooks.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/pyramid/test/test_hooks.py	Fri Oct 18 23:39:03 2019 +0200
@@ -1,7 +1,4 @@
-from six import text_type
-
 from cubicweb.pyramid.test import PyramidCWTest
-from cubicweb.pyramid import tools
 
 
 def set_language(request):
@@ -11,10 +8,10 @@
         cnx.execute('DELETE CWProperty X WHERE X for_user U, U eid %(u)s',
                     {'u': cnx.user.eid})
     else:
-        cnx.user.set_property(u'ui.language', text_type(lang))
+        cnx.user.set_property(u'ui.language', lang)
     cnx.commit()
 
-    request.response.text = text_type(cnx.user.properties.get('ui.language', ''))
+    request.response.text = cnx.user.properties.get('ui.language', '')
     return request.response
 
 
@@ -29,7 +26,7 @@
                     {'u': cnx.user.eid})
     cnx.commit()
 
-    request.response.text = text_type(','.join(sorted(cnx.user.groups)))
+    request.response.text = ','.join(sorted(cnx.user.groups))
     return request.response
 
 
--- a/cubicweb/repoapi.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/repoapi.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,27 +17,17 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Official API to access the content of a repository."""
 
-from warnings import warn
-
-from six import add_metaclass
-
-from logilab.common.deprecation import class_deprecated
-
 from cubicweb import AuthenticationError
 from cubicweb.server.session import Connection
 
 
-def get_repository(uri=None, config=None, vreg=None):
+def get_repository(config, vreg=None):
     """get a repository for the given URI or config/vregistry (in case we're
     loading the repository for a client, eg web server, configuration).
 
     The returned repository may be an in-memory repository or a proxy object
     using a specific RPC method, depending on the given URI.
     """
-    if uri is not None:
-        warn('[3.22] get_repository only wants a config')
-
-    assert config is not None, 'get_repository(config=config)'
     return config.repository(vreg)
 
 
@@ -63,8 +53,3 @@
     anon_login, anon_password = anoninfo
     # use vreg's repository cache
     return connect(repo, anon_login, password=anon_password)
-
-
-@add_metaclass(class_deprecated)
-class ClientConnection(Connection):
-    __deprecation_warning__ = '[3.20] %(cls)s is deprecated, use Connection instead'
--- a/cubicweb/req.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/req.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,22 +17,17 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Base class for request/session"""
 
-from warnings import warn
 from datetime import time, datetime, timedelta
-
-from six import PY2, PY3, text_type
-from six.moves.urllib.parse import (parse_qs, parse_qsl,
-                                    quote as urlquote, unquote as urlunquote,
-                                    urlsplit, urlunsplit)
+from urllib.parse import (parse_qs, parse_qsl,
+                          quote as urlquote, unquote as urlunquote,
+                          urlsplit, urlunsplit)
 
 from logilab.common.decorators import cached
-from logilab.common.deprecation import deprecated
 from logilab.common.date import ustrftime, strptime, todate, todatetime
 
 from rql.utils import rqlvar_maker
 
-from cubicweb import (Unauthorized, NoSelectableObject, NoResultError,
-                      MultipleResultsError, uilib)
+from cubicweb import Unauthorized, NoSelectableObject, uilib
 from cubicweb.rset import ResultSet
 
 ONESECOND = timedelta(0, 1, 0)
@@ -76,7 +71,7 @@
         self.user = None
         self.lang = None
         self.local_perm_cache = {}
-        self._ = text_type
+        self._ = str
 
     def _set_user(self, orig_user):
         """set the user for this req_session_base
@@ -100,10 +95,10 @@
             gettext, pgettext = self.vreg.config.translations[lang]
         except KeyError:
             assert self.vreg.config.mode == 'test'
-            gettext = text_type
+            gettext = str
 
             def pgettext(x, y):
-                return text_type(y)
+                return str(y)
 
         # use _cw.__ to translate a message without registering it to the catalog
         self._ = self.__ = gettext
@@ -182,29 +177,6 @@
         cls = self.vreg['etypes'].etype_class(etype)
         return cls.cw_instantiate(self.execute, **kwargs)
 
-    @deprecated('[3.18] use find(etype, **kwargs).entities()')
-    def find_entities(self, etype, **kwargs):
-        """find entities of the given type and attribute values.
-
-        >>> users = find_entities('CWGroup', name=u'users')
-        >>> groups = find_entities('CWGroup')
-        """
-        return self.find(etype, **kwargs).entities()
-
-    @deprecated('[3.18] use find(etype, **kwargs).one()')
-    def find_one_entity(self, etype, **kwargs):
-        """find one entity of the given type and attribute values.
-        raise :exc:`FindEntityError` if can not return one and only one entity.
-
-        >>> users = find_one_entity('CWGroup', name=u'users')
-        >>> groups = find_one_entity('CWGroup')
-        Exception()
-        """
-        try:
-            return self.find(etype, **kwargs).one()
-        except (NoResultError, MultipleResultsError) as e:
-            raise FindEntityError("%s: (%s, %s)" % (str(e), etype, kwargs))
-
     def find(self, etype, **kwargs):
         """find entities of the given type and attribute values.
 
@@ -248,33 +220,6 @@
         if first in ('insert', 'set', 'delete'):
             raise Unauthorized(self._('only select queries are authorized'))
 
-    def get_cache(self, cachename):
-        """cachename should be dotted names as in :
-
-        - cubicweb.mycache
-        - cubes.blog.mycache
-        - etc.
-        """
-        warn.warning('[3.19] .get_cache will disappear soon. '
-                     'Distributed caching mechanisms are being introduced instead.'
-                     'Other caching mechanism can be used more reliably '
-                     'to the same effect.',
-                     DeprecationWarning)
-        if cachename in CACHE_REGISTRY:
-            cache = CACHE_REGISTRY[cachename]
-        else:
-            cache = CACHE_REGISTRY[cachename] = Cache()
-        _now = datetime.now()
-        if _now > cache.latest_cache_lookup + ONESECOND:
-            ecache = self.execute(
-                'Any C,T WHERE C is CWCache, C name %(name)s, C timestamp T',
-                {'name': cachename}).get_entity(0, 0)
-            cache.latest_cache_lookup = _now
-            if not ecache.valid(cache.cache_creation_date):
-                cache.clear()
-                cache.cache_creation_date = _now
-        return cache
-
     # url generation methods ##################################################
 
     def build_url(self, *args, **kwargs):
@@ -296,9 +241,6 @@
         #     not try to process it and directly call req.build_url()
         base_url = kwargs.pop('base_url', None)
         if base_url is None:
-            if kwargs.pop('__secure__', None) is not None:
-                warn('[3.25] __secure__ argument is deprecated',
-                     DeprecationWarning, stacklevel=2)
             base_url = self.base_url()
         path = self.build_url_path(method, kwargs)
         if not kwargs:
@@ -330,9 +272,6 @@
         necessary encoding / decoding. Also it's designed to quote each
         part of a url path and so the '/' character will be encoded as well.
         """
-        if PY2 and isinstance(value, text_type):
-            quoted = urlquote(value.encode(self.encoding), safe=safe)
-            return text_type(quoted, self.encoding)
         return urlquote(str(value), safe=safe)
 
     def url_unquote(self, quoted):
@@ -341,28 +280,13 @@
         decoding is based on `self.encoding` which is the encoding
         used in `url_quote`
         """
-        if PY3:
-            return urlunquote(quoted)
-        if isinstance(quoted, text_type):
-            quoted = quoted.encode(self.encoding)
-        try:
-            return text_type(urlunquote(quoted), self.encoding)
-        except UnicodeDecodeError:  # might occurs on manually typed URLs
-            return text_type(urlunquote(quoted), 'iso-8859-1')
+        return urlunquote(quoted)
 
     def url_parse_qsl(self, querystring):
         """return a list of (key, val) found in the url quoted query string"""
-        if PY3:
-            for key, val in parse_qsl(querystring):
-                yield key, val
-            return
-        if isinstance(querystring, text_type):
-            querystring = querystring.encode(self.encoding)
         for key, val in parse_qsl(querystring):
-            try:
-                yield text_type(key, self.encoding), text_type(val, self.encoding)
-            except UnicodeDecodeError:  # might occurs on manually typed URLs
-                yield text_type(key, 'iso-8859-1'), text_type(val, 'iso-8859-1')
+            yield key, val
+        return
 
     def rebuild_url(self, url, **newparams):
         """return the given url with newparams inserted. If any new params
@@ -370,8 +294,6 @@
 
         newparams may only be mono-valued.
         """
-        if PY2 and isinstance(url, text_type):
-            url = url.encode(self.encoding)
         schema, netloc, path, query, fragment = urlsplit(url)
         query = parse_qs(query)
         # sort for testing predictability
@@ -442,7 +364,7 @@
             as_string = formatters[attrtype]
         except KeyError:
             self.error('given bad attrtype %s', attrtype)
-            return text_type(value)
+            return str(value)
         return as_string(value, self, props, displaytime)
 
     def format_date(self, date, date_format=None, time=False):
@@ -505,12 +427,7 @@
             raise ValueError(self._('can\'t parse %(value)r (expected %(format)s)')
                              % {'value': value, 'format': format})
 
-    def base_url(self, **kwargs):
+    def base_url(self):
         """Return the root url of the instance."""
-        secure = kwargs.pop('secure', None)
-        if secure is not None:
-            warn('[3.25] secure argument is deprecated', DeprecationWarning, stacklevel=2)
-        if kwargs:
-            raise TypeError('base_url got unexpected keyword arguments %s' % ', '.join(kwargs))
         url = self.vreg.config['base-url']
         return url if url is None else url.rstrip('/') + '/'
--- a/cubicweb/rqlrewrite.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/rqlrewrite.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,8 +21,6 @@
 This is used for instance for read security checking in the repository.
 """
 
-from six import text_type, string_types
-
 from rql import nodes as n, stmts, TypeResolverException
 from rql.utils import common_parent
 
@@ -640,7 +638,7 @@
             while argname in self.kwargs:
                 argname = subselect.allocate_varname()
             subselect.add_constant_restriction(subselect.get_variable(self.u_varname),
-                                               'eid', text_type(argname), 'Substitute')
+                                               'eid', argname, 'Substitute')
             self.kwargs[argname] = self.session.user.eid
         add_types_restriction(self.schema, subselect, subselect,
                               solutions=self.solutions)
@@ -795,7 +793,7 @@
                 # insert "U eid %(u)s"
                 stmt.add_constant_restriction(
                     stmt.get_variable(self.u_varname),
-                    'eid', text_type(argname), 'Substitute')
+                    'eid', argname, 'Substitute')
                 self.kwargs[argname] = self.session.user.eid
             return self.u_varname
         key = (self.current_expr, self.varmap, vname)
@@ -917,7 +915,7 @@
                 return n.Constant(vi['const'], 'Int')
             return n.VariableRef(stmt.get_variable(selectvar))
         vname_or_term = self._get_varname_or_term(node.name)
-        if isinstance(vname_or_term, string_types):
+        if isinstance(vname_or_term, str):
             return n.VariableRef(stmt.get_variable(vname_or_term))
         # shared term
         return vname_or_term.copy(stmt)
--- a/cubicweb/rset.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/rset.py	Fri Oct 18 23:39:03 2019 +0200
@@ -1,4 +1,4 @@
-# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -17,22 +17,12 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """The `ResultSet` class which is returned as result of an rql query"""
 
-
-from warnings import warn
-
-from six import PY3, text_type
-from six.moves import range
-
-from logilab.common import nullobject
 from logilab.common.decorators import cached, clear_cache, copy_cache
 from rql import nodes, stmts
 
 from cubicweb import NotAnEntity, NoResultError, MultipleResultsError, UnknownEid
 
 
-_MARKER = nullobject()
-
-
 class ResultSet(object):
     """A result set wraps a RQL query result. This object implements
     partially the list protocol to allow direct use as a list of
@@ -52,10 +42,7 @@
     :param rql: the original RQL query string
     """
 
-    def __init__(self, results, rql, args=None, description=None, rqlst=None):
-        if rqlst is not None:
-            warn('[3.20] rqlst parameter is deprecated',
-                 DeprecationWarning, stacklevel=2)
+    def __init__(self, results, rql, args=None, description=None):
         self.rows = results
         self.rowcount = results and len(results) or 0
         # original query and arguments
@@ -371,25 +358,11 @@
         rset.limited = (limit, offset)
         return rset
 
-    def printable_rql(self, encoded=_MARKER):
+    def printable_rql(self):
         """return the result set's origin rql as a string, with arguments
         substitued
         """
-        if encoded is not _MARKER:
-            warn('[3.21] the "encoded" argument is deprecated', DeprecationWarning)
-        encoding = self.req.encoding
-        rqlstr = self.syntax_tree().as_string(kwargs=self.args)
-        if PY3:
-            return rqlstr
-        # sounds like we get encoded or unicode string due to a bug in as_string
-        if not encoded:
-            if isinstance(rqlstr, text_type):
-                return rqlstr
-            return text_type(rqlstr, encoding)
-        else:
-            if isinstance(rqlstr, text_type):
-                return rqlstr.encode(encoding)
-            return rqlstr
+        return self.syntax_tree().as_string(kwargs=self.args)
 
     # client helper methods ###################################################
 
@@ -401,6 +374,8 @@
             if self.rows[i][col] is not None:
                 yield self.get_entity(i, col)
 
+    all = entities
+
     def iter_rows_with_entities(self):
         """ iterates over rows, and for each row
         eids are converted to plain entities
@@ -467,6 +442,34 @@
         else:
             raise MultipleResultsError("Multiple rows were found for one()")
 
+    def first(self, col=0):
+        """Retrieve the first entity from the query.
+
+        If the result set is empty, raises :exc:`NoResultError`.
+
+        :type col: int
+        :param col: The column localising the entity in the unique row
+
+        :return: the partially initialized `Entity` instance
+        """
+        if len(self) == 0:
+            raise NoResultError("No row was found for first()")
+        return self.get_entity(0, col)
+
+    def last(self, col=0):
+        """Retrieve the last entity from the query.
+
+        If the result set is empty, raises :exc:`NoResultError`.
+
+        :type col: int
+        :param col: The column localising the entity in the unique row
+
+        :return: the partially initialized `Entity` instance
+        """
+        if len(self) == 0:
+            raise NoResultError("No row was found for last()")
+        return self.get_entity(-1, col)
+
     def _make_entity(self, row, col):
         """Instantiate an entity, and store it in the entity cache"""
         # build entity instance
--- a/cubicweb/rtags.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/rtags.py	Fri Oct 18 23:39:03 2019 +0200
@@ -39,8 +39,6 @@
 
 import logging
 
-from six import string_types
-
 from logilab.common.logging_ext import set_log_methods
 from logilab.common.registry import RegistrableInstance, yes
 
@@ -182,7 +180,7 @@
         return tag
 
     def _tag_etype_attr(self, etype, attr, desttype='*', *args, **kwargs):
-        if isinstance(attr, string_types):
+        if isinstance(attr, str):
             attr, role = attr, 'subject'
         else:
             attr, role = attr
--- a/cubicweb/schema.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,21 +17,14 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """classes to define schemas for CubicWeb"""
 
-from __future__ import print_function
-
 from functools import wraps
 import re
 from os.path import join
 from hashlib import md5
 from logging import getLogger
-from warnings import warn
-
-from six import PY2, text_type, string_types, add_metaclass
-from six.moves import range
 
 from logilab.common.decorators import cached, clear_cache, monkeypatch, cachedproperty
 from logilab.common.logging_ext import set_log_methods
-from logilab.common.deprecation import deprecated
 from logilab.common.textutils import splitstrip
 from logilab.common.graph import get_cycles
 
@@ -45,12 +38,12 @@
                          cleanup_sys_modules, fill_schema_from_namespace)
 from yams.buildobjs import _add_relation as yams_add_relation
 
-from rql import parse, nodes, stmts, RQLSyntaxError, TypeResolverException
+from rql import parse, nodes, RQLSyntaxError, TypeResolverException
 from rql.analyze import ETypeResolver
 
 import cubicweb
 from cubicweb import server
-from cubicweb import ETYPE_NAME_MAP, ValidationError, Unauthorized, _
+from cubicweb import ValidationError, Unauthorized, _
 
 
 PURE_VIRTUAL_RTYPES = set(('identity', 'has_text',))
@@ -96,7 +89,7 @@
                       'WorkflowTransition', 'BaseTransition',
                       'SubWorkflowExitPoint'))
 
-INTERNAL_TYPES = set(('CWProperty', 'CWCache', 'ExternalUri', 'CWDataImport',
+INTERNAL_TYPES = set(('CWProperty', 'ExternalUri', 'CWDataImport',
                       'CWSource', 'CWSourceHostConfig', 'CWSession'))
 
 UNIQUE_CONSTRAINTS = ('SizeConstraint', 'FormatConstraint',
@@ -147,8 +140,6 @@
     added/removed for instance)
     """
     union = parse(u'Any 1 WHERE %s' % rqlstring).as_string()
-    if PY2 and isinstance(union, str):
-        union = union.decode('utf-8')
     return union.split(' WHERE ', 1)[1]
 
 
@@ -220,7 +211,7 @@
         """
         self.eid = eid  # eid of the entity representing this rql expression
         assert mainvars, 'bad mainvars %s' % mainvars
-        if isinstance(mainvars, string_types):
+        if isinstance(mainvars, str):
             mainvars = set(splitstrip(mainvars))
         elif not isinstance(mainvars, set):
             mainvars = set(mainvars)
@@ -233,8 +224,8 @@
             raise RQLSyntaxError(expression)
         for mainvar in mainvars:
             if len(self.snippet_rqlst.defined_vars[mainvar].references()) < 2:
-                _LOGGER.warn('You did not use the %s variable in your RQL '
-                             'expression %s', mainvar, self)
+                _LOGGER.warning('You did not use the %s variable in your RQL '
+                                'expression %s', mainvar, self)
         # graph of links between variables, used by rql rewriter
         self.vargraph = vargraph(self.snippet_rqlst)
         # useful for some instrumentation, e.g. localperms permcheck command
@@ -570,15 +561,6 @@
     return eschemas
 
 
-def bw_normalize_etype(etype):
-    if etype in ETYPE_NAME_MAP:
-        msg = '%s has been renamed to %s, please update your code' % (
-            etype, ETYPE_NAME_MAP[etype])
-        warn(msg, DeprecationWarning, stacklevel=4)
-        etype = ETYPE_NAME_MAP[etype]
-    return etype
-
-
 def display_name(req, key, form='', context=None):
     """return a internationalized string for the key (schema entity or relation
     name) in a given form
@@ -590,9 +572,9 @@
         key = key + '_' + form
     # ensure unicode
     if context is not None:
-        return text_type(req.pgettext(context, key))
+        return req.pgettext(context, key)
     else:
-        return text_type(req._(key))
+        return req._(key)
 
 
 def _override_method(cls, method_name=None, pass_original=False):
@@ -638,7 +620,7 @@
     """
     assert action in self.ACTIONS, action
     try:
-        return frozenset(g for g in self.permissions[action] if isinstance(g, string_types))
+        return frozenset(g for g in self.permissions[action] if isinstance(g, str))
     except KeyError:
         return ()
 
@@ -657,7 +639,7 @@
     """
     assert action in self.ACTIONS, action
     try:
-        return tuple(g for g in self.permissions[action] if not isinstance(g, string_types))
+        return tuple(g for g in self.permissions[action] if not isinstance(g, str))
     except KeyError:
         return ()
 
@@ -993,10 +975,6 @@
                     return False
         return True
 
-    @deprecated('use .rdef(subjtype, objtype).role_cardinality(role)')
-    def cardinality(self, subjtype, objtype, target):
-        return self.rdef(subjtype, objtype).role_cardinality(target)
-
 
 class CubicWebSchema(Schema):
     """set of entities and relations schema defining the possible data sets
@@ -1038,7 +1016,6 @@
 
     def add_entity_type(self, edef):
         edef.name = str(edef.name)
-        edef.name = bw_normalize_etype(edef.name)
         if not re.match(self.etype_name_re, edef.name):
             raise BadSchemaDefinition(
                 '%r is not a valid name for an entity type. It should start '
@@ -1084,8 +1061,6 @@
         :param: the newly created or just completed relation schema
         """
         rdef.name = rdef.name.lower()
-        rdef.subject = bw_normalize_etype(rdef.subject)
-        rdef.object = bw_normalize_etype(rdef.object)
         rdefs = super(CubicWebSchema, self).add_relation_def(rdef)
         if rdefs:
             try:
@@ -1182,14 +1157,13 @@
 
 # additional cw specific constraints ###########################################
 
-@monkeypatch(BaseConstraint)
-def name_for(self, rdef):
+def constraint_name_for(constraint, rdef):
     """Return a unique, size controlled, name for this constraint applied to given `rdef`.
 
     This name may be used as name for the constraint in the database.
     """
-    return 'cstr' + md5((rdef.subject.type + rdef.rtype.type + self.type()
-                         + (self.serialize() or '')).encode('ascii')).hexdigest()
+    return 'cstr' + md5((rdef.subject.type + rdef.rtype.type + constraint.type()
+                         + (constraint.serialize() or '')).encode('ascii')).hexdigest()
 
 
 class BaseRQLConstraint(RRQLExpression, BaseConstraint):
@@ -1352,8 +1326,7 @@
         return cls
 
 
-@add_metaclass(workflowable_definition)
-class WorkflowableEntityType(ybo.EntityType):
+class WorkflowableEntityType(ybo.EntityType, metaclass=workflowable_definition):
     """Use this base class instead of :class:`EntityType` to have workflow
     relations (i.e. `in_state`, `wf_info_for` and `custom_workflow`) on your
     entity type.
@@ -1463,27 +1436,3 @@
         if hasperm:
             return self.regular_formats + tuple(NEED_PERM_FORMATS)
     return self.regular_formats
-
-
-# XXX itou for some Statement methods
-
-@_override_method(stmts.ScopeNode, pass_original=True)
-def get_etype(self, name, _orig):
-    return _orig(self, bw_normalize_etype(name))
-
-
-@_override_method(stmts.Delete, method_name='add_main_variable',
-                  pass_original=True)
-def _add_main_variable_delete(self, etype, vref, _orig):
-    return _orig(self, bw_normalize_etype(etype), vref)
-
-
-@_override_method(stmts.Insert, method_name='add_main_variable',
-                  pass_original=True)
-def _add_main_variable_insert(self, etype, vref, _orig):
-    return _orig(self, bw_normalize_etype(etype), vref)
-
-
-@_override_method(stmts.Select, pass_original=True)
-def set_statement_type(self, etype, _orig):
-    return _orig(self, bw_normalize_etype(etype))
--- a/cubicweb/schemas/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/schemas/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -32,20 +32,3 @@
 # permissions for relation type that should only set by hooks using unsafe
 # execute, readable by anyone
 HOOKS_RTYPE_PERMS = RO_REL_PERMS # XXX deprecates
-
-
-from logilab.common.modutils import LazyObject
-from logilab.common.deprecation import deprecated
-class MyLazyObject(LazyObject):
-
-    def _getobj(self):
-        try:
-            return super(MyLazyObject, self)._getobj()
-        except ImportError:
-            raise ImportError('In cubicweb 3.14, function %s has been moved to '
-                              'cube localperms. Install it first.' % self.obj)
-
-for name in ('xperm', 'xexpr', 'xrexpr', 'xorexpr', 'sexpr', 'restricted_sexpr',
-             'restricted_oexpr', 'oexpr', 'relxperm', 'relxexpr', '_perm'):
-    msg = '[3.14] import %s from cubes.localperms' % name
-    globals()[name] = deprecated(msg, name=name, doc='deprecated')(MyLazyObject('cubes.localperms', name))
--- a/cubicweb/schemas/base.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/schemas/base.py	Fri Oct 18 23:39:03 2019 +0200
@@ -209,29 +209,6 @@
     object = 'ExternalUri'
 
 
-class CWCache(EntityType):
-    """a simple cache entity characterized by a name and
-    a validity date.
-
-    The target application is responsible for updating timestamp
-    when necessary to invalidate the cache (typically in hooks).
-
-    Also, checkout the AppObject.get_cache() method.
-    """
-    # XXX only handle by hooks, shouldn't be readable/editable at all through
-    # the ui and so no permissions should be granted, no?
-    __permissions__ = {
-        'read':   ('managers', 'users', 'guests'),
-        'add':    ('managers',),
-        'update': ('managers', 'users',), # XXX
-        'delete': ('managers',),
-        }
-
-    name = String(required=True, unique=True, maxsize=128,
-                  description=_('name of the cache'))
-    timestamp = TZDatetime(default='NOW')
-
-
 class CWSource(EntityType):
     __permissions__ = {
         'read':   ('managers', 'users', 'guests'),
--- a/cubicweb/selectors.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,107 +0,0 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-
-from warnings import warn
-
-from six import string_types
-
-from logilab.common.deprecation import deprecated, class_renamed
-
-from cubicweb.predicates import *
-
-
-warn('[3.15] cubicweb.selectors renamed into cubicweb.predicates',
-     DeprecationWarning, stacklevel=2)
-
-# XXX pre 3.15 bw compat
-from cubicweb.appobject import (objectify_selector, traced_selection,
-                                lltrace, yes)
-
-ExpectedValueSelector = class_renamed('ExpectedValueSelector',
-                                      ExpectedValuePredicate)
-EClassSelector = class_renamed('EClassSelector', EClassPredicate)
-EntitySelector = class_renamed('EntitySelector', EntityPredicate)
-
-
-class on_transition(is_in_state):
-    """Return 1 if entity is in one of the transitions given as argument list
-
-    Especially useful to match passed transition to enable notifications when
-    your workflow allows several transition to the same states.
-
-    Note that if workflow `change_state` adapter method is used, this predicate
-    will not be triggered.
-
-    You should use this instead of your own :class:`score_entity` predicate to
-    avoid some gotchas:
-
-    * possible views gives a fake entity with no state
-    * you must use the latest tr info thru the workflow adapter for repository
-      side checking of the current state
-
-    In debug mode, this predicate can raise:
-    :raises: :exc:`ValueError` for unknown transition names
-        (etype workflow only not checked in custom workflow)
-
-    :rtype: int
-    """
-    @deprecated('[3.12] on_transition is deprecated, you should rather use '
-                'on_fire_transition(etype, trname)')
-    def __init__(self, *expected):
-        super(on_transition, self).__init__(*expected)
-
-    def _score(self, adapted):
-        trinfo = adapted.latest_trinfo()
-        if trinfo and trinfo.by_transition:
-            return trinfo.by_transition[0].name in self.expected
-
-    def _validate(self, adapted):
-        wf = adapted.current_workflow
-        valid = [n.name for n in wf.reverse_transition_of]
-        unknown = sorted(self.expected.difference(valid))
-        if unknown:
-            raise ValueError("%s: unknown transition(s): %s"
-                             % (wf.name, ",".join(unknown)))
-
-
-entity_implements = class_renamed('entity_implements', is_instance)
-
-class _but_etype(EntityPredicate):
-    """accept if the given entity types are not found in the result set.
-
-    See `EntityPredicate` documentation for behaviour when row is not specified.
-
-    :param *etypes: entity types (`string_types`) which should be refused
-    """
-    def __init__(self, *etypes):
-        super(_but_etype, self).__init__()
-        self.but_etypes = etypes
-
-    def score(self, req, rset, row, col):
-        if rset.description[row][col] in self.but_etypes:
-            return 0
-        return 1
-
-but_etype = class_renamed('but_etype', _but_etype, 'use ~is_instance(*etypes) instead')
-
-# XXX deprecated the one_* variants of predicates below w/ multi_xxx(nb=1)?
-#     take care at the implementation though (looking for the 'row' argument's
-#     value)
-two_lines_rset = class_renamed('two_lines_rset', multi_lines_rset)
-two_cols_rset = class_renamed('two_cols_rset', multi_columns_rset)
-two_etypes_rset = class_renamed('two_etypes_rset', multi_etypes_rset)
--- a/cubicweb/server/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,13 +20,8 @@
 
 The server module contains functions to initialize a new repository.
 """
-from __future__ import print_function
-
 from contextlib import contextmanager
 
-from six import text_type, string_types
-from six.moves import filter
-
 from logilab.common.modutils import LazyObject
 from logilab.common.textutils import splitstrip
 from logilab.common.registry import yes
@@ -68,8 +63,6 @@
 DBG_SQL = 2
 #: repository events
 DBG_REPO = 4
-#: multi-sources
-DBG_MS = 8
 #: hooks
 DBG_HOOKS = 16
 #: operations
@@ -79,7 +72,7 @@
 #: more verbosity
 DBG_MORE = 128
 #: all level enabled
-DBG_ALL = DBG_RQL + DBG_SQL + DBG_REPO + DBG_MS + DBG_HOOKS + DBG_OPS + DBG_SEC + DBG_MORE
+DBG_ALL = DBG_RQL + DBG_SQL + DBG_REPO + DBG_HOOKS + DBG_OPS + DBG_SEC + DBG_MORE
 
 _SECURITY_ITEMS = []
 _SECURITY_CAPS = ['read', 'add', 'update', 'delete', 'transition']
@@ -133,7 +126,7 @@
     if not debugmode:
         DEBUG = 0
         return
-    if isinstance(debugmode, string_types):
+    if isinstance(debugmode, str):
         for mode in splitstrip(debugmode, sep='|'):
             DEBUG |= globals()[mode]
     else:
@@ -192,7 +185,7 @@
     user = session.create_entity('CWUser', login=login, upassword=pwd)
     for group in groups:
         session.execute('SET U in_group G WHERE U eid %(u)s, G name %(group)s',
-                        {'u': user.eid, 'group': text_type(group)})
+                        {'u': user.eid, 'group': group})
     return user
 
 
@@ -270,17 +263,17 @@
         # insert base groups and default admin
         print('-> inserting default user and default groups.')
         try:
-            login = text_type(sourcescfg['admin']['login'])
+            login = sourcescfg['admin']['login']
             pwd = sourcescfg['admin']['password']
         except KeyError:
             if interactive:
                 msg = 'enter login and password of the initial manager account'
                 login, pwd = manager_userpasswd(msg=msg, confirm=True)
             else:
-                login, pwd = text_type(source['db-user']), source['db-password']
+                login, pwd = source['db-user'], source['db-password']
         # sort for eid predicatability as expected in some server tests
         for group in sorted(BASE_GROUPS):
-            cnx.create_entity('CWGroup', name=text_type(group))
+            cnx.create_entity('CWGroup', name=group)
         admin = create_user(cnx, login, pwd, u'managers')
         cnx.execute('SET X owned_by U WHERE X is IN (CWGroup,CWSource), U eid %(u)s',
                     {'u': admin.eid})
--- a/cubicweb/server/checkintegrity.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/checkintegrity.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,7 +20,6 @@
 * integrity of a CubicWeb repository. Hum actually only the system database is
   checked.
 """
-from __future__ import print_function
 
 import sys
 from datetime import datetime
--- a/cubicweb/server/hook.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/hook.py	Fri Oct 18 23:39:03 2019 +0200
@@ -75,7 +75,7 @@
 
 Hooks are being fired immediately on data operations, and it is sometime
 necessary to delay the actual work down to a time where we can expect all
-information to be there, or when all other hooks have run (though take case
+information to be there, or when all other hooks have run (though take care
 since operations may themselves trigger hooks). Also while the order of
 execution of hooks is data dependant (and thus hard to predict), it is possible
 to force an order on operations.
@@ -245,13 +245,11 @@
 .. autoclass:: cubicweb.server.hook.LateOperation
 .. autoclass:: cubicweb.server.hook.DataOperationMixIn
 """
-from __future__ import print_function
 
 from logging import getLogger
 from itertools import chain
 
 from logilab.common.decorators import classproperty, cached
-from logilab.common.deprecation import deprecated, class_renamed
 from logilab.common.logging_ext import set_log_methods
 from logilab.common.registry import (NotPredicate, OrPredicate,
                                      objectify_predicate)
@@ -428,10 +426,6 @@
 def issued_from_user_query(cls, req, **kwargs):
     return 0 if req.hooks_in_progress else 1
 
-from_dbapi_query = class_renamed('from_dbapi_query',
-                                 issued_from_user_query,
-                                 message='[3.21] ')
-
 
 class rechain(object):
     def __init__(self, *iterators):
@@ -445,7 +439,7 @@
     named parameters `frometypes` and `toetypes` can be used to restrict
     target subject and/or object entity types of the relation.
 
-    :param \*expected: possible relation types
+    :param *expected: possible relation types
     :param frometypes: candidate entity types as subject of relation
     :param toetypes: candidate entity types as object of relation
     """
@@ -738,11 +732,6 @@
         self.processed = None # 'precommit', 'commit'
         self.failed = False
 
-    @property
-    @deprecated('[3.19] Operation.session is deprecated, use Operation.cnx instead')
-    def session(self):
-        return self.cnx
-
     def register(self, cnx):
         cnx.add_operation(self, self.insert_index())
 
@@ -795,7 +784,7 @@
 
 class DataOperationMixIn(object):
     """Mix-in class to ease applying a single operation on a set of data,
-    avoiding to create as many as operation as they are individual modification.
+    avoiding creating as many operations as there are individual modifications.
     The body of the operation must then iterate over the values that have been
     stored in a single operation instance.
 
--- a/cubicweb/server/migractions.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/migractions.py	Fri Oct 18 23:39:03 2019 +0200
@@ -26,33 +26,28 @@
 * add an entity
 * execute raw RQL queries
 """
-from __future__ import print_function
-
-
 
 import sys
 import os
 import tarfile
 import tempfile
 import shutil
+import traceback
 import os.path as osp
 from datetime import datetime
 from glob import glob
 from copy import copy
 from contextlib import contextmanager
 
-from six import PY2, text_type
-
-from logilab.common.deprecation import deprecated
 from logilab.common.decorators import cached, clear_cache
 
 from yams.buildobjs import EntityType
 from yams.constraints import SizeConstraint
 from yams.schema import RelationDefinitionSchema
 
-from cubicweb import CW_SOFTWARE_ROOT, AuthenticationError, ExecutionError
+from cubicweb import CW_SOFTWARE_ROOT, ETYPE_NAME_MAP, AuthenticationError, ExecutionError
 from cubicweb.predicates import is_instance
-from cubicweb.schema import (ETYPE_NAME_MAP, META_RTYPES, VIRTUAL_RTYPES,
+from cubicweb.schema import (META_RTYPES, VIRTUAL_RTYPES,
                              PURE_VIRTUAL_RTYPES,
                              CubicWebRelationSchema, order_eschemas)
 from cubicweb.cwvreg import CW_EVENT_MANAGER
@@ -154,7 +149,7 @@
 
     def cube_upgraded(self, cube, version):
         self.cmd_set_property('system.version.%s' % cube.lower(),
-                              text_type(version))
+                              str(version))
         self.commit()
 
     def shutdown(self):
@@ -268,9 +263,10 @@
         source = repo.system_source
         try:
             source.restore(osp.join(tmpdir, source.uri), self.confirm, drop, format)
-        except Exception as exc:
+        except Exception:
+            _, exc, traceback_ = sys.exc_info()
             print('-> error trying to restore %s [%s]' % (source.uri, exc))
-            if not self.confirm('Continue anyway?', default='n'):
+            if not self.confirm('Continue anyway?', default='n', pdb=True, traceback=traceback_):
                 raise SystemExit(1)
         finally:
             shutil.rmtree(tmpdir)
@@ -1006,7 +1002,7 @@
         # elif simply renaming an entity type
         else:
             self.rqlexec('SET ET name %(newname)s WHERE ET is CWEType, ET name %(on)s',
-                         {'newname': text_type(newname), 'on': oldname},
+                         {'newname': newname, 'on': oldname},
                          ask_confirm=False)
         if commit:
             self.commit()
@@ -1218,8 +1214,6 @@
         values = []
         for k, v in kwargs.items():
             values.append('X %s %%(%s)s' % (k, k))
-            if PY2 and isinstance(v, str):
-                kwargs[k] = unicode(v)
         rql = 'SET %s WHERE %s' % (','.join(values), ','.join(restriction))
         self.rqlexec(rql, kwargs, ask_confirm=self.verbosity >= 2)
         if commit:
@@ -1251,7 +1245,7 @@
                 self.rqlexec('SET C value %%(v)s WHERE X from_entity S, X relation_type R,'
                              'X constrained_by C, C cstrtype CT, CT name "SizeConstraint",'
                              'S name "%s", R name "%s"' % (etype, rtype),
-                             {'v': text_type(SizeConstraint(size).serialize())},
+                             {'v': SizeConstraint(size).serialize()},
                              ask_confirm=self.verbosity >= 2)
             else:
                 self.rqlexec('DELETE X constrained_by C WHERE X from_entity S, X relation_type R,'
@@ -1288,7 +1282,7 @@
 
          :rtype: `Workflow`
         """
-        wf = self.cmd_create_entity('Workflow', name=text_type(name),
+        wf = self.cmd_create_entity('Workflow', name=name,
                                     **kwargs)
         if not isinstance(wfof, (list, tuple)):
             wfof = (wfof,)
@@ -1298,19 +1292,18 @@
 
         for etype in wfof:
             eschema = self.repo.schema[etype]
-            etype = text_type(etype)
             if ensure_workflowable:
                 assert 'in_state' in eschema.subjrels, _missing_wf_rel(etype)
                 assert 'custom_workflow' in eschema.subjrels, _missing_wf_rel(etype)
                 assert 'wf_info_for' in eschema.objrels, _missing_wf_rel(etype)
             rset = self.rqlexec(
                 'SET X workflow_of ET WHERE X eid %(x)s, ET name %(et)s',
-                {'x': wf.eid, 'et': text_type(etype)}, ask_confirm=False)
+                {'x': wf.eid, 'et': etype}, ask_confirm=False)
             assert rset, 'unexistant entity type %s' % etype
             if default:
                 self.rqlexec(
                     'SET ET default_workflow X WHERE X eid %(x)s, ET name %(et)s',
-                    {'x': wf.eid, 'et': text_type(etype)}, ask_confirm=False)
+                    {'x': wf.eid, 'et': etype}, ask_confirm=False)
         if commit:
             self.commit()
         return wf
@@ -1341,13 +1334,13 @@
         To set a user specific property value, use appropriate method on CWUser
         instance.
         """
-        value = text_type(value)
+        value = str(value)
         try:
             prop = self.rqlexec(
                 'CWProperty X WHERE X pkey %(k)s, NOT X for_user U',
-                {'k': text_type(pkey)}, ask_confirm=False).get_entity(0, 0)
+                {'k': str(pkey)}, ask_confirm=False).get_entity(0, 0)
         except Exception:
-            self.cmd_create_entity('CWProperty', pkey=text_type(pkey), value=value)
+            self.cmd_create_entity('CWProperty', pkey=str(pkey), value=value)
         else:
             prop.cw_set(value=value)
 
@@ -1385,20 +1378,6 @@
         """find entities of the given type and attribute values"""
         return self.cnx.find(etype, **kwargs)
 
-    @deprecated("[3.19] use find(*args, **kwargs).entities() instead")
-    def cmd_find_entities(self, etype, **kwargs):
-        """find entities of the given type and attribute values"""
-        return self.cnx.find(etype, **kwargs).entities()
-
-    @deprecated("[3.19] use find(*args, **kwargs).one() instead")
-    def cmd_find_one_entity(self, etype, **kwargs):
-        """find one entity of the given type and attribute values.
-
-        raise :exc:`cubicweb.req.FindEntityError` if can not return one and only
-        one entity.
-        """
-        return self.cnx.find(etype, **kwargs).one()
-
     def cmd_update_etype_fti_weight(self, etype, weight):
         if self.repo.system_source.dbdriver == 'postgres':
             self.sqlexec('UPDATE appears SET weight=%(weight)s '
@@ -1413,6 +1392,25 @@
         from cubicweb.server.checkintegrity import reindex_entities
         reindex_entities(self.repo.schema, self.cnx, etypes=etypes)
 
+    def cmd_update_bfss_path(self, old_path, new_path, commit=True):
+        """
+        Change the path of all Files from old_path to new_path.
+        """
+        changes = []
+        for f_eid, fspath in self.rqlexec(
+                'Any F, FSPATH(D) WHERE F is File, F data D'):
+            fspath = fspath.getvalue().decode('utf-8')
+            dirname = os.path.dirname(fspath)
+            if dirname == old_path:
+                newpath = os.path.join(new_path, os.path.basename(fspath))
+                changes.append({'expected': newpath, 'eid': f_eid})
+        self.repo.system_source.doexecmany(
+            self.cnx,
+            'UPDATE cw_file SET cw_data=%(expected)s WHERE cw_eid=%(eid)s',
+            changes)
+        if commit:
+            self.commit()
+
     @contextmanager
     def cmd_dropped_constraints(self, etype, attrname, cstrtype=None,
                                 droprequired=False):
@@ -1458,8 +1456,9 @@
             try:
                 cu = self.cnx.system_sql(sql, args)
             except Exception:
-                ex = sys.exc_info()[1]
-                if self.confirm('Error: %s\nabort?' % ex, pdb=True):
+                _, ex, traceback_ = sys.exc_info()
+                traceback.print_exc()
+                if self.confirm('abort?', pdb=True, traceback=traceback_):
                     raise
                 return
             try:
@@ -1483,8 +1482,10 @@
             if not ask_confirm or self.confirm('Execute rql: %s ?' % msg):
                 try:
                     res = execute(rql, kwargs, build_descr=build_descr)
-                except Exception as ex:
-                    if self.confirm('Error: %s\nabort?' % ex, pdb=True):
+                except Exception:
+                    _, ex, traceback_ = sys.exc_info()
+                    traceback.print_exc()
+                    if self.confirm('abort?', pdb=True, traceback=traceback_):
                         raise
         return res
 
@@ -1548,10 +1549,6 @@
         if commit:
             self.commit()
 
-    @deprecated("[3.15] use rename_relation_type(oldname, newname)")
-    def cmd_rename_relation(self, oldname, newname, commit=True):
-        self.cmd_rename_relation_type(oldname, newname, commit)
-
 
 class ForRqlIterator:
     """specific rql iterator to make the loop skipable"""
@@ -1576,8 +1573,10 @@
                 raise StopIteration
         try:
             return self._h._cw.execute(rql, kwargs)
-        except Exception as ex:
-            if self._h.confirm('Error: %s\nabort?' % ex):
+        except Exception:
+            _, ex, traceback_ = sys.exc_info()
+            traceback.print_exc()
+            if self._h.confirm('abort?', pdb=True, traceback=traceback_):
                 raise
             else:
                 raise StopIteration
--- a/cubicweb/server/querier.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/querier.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,13 +18,8 @@
 """Helper classes to execute RQL queries on a set of sources, performing
 security checking and data aggregation.
 """
-from __future__ import print_function
-
 from itertools import repeat
 
-from six import text_type, string_types, integer_types
-from six.moves import range, zip
-
 from rql import RQLSyntaxError, CoercionError
 from rql.stmts import Union
 from rql.nodes import ETYPE_PYOBJ_MAP, etype_from_pyobj, Relation, Exists, Not
@@ -36,6 +31,7 @@
 from cubicweb.rset import ResultSet
 
 from cubicweb.utils import QueryCache, RepeatList
+from cubicweb.misc.source_highlight import highlight
 from cubicweb.server.rqlannotation import SQLGenAnnotator, set_qdata
 from cubicweb.server.ssplanner import READ_ONLY_RTYPES, add_types_restriction
 from cubicweb.server.edition import EditedEntity
@@ -45,9 +41,9 @@
 ETYPE_PYOBJ_MAP[Binary] = 'Bytes'
 
 
-def empty_rset(rql, args, rqlst=None):
+def empty_rset(rql, args):
     """build an empty result set object"""
-    return ResultSet([], rql, args, rqlst=rqlst)
+    return ResultSet([], rql, args)
 
 
 # permission utilities ########################################################
@@ -167,7 +163,6 @@
         # various resource accesors
         self.querier = querier
         self.schema = querier.schema
-        self.sqlannotate = querier.sqlgen_annotate
         self.rqlhelper = cnx.vreg.rqlhelper
 
     def annotate_rqlst(self):
@@ -219,7 +214,7 @@
             noinvariant = ()
         if cached is None:
             self.rqlhelper.simplify(union)
-            self.sqlannotate(union)
+            self.querier.sqlgen_annotate(union)
             set_qdata(self.schema.rschema, union, noinvariant)
         if union.has_text_query:
             self.cache_key = None
@@ -443,13 +438,13 @@
         relations = {}
         for subj, rtype, obj in self.relation_defs():
             # if a string is given into args instead of an int, we get it here
-            if isinstance(subj, string_types):
+            if isinstance(subj, str):
                 subj = int(subj)
-            elif not isinstance(subj, integer_types):
+            elif not isinstance(subj, int):
                 subj = subj.entity.eid
-            if isinstance(obj, string_types):
+            if isinstance(obj, str):
                 obj = int(obj)
-            elif not isinstance(obj, integer_types):
+            elif not isinstance(obj, int):
                 obj = obj.entity.eid
             if repo.schema.rschema(rtype).inlined:
                 if subj not in edited_entities:
@@ -481,10 +476,8 @@
     def set_schema(self, schema):
         self.schema = schema
         self.clear_caches()
-        rqlhelper = self._repo.vreg.rqlhelper
-        self._annotate = rqlhelper.annotate
         # rql planner
-        self._planner = SSPlanner(schema, rqlhelper)
+        self._planner = SSPlanner(schema, self._repo.vreg.rqlhelper)
         # sql generation annotator
         self.sqlgen_annotate = SQLGenAnnotator(schema).annotate
 
@@ -527,7 +520,7 @@
         if server.DEBUG & (server.DBG_RQL | server.DBG_SQL):
             if server.DEBUG & (server.DBG_MORE | server.DBG_SQL):
                 print('*'*80)
-            print('querier input', repr(rql), repr(args))
+            print("querier input", highlight(repr(rql)[1:-1], 'RQL'), repr(args))
         try:
             rqlst, cachekey = self.rql_cache.get(cnx, rql, args)
         except UnknownEid:
@@ -550,7 +543,7 @@
             # Rewrite computed relations
             rewriter = RQLRelationRewriter(cnx)
             rewriter.rewrite(rqlst, args)
-            self._annotate(rqlst)
+            self._repo.vreg.rqlhelper.annotate(rqlst)
             if args:
                 # different SQL generated when some argument is None or not (IS
                 # NULL). This should be considered when computing sql cache key
@@ -626,7 +619,7 @@
         def parse(rql, annotate=False, parse=rqlhelper.parse):
             """Return a freshly parsed syntax tree for the given RQL."""
             try:
-                return parse(text_type(rql), annotate=annotate)
+                return parse(rql, annotate=annotate)
             except UnicodeError:
                 raise RQLSyntaxError(rql)
         self._parse = parse
--- a/cubicweb/server/repository.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/repository.py	Fri Oct 18 23:39:03 2019 +0200
@@ -26,17 +26,12 @@
 * handles session management
 """
 
-from __future__ import print_function
-
-from warnings import warn
 from itertools import chain
 from contextlib import contextmanager
 from logging import getLogger
-
-from six.moves import range, queue
+import queue
 
 from logilab.common.decorators import cached, clear_cache
-from logilab.common.deprecation import deprecated
 
 from yams import BadSchemaDefinition
 from rql.utils import rqlvar_maker
@@ -323,14 +318,6 @@
                        for sourceent, source in self._sources())
         return mapping
 
-    @property
-    @deprecated("[3.25] use source_by_eid(<eid>)")
-    def sources_by_eid(self):
-        mapping = {self.system_source.eid: self.system_source}
-        mapping.update((sourceent.eid, source)
-                       for sourceent, source in self._sources())
-        return mapping
-
     def _sources(self):
         if self.config.quick_start:
             return
@@ -516,31 +503,6 @@
 
     # public (dbapi) interface ################################################
 
-    @deprecated("[3.19] use _cw.call_service('repo_stats')")
-    def stats(self):  # XXX restrict to managers session?
-        """Return a dictionary containing some statistics about the repository
-        resources usage.
-
-        This is a public method, not requiring a session id.
-
-        This method is deprecated in favor of using _cw.call_service('repo_stats')
-        """
-        with self.internal_cnx() as cnx:
-            return cnx.call_service('repo_stats')
-
-    @deprecated("[3.19] use _cw.call_service('repo_gc_stats')")
-    def gc_stats(self, nmax=20):
-        """Return a dictionary containing some statistics about the repository
-        memory usage.
-
-        This is a public method, not requiring a session id.
-
-        nmax is the max number of (most) referenced object returned as
-        the 'referenced' result
-        """
-        with self.internal_cnx() as cnx:
-            return cnx.call_service('repo_gc_stats', nmax=nmax)
-
     def get_schema(self):
         """Return the instance schema.
 
@@ -561,16 +523,11 @@
         cubes.remove('cubicweb')
         return cubes
 
-    def get_option_value(self, option, foreid=None):
+    def get_option_value(self, option):
         """Return the value for `option` in the configuration.
 
         This is a public method, not requiring a session id.
-
-        `foreid` argument is deprecated and now useless (as of 3.19).
         """
-        if foreid is not None:
-            warn('[3.19] foreid argument is deprecated', DeprecationWarning,
-                 stacklevel=2)
         # XXX we may want to check we don't give sensible information
         return self.config[option]
 
@@ -631,17 +588,6 @@
                                         'P pkey K, P value V, NOT P for_user U',
                                         build_descr=False)
 
-    @deprecated("[3.19] Use session.call_service('register_user') instead'")
-    def register_user(self, login, password, email=None, **kwargs):
-        """check a user with the given login exists, if not create it with the
-        given password. This method is designed to be used for anonymous
-        registration on public web site.
-        """
-        with self.internal_cnx() as cnx:
-            cnx.call_service('register_user', login=login, password=password,
-                             email=email, **kwargs)
-            cnx.commit()
-
     def find_users(self, fetch_attrs, **query_attrs):
         """yield user attributes for cwusers matching the given query_attrs
         (the result set cannot survive this method call)
@@ -874,10 +820,6 @@
         # operation (register pending eids before actual deletion to avoid
         # multiple call to glob_delete_entities)
         op = hook.CleanupDeletedEidsCacheOp.get_instance(cnx)
-        if not isinstance(eids, (set, frozenset)):
-            warn('[3.13] eids should be given as a set', DeprecationWarning,
-                 stacklevel=2)
-            eids = frozenset(eids)
         eids = eids - op._container
         op._container |= eids
         data_by_etype = {}  # values are [list of entities]
--- a/cubicweb/server/rqlannotation.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/rqlannotation.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,138 +19,11 @@
 code generation.
 """
 
-from __future__ import print_function
-
 from rql import BadRQLQuery
 from rql.nodes import Relation, VariableRef, Constant, Variable, Or
 from rql.utils import common_parent
 
 
-def _annotate_select(annotator, rqlst):
-    has_text_query = False
-    for subquery in rqlst.with_:
-        if annotator._annotate_union(subquery.query):
-            has_text_query = True
-    getrschema = annotator.schema.rschema
-    for var in rqlst.defined_vars.values():
-        stinfo = var.stinfo
-        if stinfo.get('ftirels'):
-            has_text_query = True
-        if stinfo['attrvar']:
-            stinfo['invariant'] = False
-            stinfo['principal'] = _select_main_var(stinfo['rhsrelations'])
-            continue
-        if stinfo['typerel'] is None:
-            # those particular queries should be executed using the system
-            # entities table unless there is some type restriction
-            if not stinfo['relations']:
-                # Any X, Any MAX(X)...
-                stinfo['invariant'] = True
-                stinfo['principal'] = None
-                continue
-            if (any(rel for rel in stinfo['relations']
-                    if rel.r_type == 'eid' and rel.operator() != '=')
-                    and not any(r for r in var.stinfo['relations'] - var.stinfo['rhsrelations']
-                                if r.r_type != 'eid'
-                                and (getrschema(r.r_type).inlined or getrschema(r.r_type).final))):
-                # Any X WHERE X eid > 2
-                stinfo['invariant'] = True
-                stinfo['principal'] = None
-                continue
-        if stinfo['selected'] and var.valuable_references() == 1 + bool(stinfo['constnode']):
-            # "Any X", "Any X, Y WHERE X attr Y"
-            stinfo['invariant'] = False
-            continue
-        joins = set()
-        invariant = False
-        for ref in var.references():
-            rel = ref.relation()
-            if rel is None or rel.is_types_restriction():
-                continue
-            lhs, rhs = rel.get_parts()
-            onlhs = ref is lhs
-            role = 'subject' if onlhs else 'object'
-            if rel.r_type == 'eid':
-                if not (onlhs and len(stinfo['relations']) > 1):
-                    break
-                if not stinfo['constnode']:
-                    joins.add((rel, role))
-                continue
-            elif rel.r_type == 'identity':
-                # identity can't be used as principal, so check other relation are used
-                # XXX explain rhs.operator == '='
-                if rhs.operator != '=' or len(stinfo['relations']) <= 1:
-                    break
-                joins.add((rel, role))
-                continue
-            rschema = getrschema(rel.r_type)
-            if rel.optional:
-                if rel in stinfo.get('optrelations', ()):
-                    # optional variable can't be invariant if this is the lhs
-                    # variable of an inlined relation
-                    if rel not in stinfo['rhsrelations'] and rschema.inlined:
-                        break
-                # variable used as main variable of an optional relation can't
-                # be invariant, unless we can use some other relation as
-                # reference for the outer join
-                elif not stinfo['constnode']:
-                    break
-                elif len(stinfo['relations']) == 2:
-                    if onlhs:
-                        ostinfo = rhs.children[0].variable.stinfo
-                    else:
-                        ostinfo = lhs.variable.stinfo
-                    if not (ostinfo.get('optcomparisons')
-                            or any(orel for orel in ostinfo['relations']
-                                   if orel.optional and orel is not rel)):
-                        break
-            if rschema.final or (onlhs and rschema.inlined):
-                if rschema.type != 'has_text':
-                    # need join anyway if the variable appears in a final or
-                    # inlined relation
-                    break
-                joins.add((rel, role))
-                continue
-            if not stinfo['constnode']:
-                if rschema.inlined and rel.neged(strict=True):
-                    # if relation is inlined, can't be invariant if that
-                    # variable is used anywhere else.
-                    # see 'Any P WHERE NOT N ecrit_par P, N eid 512':
-                    # sql for 'NOT N ecrit_par P' is 'N.ecrit_par is NULL' so P
-                    # can use N.ecrit_par as principal
-                    if (stinfo['selected'] or len(stinfo['relations']) > 1):
-                        break
-            joins.add((rel, role))
-        else:
-            # if there is at least one ambigous relation and no other to
-            # restrict types, can't be invariant since we need to filter out
-            # other types
-            if not annotator.is_ambiguous(var):
-                invariant = True
-        stinfo['invariant'] = invariant
-        if invariant and joins:
-            # remember rqlst/solutions analyze information
-            # we have to select a kindof "main" relation which will "extrajoins"
-            # the other
-            # priority should be given to relation which are not in inner queries
-            # (eg exists)
-            try:
-                stinfo['principal'] = principal = _select_principal(var.scope, joins)
-                if getrschema(principal.r_type).inlined:
-                    # the scope of the lhs variable must be equal or outer to the
-                    # rhs variable's scope (since it's retrieved from lhs's table)
-                    sstinfo = principal.children[0].variable.stinfo
-                    sstinfo['scope'] = common_parent(sstinfo['scope'], stinfo['scope']).scope
-            except CantSelectPrincipal:
-                stinfo['invariant'] = False
-    # see unittest_rqlannotation. test_has_text_security_cache_bug
-    # XXX probably more to do, but yet that work without more...
-    for col_alias in rqlst.aliases.values():
-        if col_alias.stinfo.get('ftirels'):
-            has_text_query = True
-    return has_text_query
-
-
 class CantSelectPrincipal(Exception):
     """raised when no 'principal' variable can be found"""
 
@@ -245,6 +118,7 @@
 
 
 class SQLGenAnnotator(object):
+
     def __init__(self, schema):
         self.schema = schema
         self.nfdomain = frozenset(eschema.type for eschema in schema.entities()
@@ -255,8 +129,8 @@
         job (read sql generation)
 
         a variable is tagged as invariant if:
-        * it's a non final variable
-        * it's not used as lhs in any final or inlined relation
+        * it is a non final variable
+        * it is not used as lhs in any final or inlined relation
         * there is no type restriction on this variable (either explicit in the
           syntax tree or because a solution for this variable has been removed
           due to security filtering)
@@ -267,7 +141,132 @@
     def _annotate_union(self, union):
         has_text_query = False
         for select in union.children:
-            if _annotate_select(self, select):
+            if self._annotate_select(select):
+                has_text_query = True
+        return has_text_query
+
+    def _annotate_select(self, rqlst):
+        has_text_query = False
+        for subquery in rqlst.with_:
+            if self._annotate_union(subquery.query):
+                has_text_query = True
+        getrschema = self.schema.rschema
+        for var in rqlst.defined_vars.values():
+            stinfo = var.stinfo
+            if stinfo.get('ftirels'):
+                has_text_query = True
+            if stinfo['attrvar']:
+                stinfo['invariant'] = False
+                stinfo['principal'] = _select_main_var(stinfo['rhsrelations'])
+                continue
+            if stinfo['typerel'] is None:
+                # those particular queries should be executed using the system
+                # entities table unless there is some type restriction
+                if not stinfo['relations']:
+                    # Any X, Any MAX(X)...
+                    stinfo['invariant'] = True
+                    stinfo['principal'] = None
+                    continue
+                if (any(rel for rel in stinfo['relations']
+                        if rel.r_type == 'eid' and rel.operator() != '=')
+                        and not any(r for r in var.stinfo['relations'] - var.stinfo['rhsrelations']
+                                    if r.r_type != 'eid'
+                                    and (getrschema(r.r_type).inlined
+                                         or getrschema(r.r_type).final))):
+                    # Any X WHERE X eid > 2
+                    stinfo['invariant'] = True
+                    stinfo['principal'] = None
+                    continue
+            if stinfo['selected'] and var.valuable_references() == 1 + bool(stinfo['constnode']):
+                # "Any X", "Any X, Y WHERE X attr Y"
+                stinfo['invariant'] = False
+                continue
+            joins = set()
+            invariant = False
+            for ref in var.references():
+                rel = ref.relation()
+                if rel is None or rel.is_types_restriction():
+                    continue
+                lhs, rhs = rel.get_parts()
+                onlhs = ref is lhs
+                role = 'subject' if onlhs else 'object'
+                if rel.r_type == 'eid':
+                    if not (onlhs and len(stinfo['relations']) > 1):
+                        break
+                    if not stinfo['constnode']:
+                        joins.add((rel, role))
+                    continue
+                elif rel.r_type == 'identity':
+                    # identity can't be used as principal, so check other relation are used
+                    # XXX explain rhs.operator == '='
+                    if rhs.operator != '=' or len(stinfo['relations']) <= 1:
+                        break
+                    joins.add((rel, role))
+                    continue
+                rschema = getrschema(rel.r_type)
+                if rel.optional:
+                    if rel in stinfo.get('optrelations', ()):
+                        # optional variable can't be invariant if this is the lhs
+                        # variable of an inlined relation
+                        if rel not in stinfo['rhsrelations'] and rschema.inlined:
+                            break
+                    # variable used as main variable of an optional relation can't
+                    # be invariant, unless we can use some other relation as
+                    # reference for the outer join
+                    elif not stinfo['constnode']:
+                        break
+                    elif len(stinfo['relations']) == 2:
+                        if onlhs:
+                            ostinfo = rhs.children[0].variable.stinfo
+                        else:
+                            ostinfo = lhs.variable.stinfo
+                        if not (ostinfo.get('optcomparisons')
+                                or any(orel for orel in ostinfo['relations']
+                                       if orel.optional and orel is not rel)):
+                            break
+                if rschema.final or (onlhs and rschema.inlined):
+                    if rschema.type != 'has_text':
+                        # need join anyway if the variable appears in a final or
+                        # inlined relation
+                        break
+                    joins.add((rel, role))
+                    continue
+                if not stinfo['constnode']:
+                    if rschema.inlined and rel.neged(strict=True):
+                        # if relation is inlined, can't be invariant if that
+                        # variable is used anywhere else.
+                        # see 'Any P WHERE NOT N ecrit_par P, N eid 512':
+                        # sql for 'NOT N ecrit_par P' is 'N.ecrit_par is NULL' so P
+                        # can use N.ecrit_par as principal
+                        if (stinfo['selected'] or len(stinfo['relations']) > 1):
+                            break
+                joins.add((rel, role))
+            else:
+                # if there is at least one ambigous relation and no other to
+                # restrict types, can't be invariant since we need to filter out
+                # other types
+                if not self.is_ambiguous(var):
+                    invariant = True
+            stinfo['invariant'] = invariant
+            if invariant and joins:
+                # remember rqlst/solutions analyze information
+                # we have to select a kindof "main" relation which will "extrajoins"
+                # the other
+                # priority should be given to relation which are not in inner queries
+                # (eg exists)
+                try:
+                    stinfo['principal'] = principal = _select_principal(var.scope, joins)
+                    if getrschema(principal.r_type).inlined:
+                        # the scope of the lhs variable must be equal or outer to the
+                        # rhs variable's scope (since it's retrieved from lhs's table)
+                        sstinfo = principal.children[0].variable.stinfo
+                        sstinfo['scope'] = common_parent(sstinfo['scope'], stinfo['scope']).scope
+                except CantSelectPrincipal:
+                    stinfo['invariant'] = False
+        # see unittest_rqlannotation. test_has_text_security_cache_bug
+        # XXX probably more to do, but yet that work without more...
+        for col_alias in rqlst.aliases.values():
+            if col_alias.stinfo.get('ftirels'):
                 has_text_query = True
         return has_text_query
 
--- a/cubicweb/server/schema2sql.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/schema2sql.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,14 +19,13 @@
 
 from hashlib import md5
 
-from six import string_types, text_type
-from six.moves import range
-
 from yams.constraints import (SizeConstraint, UniqueConstraint, Attribute,
                               NOW, TODAY)
 from logilab import database
 from logilab.common.decorators import monkeypatch
 
+from cubicweb.schema import constraint_name_for
+
 # default are usually not handled at the sql level. If you want them, set
 # SET_DEFAULT to True
 SET_DEFAULT = False
@@ -86,9 +85,9 @@
     given attributes of the entity schema (actually, the later may be a schema or a string).
     """
     # keep giving eschema instead of table name for bw compat
-    table = text_type(eschema)
+    table = str(eschema)
     # unique_index_name is used as name of CWUniqueConstraint, hence it should be unicode
-    return text_type(build_index_name(table, attrs, 'unique_'))
+    return build_index_name(table, attrs, 'unique_')
 
 
 def iter_unique_index_names(eschema):
@@ -187,7 +186,7 @@
     constraint. Maybe (None, None) if the constraint is not handled in the backend.
     """
     attr = rdef.rtype.type
-    cstrname = constraint.name_for(rdef)
+    cstrname = constraint_name_for(constraint, rdef)
     if constraint.type() == 'BoundaryConstraint':
         value = constraint_value_as_sql(constraint.boundary, dbhelper, prefix)
         return cstrname, '%s%s %s %s' % (prefix, attr, constraint.operator, value)
@@ -202,7 +201,7 @@
         return cstrname, ' AND '.join(condition)
     elif constraint.type() == 'StaticVocabularyConstraint':
         sample = next(iter(constraint.vocabulary()))
-        if not isinstance(sample, string_types):
+        if not isinstance(sample, str):
             values = ', '.join(str(word) for word in constraint.vocabulary())
         else:
             # XXX better quoting?
--- a/cubicweb/server/schemaserial.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/schemaserial.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,20 +17,16 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """functions for schema / permissions (de)serialization using RQL"""
 
-from __future__ import print_function
-
 import json
 import sys
 import sqlite3
 
-from six import PY2, text_type, string_types
-
 from logilab.common.shellutils import ProgressBar, DummyProgressBar
 
 from yams import BadSchemaDefinition, schema as schemamod, buildobjs as ybo, constraints
 
-from cubicweb import Binary
-from cubicweb.schema import (KNOWN_RPROPERTIES, CONSTRAINTS, ETYPE_NAME_MAP,
+from cubicweb import Binary, ETYPE_NAME_MAP
+from cubicweb.schema import (KNOWN_RPROPERTIES, CONSTRAINTS,
                              VIRTUAL_RTYPES)
 from cubicweb.server import sqlutils, schema2sql as y2sql
 
@@ -378,7 +374,7 @@
     cstrtypemap = {}
     rql = 'INSERT CWConstraintType X: X name %(ct)s'
     for cstrtype in CONSTRAINTS:
-        cstrtypemap[cstrtype] = execute(rql, {'ct': text_type(cstrtype)},
+        cstrtypemap[cstrtype] = execute(rql, {'ct': cstrtype},
                                         build_descr=False)[0][0]
         pb.update()
     # serialize relations
@@ -483,7 +479,7 @@
     for i, name in enumerate(unique_together):
         rschema = eschema.schema.rschema(name)
         rtype = 'T%d' % i
-        substs[rtype] = text_type(rschema.type)
+        substs[rtype] = rschema.type
         relations.append('C relations %s' % rtype)
         restrictions.append('%(rtype)s name %%(%(rtype)s)s' % {'rtype': rtype})
     relations = ', '.join(relations)
@@ -494,18 +490,10 @@
 
 
 def _ervalues(erschema):
-    try:
-        type_ = text_type(erschema.type)
-    except UnicodeDecodeError as e:
-        raise Exception("can't decode %s [was %s]" % (erschema.type, e))
-    try:
-        desc = text_type(erschema.description) or u''
-    except UnicodeDecodeError as e:
-        raise Exception("can't decode %s [was %s]" % (erschema.description, e))
     return {
-        'name': type_,
+        'name': erschema.type,
         'final': erschema.final,
-        'description': desc,
+        'description': erschema.description,
         }
 
 # rtype serialization
@@ -531,10 +519,7 @@
     values['final'] = rschema.final
     values['symmetric'] = rschema.symmetric
     values['inlined'] = rschema.inlined
-    if PY2 and isinstance(rschema.fulltext_container, str):
-        values['fulltext_container'] = unicode(rschema.fulltext_container)
-    else:
-        values['fulltext_container'] = rschema.fulltext_container
+    values['fulltext_container'] = rschema.fulltext_container
     relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
     return relations, values
 
@@ -547,7 +532,7 @@
 
 def crschema_relations_values(crschema):
     values = _ervalues(crschema)
-    values['rule'] = text_type(crschema.rule)
+    values['rule'] = crschema.rule
     # XXX why oh why?
     del values['final']
     relations = ['X %s %%(%s)s' % (attr, attr) for attr in sorted(values)]
@@ -593,8 +578,6 @@
             value = bool(value)
         elif prop == 'ordernum':
             value = int(value)
-        elif PY2 and isinstance(value, str):
-            value = unicode(value)
         if value is not None and prop == 'default':
             value = Binary.zpickle(value)
         values[amap.get(prop, prop)] = value
@@ -606,7 +589,7 @@
 def constraints2rql(cstrtypemap, constraints, rdefeid=None):
     for constraint in constraints:
         values = {'ct': cstrtypemap[constraint.type()],
-                  'value': text_type(constraint.serialize()),
+                  'value': constraint.serialize(),
                   'x': rdefeid} # when not specified, will have to be set by the caller
         yield 'INSERT CWConstraint X: X value %(value)s, X cstrtype CT, EDEF constrained_by X WHERE \
 CT eid %(ct)s, EDEF eid %(x)s', values
@@ -625,7 +608,7 @@
             # may occurs when modifying persistent schema
             continue
         for group_or_rqlexpr in grantedto:
-            if isinstance(group_or_rqlexpr, string_types):
+            if isinstance(group_or_rqlexpr, str):
                 # group
                 try:
                     yield ('SET X %s_permission Y WHERE Y eid %%(g)s, X eid %%(x)s' % action,
@@ -639,9 +622,9 @@
                 rqlexpr = group_or_rqlexpr
                 yield ('INSERT RQLExpression E: E expression %%(e)s, E exprtype %%(t)s, '
                        'E mainvars %%(v)s, X %s_permission E WHERE X eid %%(x)s' % action,
-                       {'e': text_type(rqlexpr.expression),
-                        'v': text_type(','.join(sorted(rqlexpr.mainvars))),
-                        't': text_type(rqlexpr.__class__.__name__)})
+                       {'e': rqlexpr.expression,
+                        'v': ','.join(sorted(rqlexpr.mainvars)),
+                        't': rqlexpr.__class__.__name__})
 
 # update functions
 
@@ -653,7 +636,7 @@
 def updaterschema2rql(rschema, eid):
     if rschema.rule:
         yield ('SET X rule %(r)s WHERE X eid %(x)s',
-               {'x': eid, 'r': text_type(rschema.rule)})
+               {'x': eid, 'r': rschema.rule})
     else:
         relations, values = rschema_relations_values(rschema)
         values['x'] = eid
--- a/cubicweb/server/serverconfig.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/serverconfig.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,15 +16,11 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """server.serverconfig definition"""
-from __future__ import print_function
 
-
-
+from io import StringIO
 import sys
 from os.path import join, exists
 
-from six.moves import StringIO
-
 import logilab.common.configuration as lgconfig
 from logilab.common.decorators import cached
 
--- a/cubicweb/server/serverctl.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/serverctl.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,18 +16,14 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """cubicweb-ctl commands and command handlers specific to the repository"""
-from __future__ import print_function
-
 # *ctl module should limit the number of import to be imported as quickly as
 # possible (for cubicweb-ctl reactivity, necessary for instance for usable bash
 # completion). So import locally in command helpers.
+import sched
 import sys
 import os
 from contextlib import contextmanager
 
-from six import string_types
-from six.moves import input
-
 from logilab.common.configuration import Configuration, merge_options
 from logilab.common.shellutils import ASK, generate_password
 
@@ -606,7 +602,7 @@
          {'short': 'p', 'type': 'string', 'metavar': '<new-password>',
           'default': None,
           'help': 'Use this password instead of prompt for one.\n'
-                  '/!\ THIS IS AN INSECURE PRACTICE /!\ \n'
+                  '/!\\ THIS IS AN INSECURE PRACTICE /!\\ \n'
                   'the password will appear in shell history'}
          ),
     )
@@ -1006,12 +1002,11 @@
     def run(self, args):
         from cubicweb.cwctl import init_cmdline_log_threshold
         from cubicweb.server.repository import Repository
-        from cubicweb.server.utils import scheduler
         config = ServerConfiguration.config_for(args[0])
         # Log to stdout, since the this command runs in the foreground.
         config.global_set_option('log-file', None)
         init_cmdline_log_threshold(config, self['loglevel'])
-        repo = Repository(config, scheduler())
+        repo = Repository(config, sched.scheduler())
         repo.bootstrap()
         try:
             repo.run_scheduler()
@@ -1095,8 +1090,7 @@
     for p in ('read', 'add', 'update', 'delete'):
         rule = perms.get(p)
         if rule:
-            perms[p] = tuple(str(x) if isinstance(x, string_types) else x
-                             for x in rule)
+            perms[p] = tuple(rule)
     return perms, perms in defaultrelperms or perms in defaulteperms
 
 
--- a/cubicweb/server/session.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/session.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,18 +17,12 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Repository users' and internal' sessions."""
 
-from __future__ import print_function
-
 import functools
 import sys
 from uuid import uuid4
-from warnings import warn
 from contextlib import contextmanager
 from logging import getLogger
 
-from six import text_type
-
-from logilab.common.deprecation import deprecated
 from logilab.common.registry import objectify_predicate
 
 from cubicweb import QueryError, ProgrammingError, schema, server
@@ -39,7 +33,6 @@
 
 
 NO_UNDO_TYPES = schema.SCHEMA_TYPES.copy()
-NO_UNDO_TYPES.add('CWCache')
 NO_UNDO_TYPES.add('CWSession')
 NO_UNDO_TYPES.add('CWDataImport')
 # is / is_instance_of are usually added by sql hooks except when using
@@ -73,15 +66,6 @@
     return req.vreg.config.repairing
 
 
-@deprecated('[3.17] use <object>.allow/deny_all_hooks_but instead')
-def hooks_control(obj, mode, *categories):
-    assert mode in (HOOKS_ALLOW_ALL, HOOKS_DENY_ALL)
-    if mode == HOOKS_ALLOW_ALL:
-        return obj.allow_all_hooks_but(*categories)
-    elif mode == HOOKS_DENY_ALL:
-        return obj.deny_all_hooks_but(*categories)
-
-
 class _hooks_control(object):
     """context manager to control activated hooks categories.
 
@@ -123,11 +107,6 @@
         self.cnx._hooks_categories = self.old_categories
 
 
-@deprecated('[3.17] use <object>.security_enabled instead')
-def security_enabled(obj, *args, **kwargs):
-    return obj.security_enabled(*args, **kwargs)
-
-
 class _security_enabled(object):
     """context manager to control security w/ session.execute,
 
@@ -390,11 +369,6 @@
     # shared data handling ###################################################
 
     @property
-    @deprecated('[3.25] use transaction_data or req.session.data', stacklevel=3)
-    def data(self):
-        return self.transaction_data
-
-    @property
     def rql_rewriter(self):
         return self._rewriter
 
@@ -409,24 +383,6 @@
         self.local_perm_cache.clear()
         self.rewriter = RQLRewriter(self)
 
-    @deprecated('[3.19] cnxset are automatically managed now.'
-                ' stop using explicit set and free.')
-    def set_cnxset(self):
-        pass
-
-    @deprecated('[3.19] cnxset are automatically managed now.'
-                ' stop using explicit set and free.')
-    def free_cnxset(self, ignoremode=False):
-        pass
-
-    @property
-    @contextmanager
-    @_open_only
-    @deprecated('[3.21] a cnxset is automatically set on __enter__ call now.'
-                ' stop using .ensure_cnx_set')
-    def ensure_cnx_set(self):
-        yield
-
     # Entity cache management #################################################
     #
     # The connection entity cache as held in cnx.transaction_data is removed at the
@@ -681,7 +637,7 @@
     def transaction_uuid(self, set=True):
         uuid = self.transaction_data.get('tx_uuid')
         if set and uuid is None:
-            self.transaction_data['tx_uuid'] = uuid = text_type(uuid4().hex)
+            self.transaction_data['tx_uuid'] = uuid = uuid4().hex
             self.repo.system_source.start_undoable_transaction(self, uuid)
         return uuid
 
@@ -702,12 +658,6 @@
         """Return entity type for the entity with id `eid`."""
         return self.repo.type_from_eid(eid, self)
 
-    @deprecated('[3.24] use entity_type(eid) instead')
-    @_open_only
-    def entity_metas(self, eid):
-        """Return a dictionary {type}) for the entity with id `eid`."""
-        return {'type': self.repo.type_from_eid(eid, self)}
-
     # core method #############################################################
 
     @_open_only
@@ -721,15 +671,10 @@
         return rset
 
     @_open_only
-    def rollback(self, free_cnxset=None, reset_pool=None):
+    def rollback(self):
         """rollback the current transaction"""
-        if free_cnxset is not None:
-            warn('[3.21] free_cnxset is now unneeded',
-                 DeprecationWarning, stacklevel=2)
-        if reset_pool is not None:
-            warn('[3.13] reset_pool is now unneeded',
-                 DeprecationWarning, stacklevel=2)
         cnxset = self.cnxset
+        debug = server.DEBUG & server.DBG_OPS
         assert cnxset is not None
         try:
             # by default, operations are executed with security turned off
@@ -742,19 +687,14 @@
                         self.critical('rollback error', exc_info=sys.exc_info())
                         continue
                 cnxset.rollback()
-                self.debug('rollback for transaction %s done', self)
+                if debug:
+                    print('rollback for transaction %s done' % self)
         finally:
             self.clear()
 
     @_open_only
-    def commit(self, free_cnxset=None, reset_pool=None):
+    def commit(self):
         """commit the current session's transaction"""
-        if free_cnxset is not None:
-            warn('[3.21] free_cnxset is now unneeded',
-                 DeprecationWarning, stacklevel=2)
-        if reset_pool is not None:
-            warn('[3.13] reset_pool is now unneeded',
-                 DeprecationWarning, stacklevel=2)
         assert self.cnxset is not None
         cstate = self.commit_state
         if cstate == 'uncommitable':
@@ -788,7 +728,8 @@
                                 print(operation)
                             operation.handle_event('precommit_event')
                     self.pending_operations[:] = processed
-                    self.debug('precommit transaction %s done', self)
+                    if debug:
+                        print('precommit transaction %s done' % self)
                 except BaseException:
                     # if error on [pre]commit:
                     #
@@ -834,7 +775,8 @@
                                 raise
                             self.critical('error while postcommit',
                                           exc_info=sys.exc_info())
-                self.debug('postcommit transaction %s done', self)
+                if debug:
+                    print('postcommit transaction %s done' % self)
                 return self.transaction_uuid(set=False)
         finally:
             self.clear()
--- a/cubicweb/server/sources/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/sources/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,13 +17,9 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """cubicweb server sources support"""
 
-from __future__ import print_function
-
 from time import time
 from logging import getLogger
 
-from six import text_type
-
 from logilab.common import configuration
 from logilab.common.textutils import unormalize
 
@@ -31,12 +27,13 @@
 
 from cubicweb import ValidationError, set_log_methods, server, _
 from cubicweb.server import SOURCE_TYPES
+from cubicweb.misc.source_highlight import highlight
 
 
 def dbg_st_search(uri, union, args, cachekey=None, prefix='rql for'):
     if server.DEBUG & server.DBG_RQL:
         global t
-        print('  %s %s source: %s' % (prefix, uri, repr(union.as_string())))
+        print(" ", prefix, uri, "source:", highlight(repr(union.as_string())[1:-1], 'RQL'))
         t = time()
         if server.DEBUG & server.DBG_MORE:
             print('    args', repr(args))
@@ -97,7 +94,7 @@
         self.uri = source_config.pop('uri')
         # unormalize to avoid non-ascii characters in logger's name, this will cause decoding error
         # on logging
-        set_log_methods(self, getLogger('cubicweb.sources.' + unormalize(text_type(self.uri))))
+        set_log_methods(self, getLogger('cubicweb.sources.' + unormalize(self.uri)))
         source_config.pop('type')
         self.config = self._check_config_dict(
             eid, source_config, raise_on_error=False)
@@ -155,7 +152,7 @@
                 except Exception as ex:
                     if not raise_on_error:
                         continue
-                    msg = text_type(ex)
+                    msg = str(ex)
                     raise ValidationError(eid, {role_name('config', 'subject'): msg})
             processed[optname] = value
         # cw < 3.10 bw compat
--- a/cubicweb/server/sources/datafeed.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/sources/datafeed.py	Fri Oct 18 23:39:03 2019 +0200
@@ -24,17 +24,14 @@
 from os.path import exists
 from datetime import datetime, timedelta
 from functools import partial
-
-from six.moves.urllib.parse import urlparse
-from six.moves.urllib.request import Request, build_opener, HTTPCookieProcessor
-from six.moves.urllib.error import HTTPError
-from six.moves.http_cookiejar import CookieJar
+from http.cookiejar import CookieJar
+from urllib.parse import urlparse
+from urllib.request import Request, build_opener, HTTPCookieProcessor
+from urllib.error import HTTPError
 
 from pytz import utc
 from lxml import etree
 
-from logilab.common.deprecation import deprecated
-
 from cubicweb import ObjectNotFound, ValidationError, SourceException, _
 from cubicweb.server.sources import AbstractSource
 from cubicweb.appobject import AppObject
@@ -365,20 +362,6 @@
 
 class DataFeedXMLParser(DataFeedParser):
 
-    @deprecated()
-    def process(self, url, raise_on_error=False):
-        """IDataFeedParser main entry point"""
-        try:
-            parsed = self.parse(url)
-        except Exception as ex:
-            if raise_on_error:
-                raise
-            self.import_log.record_error(str(ex))
-            return True
-        for args in parsed:
-            self.process_item(*args, raise_on_error=raise_on_error)
-        return False
-
     def parse(self, url):
         stream = self.retrieve_url(url)
         return self.parse_etree(etree.parse(stream).getroot())
--- a/cubicweb/server/sources/ldapfeed.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/sources/ldapfeed.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,8 +21,6 @@
 
 from datetime import datetime
 
-from six import PY2, string_types
-
 import ldap3
 
 from logilab.common.configuration import merge_options
@@ -341,15 +339,13 @@
             elif self.user_attrs.get(key) == 'modification_date':
                 itemdict[key] = datetime.strptime(value[0], '%Y%m%d%H%M%SZ')
             else:
-                if PY2 and value and isinstance(value[0], str):
-                    value = [unicode(val, 'utf-8', 'replace') for val in value]
                 if len(value) == 1:
                     itemdict[key] = value = value[0]
                 else:
                     itemdict[key] = value
         # we expect memberUid to be a list of user ids, make sure of it
         member = self.group_rev_attrs['member']
-        if isinstance(itemdict.get(member), string_types):
+        if isinstance(itemdict.get(member), str):
             itemdict[member] = [itemdict[member]]
         return itemdict
 
--- a/cubicweb/server/sources/native.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/sources/native.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,21 +17,17 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Adapters for native cubicweb sources."""
 
-from __future__ import print_function
-
 from threading import Lock
 from datetime import datetime
 from contextlib import contextmanager
 from os.path import basename
+import pickle
 import re
 import itertools
 import zipfile
 import logging
 import sys
 
-from six import PY2, text_type, string_types
-from six.moves import range, cPickle as pickle, zip
-
 from logilab.common.decorators import cached, clear_cache
 from logilab.common.configuration import Method
 from logilab.common.shellutils import getlogin, ASK
@@ -54,6 +50,7 @@
 from cubicweb.server.edition import EditedEntity
 from cubicweb.server.sources import AbstractSource, dbg_st_search, dbg_results
 from cubicweb.server.sources.rql2sql import SQLGenerator
+from cubicweb.misc.source_highlight import highlight
 from cubicweb.statsd_logger import statsd_timeit
 
 
@@ -71,12 +68,12 @@
         it's a function just so that it shows up in profiling
         """
         if server.DEBUG & server.DBG_SQL:
-            print('exec', query, args)
+            print('exec', highlight(query, "SQL"), args)
         try:
             self.cu.execute(str(query), args)
         except Exception as ex:
             print("sql: %r\n args: %s\ndbms message: %r" % (
-                query, args, ex.args[0]))
+                highlight(query, "SQL"), args, ex.args[0]))
             raise
 
     def fetchall(self):
@@ -121,12 +118,11 @@
 class _UndoException(Exception):
     """something went wrong during undoing"""
 
-    def __unicode__(self):
+    def __str__(self):
         """Called by the unicode builtin; should return a Unicode object
 
         Type of _UndoException message must be `unicode` by design in CubicWeb.
         """
-        assert isinstance(self.args[0], text_type)
         return self.args[0]
 
 
@@ -526,7 +522,7 @@
                 sql, qargs, cbs = self._rql_sqlgen.generate(union, args)
                 self._cache[cachekey] = sql, qargs, cbs
         args = self.merge_args(args, qargs)
-        assert isinstance(sql, string_types), repr(sql)
+        assert isinstance(sql, str), repr(sql)
         cursor = cnx.system_sql(sql, args)
         results = self.process_result(cursor, cnx, cbs)
         assert dbg_results(results)
@@ -581,7 +577,7 @@
             self.doexec(cnx, sql, attrs)
             if cnx.ertype_supports_undo(entity.cw_etype):
                 self._record_tx_action(cnx, 'tx_entity_actions', u'C',
-                                       etype=text_type(entity.cw_etype), eid=entity.eid)
+                                       etype=entity.cw_etype, eid=entity.eid)
 
     def update_entity(self, cnx, entity):
         """replace an entity in the source"""
@@ -590,7 +586,7 @@
             if cnx.ertype_supports_undo(entity.cw_etype):
                 changes = self._save_attrs(cnx, entity, attrs)
                 self._record_tx_action(cnx, 'tx_entity_actions', u'U',
-                                       etype=text_type(entity.cw_etype), eid=entity.eid,
+                                       etype=entity.cw_etype, eid=entity.eid,
                                        changes=self._binary(pickle.dumps(changes)))
             sql = self.sqlgen.update(SQL_PREFIX + entity.cw_etype, attrs,
                                      ['cw_eid'])
@@ -605,7 +601,7 @@
                          if (r.final or r.inlined) and r not in VIRTUAL_RTYPES]
                 changes = self._save_attrs(cnx, entity, attrs)
                 self._record_tx_action(cnx, 'tx_entity_actions', u'D',
-                                       etype=text_type(entity.cw_etype), eid=entity.eid,
+                                       etype=entity.cw_etype, eid=entity.eid,
                                        changes=self._binary(pickle.dumps(changes)))
             attrs = {'cw_eid': entity.eid}
             sql = self.sqlgen.delete(SQL_PREFIX + entity.cw_etype, attrs)
@@ -616,7 +612,7 @@
         self._add_relations(cnx, rtype, [(subject, object)], inlined)
         if cnx.ertype_supports_undo(rtype):
             self._record_tx_action(cnx, 'tx_relation_actions', u'A',
-                                   eid_from=subject, rtype=text_type(rtype), eid_to=object)
+                                   eid_from=subject, rtype=rtype, eid_to=object)
 
     def add_relations(self, cnx, rtype, subj_obj_list, inlined=False):
         """add a relations to the source"""
@@ -624,7 +620,7 @@
         if cnx.ertype_supports_undo(rtype):
             for subject, object in subj_obj_list:
                 self._record_tx_action(cnx, 'tx_relation_actions', u'A',
-                                       eid_from=subject, rtype=text_type(rtype), eid_to=object)
+                                       eid_from=subject, rtype=rtype, eid_to=object)
 
     def _add_relations(self, cnx, rtype, subj_obj_list, inlined=False):
         """add a relation to the source"""
@@ -668,7 +664,7 @@
         self._delete_relation(cnx, subject, rtype, object, rschema.inlined)
         if cnx.ertype_supports_undo(rtype):
             self._record_tx_action(cnx, 'tx_relation_actions', u'R',
-                                   eid_from=subject, rtype=text_type(rtype), eid_to=object)
+                                   eid_from=subject, rtype=rtype, eid_to=object)
 
     def _delete_relation(self, cnx, subject, rtype, object, inlined=False):
         """delete a relation from the source"""
@@ -690,7 +686,7 @@
         """
         cursor = cnx.cnxset.cu
         if server.DEBUG & server.DBG_SQL:
-            print('exec', query, args, cnx.cnxset.cnx)
+            print('exec', highlight(query, "SQL"), args, cnx.cnxset.cnx)
         try:
             # str(query) to avoid error if it's a unicode string
             cursor.execute(str(query), args)
@@ -733,14 +729,17 @@
                     mo = re.search(r'\bcstr[a-f0-9]{32}\b', arg)
                     if mo is not None:
                         # postgresql
-                        raise ViolatedConstraint(cnx, cstrname=mo.group(0))
+                        raise ViolatedConstraint(cnx, cstrname=mo.group(0),
+                                                 query=query)
                     if arg.startswith('CHECK constraint failed:'):
                         # sqlite3 (new)
-                        raise ViolatedConstraint(cnx, cstrname=arg.split(':', 1)[1].strip())
+                        raise ViolatedConstraint(cnx, cstrname=arg.split(':', 1)[1].strip(),
+                                                 query=query)
                     mo = re.match('^constraint (cstr.*) failed$', arg)
                     if mo is not None:
                         # sqlite3 (old)
-                        raise ViolatedConstraint(cnx, cstrname=mo.group(1))
+                        raise ViolatedConstraint(cnx, cstrname=mo.group(1),
+                                                 query=query)
             raise
         return cursor
 
@@ -750,7 +749,8 @@
         it's a function just so that it shows up in profiling
         """
         if server.DEBUG & server.DBG_SQL:
-            print('execmany', query, 'with', len(args), 'arguments', cnx.cnxset.cnx)
+            print('execmany', highlight(query, "SQL"), 'with', len(args), 'arguments',
+                  cnx.cnxset.cnx)
         cursor = cnx.cnxset.cu
         try:
             # str(query) to avoid error if it's a unicode string
@@ -845,7 +845,7 @@
         """add type and source info for an eid into the system table"""
         assert cnx.cnxset is not None
         # begin by inserting eid/type/source into the entities table
-        attrs = {'type': text_type(entity.cw_etype), 'eid': entity.eid}
+        attrs = {'type': entity.cw_etype, 'eid': entity.eid}
         self._handle_insert_entity_sql(cnx, self.sqlgen.insert('entities', attrs), attrs)
         # insert core relations: is, is_instance_of and cw_source
 
@@ -920,7 +920,7 @@
                     # only, and with no eid specified
                     assert actionfilters.get('action', 'C') in 'CUD'
                     assert 'eid' not in actionfilters
-                    tearestr['etype'] = text_type(val)
+                    tearestr['etype'] = val
                 elif key == 'eid':
                     # eid filter may apply to 'eid' of tx_entity_actions or to
                     # 'eid_from' OR 'eid_to' of tx_relation_actions
@@ -931,10 +931,10 @@
                         trarestr['eid_to'] = val
                 elif key == 'action':
                     if val in 'CUD':
-                        tearestr['txa_action'] = text_type(val)
+                        tearestr['txa_action'] = val
                     else:
                         assert val in 'AR'
-                        trarestr['txa_action'] = text_type(val)
+                        trarestr['txa_action'] = val
                 else:
                     raise AssertionError('unknow filter %s' % key)
             assert trarestr or tearestr, "can't only filter on 'public'"
@@ -968,11 +968,10 @@
 
     def tx_info(self, cnx, txuuid):
         """See :class:`cubicweb.repoapi.Connection.transaction_info`"""
-        return tx.Transaction(cnx, txuuid, *self._tx_info(cnx, text_type(txuuid)))
+        return tx.Transaction(cnx, txuuid, *self._tx_info(cnx, txuuid))
 
     def tx_actions(self, cnx, txuuid, public):
         """See :class:`cubicweb.repoapi.Connection.transaction_actions`"""
-        txuuid = text_type(txuuid)
         self._tx_info(cnx, txuuid)
         restr = {'tx_uuid': txuuid}
         if public:
@@ -1105,8 +1104,6 @@
             elif eschema.destination(rtype) in ('Bytes', 'Password'):
                 changes[column] = self._binary(value)
                 edited[rtype] = Binary(value)
-            elif PY2 and isinstance(value, str):
-                edited[rtype] = text_type(value, cnx.encoding, 'replace')
             else:
                 edited[rtype] = value
         # This must only be done after init_entitiy_caches : defered in calling functions
@@ -1146,14 +1143,14 @@
         try:
             sentity, oentity, rdef = _undo_rel_info(cnx, subj, rtype, obj)
         except _UndoException as ex:
-            errors.append(text_type(ex))
+            errors.append(str(ex))
         else:
             for role, entity in (('subject', sentity),
                                  ('object', oentity)):
                 try:
                     _undo_check_relation_target(entity, rdef, role)
                 except _UndoException as ex:
-                    errors.append(text_type(ex))
+                    errors.append(str(ex))
                     continue
         if not errors:
             self.repo.hm.call_hooks('before_add_relation', cnx,
@@ -1228,7 +1225,7 @@
         try:
             sentity, oentity, rdef = _undo_rel_info(cnx, subj, rtype, obj)
         except _UndoException as ex:
-            errors.append(text_type(ex))
+            errors.append(str(ex))
         else:
             rschema = rdef.rtype
             if rschema.inlined:
--- a/cubicweb/server/sources/rql2sql.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/sources/rql2sql.py	Fri Oct 18 23:39:03 2019 +0200
@@ -49,9 +49,6 @@
 
 import threading
 
-from six import PY2, text_type
-from six.moves import range
-
 from logilab.database import FunctionDescr, SQL_FUNCTIONS_REGISTRY
 
 from rql import BadRQLQuery, CoercionError
@@ -1517,8 +1514,6 @@
             return self.keyword_map[value]()
         if constant.type == 'Substitute':
             _id = value
-            if PY2 and isinstance(_id, text_type):
-                _id = _id.encode()
         else:
             _id = str(id(constant)).replace('-', '', 1)
             self._query_attrs[_id] = value
--- a/cubicweb/server/sources/storages.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/sources/storages.py	Fri Oct 18 23:39:03 2019 +0200
@@ -23,8 +23,6 @@
 from contextlib import contextmanager
 import tempfile
 
-from six import PY2, PY3, text_type, binary_type
-
 from logilab.common import nullobject
 
 from yams.schema import role_name
@@ -113,15 +111,8 @@
 class BytesFileSystemStorage(Storage):
     """store Bytes attribute value on the file system"""
     def __init__(self, defaultdir, fsencoding=_marker, wmode=0o444):
-        if PY3:
-            if not isinstance(defaultdir, text_type):
-                raise TypeError('defaultdir must be a unicode object in python 3')
-            if fsencoding is not _marker:
-                raise ValueError('fsencoding is no longer supported in python 3')
-        else:
-            self.fsencoding = fsencoding or 'utf-8'
-            if isinstance(defaultdir, text_type):
-                defaultdir = defaultdir.encode(fsencoding)
+        if fsencoding is not _marker:
+            raise ValueError('fsencoding is no longer supported in python 3')
         self.default_directory = defaultdir
         # extra umask to use when creating file
         # 0444 as in "only allow read bit in permission"
@@ -160,7 +151,7 @@
             if binary is not None:
                 fd, fpath = self.new_fs_path(entity, attr)
                 # bytes storage used to store file's path
-                binary_obj = Binary(fpath if PY2 else fpath.encode('utf-8'))
+                binary_obj = Binary(fpath.encode('utf-8'))
                 entity.cw_edited.edited_attribute(attr, binary_obj)
                 self._writecontent(fd, binary)
                 AddFileOp.get_instance(entity._cw).add_data(fpath)
@@ -204,7 +195,7 @@
                 entity.cw_edited.edited_attribute(attr, None)
             else:
                 # register the new location for the file.
-                binary_obj = Binary(fpath if PY2 else fpath.encode('utf-8'))
+                binary_obj = Binary(fpath.encode('utf-8'))
                 entity.cw_edited.edited_attribute(attr, binary_obj)
         if oldpath is not None and oldpath != fpath:
             # Mark the old file as useless so the file will be removed at
@@ -224,19 +215,17 @@
         # available. Keeping the extension is useful for example in the case of
         # PIL processing that use filename extension to detect content-type, as
         # well as providing more understandable file names on the fs.
-        if PY2:
-            attr = attr.encode('ascii')
         basename = [str(entity.eid), attr]
         name = entity.cw_attr_metadata(attr, 'name')
         if name is not None:
-            basename.append(name.encode(self.fsencoding) if PY2 else name)
+            basename.append(name)
         fd, fspath = uniquify_path(self.default_directory,
                                '_'.join(basename))
         if fspath is None:
             msg = entity._cw._('failed to uniquify path (%s, %s)') % (
                 self.default_directory, '_'.join(basename))
             raise ValidationError(entity.eid, {role_name(attr, 'subject'): msg})
-        assert isinstance(fspath, str)  # bytes on py2, unicode on py3
+        assert isinstance(fspath, str)
         return fd, fspath
 
     def current_fs_path(self, entity, attr):
@@ -251,11 +240,8 @@
         if rawvalue is None: # no previous value
             return None
         fspath = sysource._process_value(rawvalue, cu.description[0],
-                                         binarywrap=binary_type)
-        if PY3:
-            fspath = fspath.decode('utf-8')
-        assert isinstance(fspath, str)  # bytes on py2, unicode on py3
-        return fspath
+                                         binarywrap=bytes)
+        return fspath.decode('utf-8')
 
     def migrate_entity(self, entity, attribute):
         """migrate an entity attribute to the storage"""
@@ -274,7 +260,7 @@
 class AddFileOp(hook.DataOperationMixIn, hook.Operation):
     def rollback_event(self):
         for filepath in self.get_data():
-            assert isinstance(filepath, str)  # bytes on py2, unicode on py3
+            assert isinstance(filepath, str)
             try:
                 unlink(filepath)
             except Exception as ex:
@@ -283,7 +269,7 @@
 class DeleteFileOp(hook.DataOperationMixIn, hook.Operation):
     def postcommit_event(self):
         for filepath in self.get_data():
-            assert isinstance(filepath, str)  # bytes on py2, unicode on py3
+            assert isinstance(filepath, str)
             try:
                 unlink(filepath)
             except Exception as ex:
--- a/cubicweb/server/sqlutils.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/sqlutils.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,8 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """SQL utilities functions and classes."""
 
-from __future__ import print_function
-
 import os
 import sys
 import re
@@ -27,14 +25,10 @@
 from logging import getLogger
 from datetime import time, datetime, timedelta
 
-from six import string_types, text_type
-from six.moves import filter
-
 from pytz import utc
 
 from logilab import database as db, common as lgc
 from logilab.common.shellutils import ProgressBar, DummyProgressBar
-from logilab.common.deprecation import deprecated
 from logilab.common.logging_ext import set_log_methods
 from logilab.common.date import utctime, utcdatetime, strptime
 from logilab.database.sqlgen import SQLGenerator
@@ -53,7 +47,7 @@
     env = os.environ.copy()
     for key, value in (extra_env or {}).items():
         env.setdefault(key, value)
-    if isinstance(cmd, string_types):
+    if isinstance(cmd, str):
         print(cmd)
         return subprocess.call(cmd, shell=True, env=env)
     else:
@@ -82,7 +76,7 @@
     else:
         execute = cursor_or_execute
     sqlstmts_as_string = False
-    if isinstance(sqlstmts, string_types):
+    if isinstance(sqlstmts, str):
         sqlstmts_as_string = True
         sqlstmts = sqlstmts.split(delimiter)
     if withpb:
@@ -237,21 +231,6 @@
         self.cnx = self._source.get_connection()
         self.cu = self.cnx.cursor()
 
-    @deprecated('[3.19] use .cu instead')
-    def __getitem__(self, uri):
-        assert uri == 'system'
-        return self.cu
-
-    @deprecated('[3.19] use repo.system_source instead')
-    def source(self, uid):
-        assert uid == 'system'
-        return self._source
-
-    @deprecated('[3.19] use .cnx instead')
-    def connection(self, uid):
-        assert uid == 'system'
-        return self.cnx
-
 
 class SqliteConnectionWrapper(ConnectionWrapper):
     """Sqlite specific connection wrapper: close the connection each time it's
@@ -491,7 +470,7 @@
                 for row, rowdesc in zip(rset, rset.description):
                     for cellindex, (value, vtype) in enumerate(zip(row, rowdesc)):
                         if vtype in ('TZDatetime', 'Date', 'Datetime') \
-                           and isinstance(value, text_type):
+                           and isinstance(value, str):
                             found_date = True
                             value = value.rsplit('.', 1)[0]
                             try:
@@ -500,7 +479,7 @@
                                 row[cellindex] = strptime(value, '%Y-%m-%d')
                             if vtype == 'TZDatetime':
                                 row[cellindex] = row[cellindex].replace(tzinfo=utc)
-                        if vtype == 'Time' and isinstance(value, text_type):
+                        if vtype == 'Time' and isinstance(value, str):
                             found_date = True
                             try:
                                 row[cellindex] = strptime(value, '%H:%M:%S')
@@ -533,7 +512,7 @@
                 self.values.add(value)
 
         def finalize(self):
-            return ', '.join(text_type(v) for v in self.values)
+            return ', '.join(str(v) for v in self.values)
 
     cnx.create_aggregate("GROUP_CONCAT", 1, group_concat)
 
--- a/cubicweb/server/ssplanner.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/ssplanner.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,8 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """plan execution of rql queries on a single source"""
 
-from six import text_type
-
 from rql.stmts import Union, Select
 from rql.nodes import Constant, Relation
 
@@ -55,7 +53,7 @@
                 value = rhs.eval(plan.args)
                 eschema = edef.entity.e_schema
                 attrtype = eschema.subjrels[rtype].objects(eschema)[0]
-                if attrtype == 'Password' and isinstance(value, text_type):
+                if attrtype == 'Password' and isinstance(value, str):
                     value = value.encode('UTF8')
                 edef.edited_attribute(rtype, value)
             elif str(rhs) in to_build:
--- a/cubicweb/server/test/data-migractions/cubes/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-__import__('pkg_resources').declare_namespace(__name__)
--- a/cubicweb/server/test/data-migractions/cubes/fakecustomtype/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,50 +0,0 @@
-# pylint: disable-msg=W0622
-"""cubicweb-fakeemail packaging information"""
-
-modname = 'fakecustomtype'
-distname = "cubicweb-%s" % modname
-
-numversion = (1, 0, 0)
-version = '.'.join(str(num) for num in numversion)
-
-license = 'LGPL'
-author = "Logilab"
-author_email = "contact@logilab.fr"
-web = 'http://www.cubicweb.org/project/%s' % distname
-description = "whatever"
-classifiers = [
-           'Environment :: Web Environment',
-           'Framework :: CubicWeb',
-           'Programming Language :: Python',
-           'Programming Language :: JavaScript',
-]
-
-# used packages
-__depends__ = {'cubicweb': '>= 3.19.0',
-               }
-
-
-# packaging ###
-
-from os import listdir as _listdir
-from os.path import join, isdir
-from glob import glob
-
-THIS_CUBE_DIR = join('share', 'cubicweb', 'cubes', modname)
-
-def listdir(dirpath):
-    return [join(dirpath, fname) for fname in _listdir(dirpath)
-            if fname[0] != '.' and not fname.endswith('.pyc')
-            and not fname.endswith('~')
-            and not isdir(join(dirpath, fname))]
-
-data_files = [
-    # common files
-    [THIS_CUBE_DIR, [fname for fname in glob('*.py') if fname != 'setup.py']],
-    ]
-# check for possible extended cube layout
-for dirname in ('entities', 'views', 'sobjects', 'hooks', 'schema', 'data', 'i18n', 'migration', 'wdoc'):
-    if isdir(dirname):
-        data_files.append([join(THIS_CUBE_DIR, dirname), listdir(dirname)])
-# Note: here, you'll need to add subdirectories if you want
-# them to be included in the debian package
--- a/cubicweb/server/test/data-migractions/cubes/fakecustomtype/schema.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-
-from yams.buildobjs import EntityType, make_type
-
-Numeric = make_type('Numeric')
-
-class Location(EntityType):
-    num = Numeric(scale=10, precision=18)
--- a/cubicweb/server/test/data-migractions/cubes/fakecustomtype/site_cubicweb.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-from yams import register_base_type
-from logilab.database import get_db_helper
-from logilab.database.sqlgen import SQLExpression
-
-_NUMERIC_PARAMETERS = {'scale': 0, 'precision': None}
-register_base_type('Numeric', _NUMERIC_PARAMETERS)
-
-# Add the datatype to the helper mapping
-pghelper = get_db_helper('postgres')
-
-
-def pg_numeric_sqltype(rdef):
-    """Return a PostgreSQL column type corresponding to rdef
-    """
-    return 'numeric(%s, %s)' % (rdef.precision, rdef.scale)
-
-pghelper.TYPE_MAPPING['Numeric'] = pg_numeric_sqltype
--- a/cubicweb/server/test/data-migractions/cubes/fakeemail/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-# pylint: disable-msg=W0622
-"""cubicweb-fakeemail packaging information"""
-
-modname = 'fakeemail'
-distname = "cubicweb-%s" % modname
-
-numversion = (1, 10, 0)
-version = '.'.join(str(num) for num in numversion)
-
-license = 'LGPL'
-author = "Logilab"
-author_email = "contact@logilab.fr"
-web = 'http://www.cubicweb.org/project/%s' % distname
-description = "email component for the CubicWeb framework"
-classifiers = [
-           'Environment :: Web Environment',
-           'Framework :: CubicWeb',
-           'Programming Language :: Python',
-           'Programming Language :: JavaScript',
-]
-
-# used packages
-__depends__ = {'cubicweb': '>= 3.19.0',
-               'cubicweb-file': '>= 1.9.0',
-               'logilab-common': '>= 0.58.3',
-               }
-__recommends__ = {'cubicweb-comment': None}
-
-
-# packaging ###
-
-from os import listdir as _listdir
-from os.path import join, isdir
-from glob import glob
-
-THIS_CUBE_DIR = join('share', 'cubicweb', 'cubes', modname)
-
-def listdir(dirpath):
-    return [join(dirpath, fname) for fname in _listdir(dirpath)
-            if fname[0] != '.' and not fname.endswith('.pyc')
-            and not fname.endswith('~')
-            and not isdir(join(dirpath, fname))]
-
-data_files = [
-    # common files
-    [THIS_CUBE_DIR, [fname for fname in glob('*.py') if fname != 'setup.py']],
-    ]
-# check for possible extended cube layout
-for dirname in ('entities', 'views', 'sobjects', 'hooks', 'schema', 'data', 'i18n', 'migration', 'wdoc'):
-    if isdir(dirname):
-        data_files.append([join(THIS_CUBE_DIR, dirname), listdir(dirname)])
-# Note: here, you'll need to add subdirectories if you want
-# them to be included in the debian package
--- a/cubicweb/server/test/data-migractions/cubes/fakeemail/schema.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,86 +0,0 @@
-"""entity/relation schemas to store email in an cubicweb instance
-
-:organization: Logilab
-:copyright: 2006-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
-"""
-
-from cubicweb import _
-
-# pylint: disable-msg=E0611,F0401
-from yams.buildobjs import (SubjectRelation, RelationType, EntityType,
-                            String, Datetime, Int, RelationDefinition)
-from yams.reader import context
-
-from cubicweb.schema import ERQLExpression
-
-
-class Email(EntityType):
-    """electronic mail"""
-    subject   = String(fulltextindexed=True)
-    date      = Datetime(description=_('UTC time on which the mail was sent'))
-    messageid = String(required=True, indexed=True)
-    headers   = String(description=_('raw headers'))
-
-    sender     = SubjectRelation('EmailAddress', cardinality='?*')
-    # an email with only Bcc is acceptable, don't require any recipients
-    recipients = SubjectRelation('EmailAddress')
-    cc         = SubjectRelation('EmailAddress')
-
-    parts       = SubjectRelation('EmailPart', cardinality='*1', composite='subject')
-    attachment  = SubjectRelation('File')
-
-    reply_to    = SubjectRelation('Email', cardinality='?*')
-    cites       = SubjectRelation('Email')
-    in_thread   = SubjectRelation('EmailThread', cardinality='?*')
-
-
-class EmailPart(EntityType):
-    """an email attachment"""
-    __permissions__ = {
-        'read':   ('managers', 'users', 'guests',), # XXX if E parts X, U has_read_permission E
-        'add':    ('managers', ERQLExpression('E parts X, U has_update_permission E'),),
-        'delete': ('managers', ERQLExpression('E parts X, U has_update_permission E')),
-        'update': ('managers', 'owners',),
-        }
-
-    content  = String(fulltextindexed=True)
-    content_format = String(required=True, maxsize=50)
-    ordernum = Int(required=True)
-    alternative = SubjectRelation('EmailPart', symmetric=True)
-
-
-class EmailThread(EntityType):
-    """discussion thread"""
-    title = String(required=True, indexed=True, fulltextindexed=True)
-    see_also = SubjectRelation('EmailThread')
-    forked_from = SubjectRelation('EmailThread', cardinality='?*')
-
-class parts(RelationType):
-    """ """
-    fulltext_container = 'subject'
-
-class sender(RelationType):
-    """ """
-    inlined = True
-
-class in_thread(RelationType):
-    """ """
-    inlined = True
-
-class reply_to(RelationType):
-    """ """
-    inlined = True
-
-class generated_by(RelationType):
-    """mark an entity as generated from an email"""
-    cardinality = '?*'
-    subject = ('TrInfo',)
-    object = 'Email'
-
-# if comment is installed
-if 'Comment' in context.defined:
-    class comment_generated_by(RelationDefinition):
-        subject = 'Comment'
-        name = 'generated_by'
-        object = 'Email'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_basket/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_basket/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,21 @@
+from yams.buildobjs import EntityType, RelationDefinition, String, RichString
+from cubicweb.schema import ERQLExpression
+
+
+class Basket(EntityType):
+    """a basket contains a set of other entities"""
+    __permissions__ = {
+        'read':   ('managers', ERQLExpression('X owned_by U'),),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', 'owners',),
+        'update': ('managers', 'owners',),
+        }
+
+    name = String(required=True, indexed=True, internationalizable=True,
+                  maxsize=128)
+    description = RichString(fulltextindexed=True)
+
+
+class in_basket(RelationDefinition):
+    subject = '*'
+    object = 'Basket'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_card/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_card/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+from yams.buildobjs import EntityType, String, RichString
+
+
+class Card(EntityType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', 'users'),
+        'delete': ('managers', 'owners'),
+        'update': ('managers', 'owners',),
+        }
+
+    title = String(required=True, fulltextindexed=True, maxsize=256)
+    synopsis = String(fulltextindexed=True, maxsize=512,
+                      description=("an abstract for this card"))
+    content = RichString(fulltextindexed=True, internationalizable=True,
+                         default_format='text/rest')
+    wikiid = String(maxsize=64, unique=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_comment/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_comment/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,26 @@
+from yams.buildobjs import (EntityType, RelationType, SubjectRelation,
+                            RichString)
+from cubicweb.schema import RRQLExpression
+
+
+class Comment(EntityType):
+    """a comment is a reply about another entity"""
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests',),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', 'owners',),
+        'update': ('managers', 'owners',),
+        }
+    content = RichString(required=True, fulltextindexed=True)
+    comments = SubjectRelation('Comment', cardinality='1*', composite='object')
+
+
+class comments(RelationType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', RRQLExpression('S owned_by U'),),
+        }
+    inlined = True
+    composite = 'object'
+    cardinality = '1*'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_fakecustomtype/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,50 @@
+# pylint: disable-msg=W0622
+"""cubicweb-fakeemail packaging information"""
+
+modname = 'fakecustomtype'
+distname = "cubicweb-%s" % modname
+
+numversion = (1, 0, 0)
+version = '.'.join(str(num) for num in numversion)
+
+license = 'LGPL'
+author = "Logilab"
+author_email = "contact@logilab.fr"
+web = 'http://www.cubicweb.org/project/%s' % distname
+description = "whatever"
+classifiers = [
+           'Environment :: Web Environment',
+           'Framework :: CubicWeb',
+           'Programming Language :: Python',
+           'Programming Language :: JavaScript',
+]
+
+# used packages
+__depends__ = {'cubicweb': '>= 3.19.0',
+               }
+
+
+# packaging ###
+
+from os import listdir as _listdir
+from os.path import join, isdir
+from glob import glob
+
+THIS_CUBE_DIR = join('share', 'cubicweb', 'cubes', modname)
+
+def listdir(dirpath):
+    return [join(dirpath, fname) for fname in _listdir(dirpath)
+            if fname[0] != '.' and not fname.endswith('.pyc')
+            and not fname.endswith('~')
+            and not isdir(join(dirpath, fname))]
+
+data_files = [
+    # common files
+    [THIS_CUBE_DIR, [fname for fname in glob('*.py') if fname != 'setup.py']],
+    ]
+# check for possible extended cube layout
+for dirname in ('entities', 'views', 'sobjects', 'hooks', 'schema', 'data', 'i18n', 'migration', 'wdoc'):
+    if isdir(dirname):
+        data_files.append([join(THIS_CUBE_DIR, dirname), listdir(dirname)])
+# Note: here, you'll need to add subdirectories if you want
+# them to be included in the debian package
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_fakecustomtype/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,7 @@
+
+from yams.buildobjs import EntityType, make_type
+
+Numeric = make_type('Numeric')
+
+class Location(EntityType):
+    num = Numeric(scale=10, precision=18)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_fakecustomtype/site_cubicweb.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+from yams import register_base_type
+from logilab.database import get_db_helper
+from logilab.database.sqlgen import SQLExpression
+
+_NUMERIC_PARAMETERS = {'scale': 0, 'precision': None}
+register_base_type('Numeric', _NUMERIC_PARAMETERS)
+
+# Add the datatype to the helper mapping
+pghelper = get_db_helper('postgres')
+
+
+def pg_numeric_sqltype(rdef):
+    """Return a PostgreSQL column type corresponding to rdef
+    """
+    return 'numeric(%s, %s)' % (rdef.precision, rdef.scale)
+
+pghelper.TYPE_MAPPING['Numeric'] = pg_numeric_sqltype
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_fakeemail/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,53 @@
+# pylint: disable-msg=W0622
+"""cubicweb-fakeemail packaging information"""
+
+modname = 'fakeemail'
+distname = "cubicweb-%s" % modname
+
+numversion = (1, 10, 0)
+version = '.'.join(str(num) for num in numversion)
+
+license = 'LGPL'
+author = "Logilab"
+author_email = "contact@logilab.fr"
+web = 'http://www.cubicweb.org/project/%s' % distname
+description = "email component for the CubicWeb framework"
+classifiers = [
+           'Environment :: Web Environment',
+           'Framework :: CubicWeb',
+           'Programming Language :: Python',
+           'Programming Language :: JavaScript',
+]
+
+# used packages
+__depends__ = {'cubicweb': '>= 3.19.0',
+               'cubicweb-file': '>= 1.9.0',
+               'logilab-common': '>= 0.58.3',
+               }
+__recommends__ = {'cubicweb-comment': None}
+
+
+# packaging ###
+
+from os import listdir as _listdir
+from os.path import join, isdir
+from glob import glob
+
+THIS_CUBE_DIR = join('share', 'cubicweb', 'cubes', modname)
+
+def listdir(dirpath):
+    return [join(dirpath, fname) for fname in _listdir(dirpath)
+            if fname[0] != '.' and not fname.endswith('.pyc')
+            and not fname.endswith('~')
+            and not isdir(join(dirpath, fname))]
+
+data_files = [
+    # common files
+    [THIS_CUBE_DIR, [fname for fname in glob('*.py') if fname != 'setup.py']],
+    ]
+# check for possible extended cube layout
+for dirname in ('entities', 'views', 'sobjects', 'hooks', 'schema', 'data', 'i18n', 'migration', 'wdoc'):
+    if isdir(dirname):
+        data_files.append([join(THIS_CUBE_DIR, dirname), listdir(dirname)])
+# Note: here, you'll need to add subdirectories if you want
+# them to be included in the debian package
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_fakeemail/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,86 @@
+"""entity/relation schemas to store email in an cubicweb instance
+
+:organization: Logilab
+:copyright: 2006-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+
+from cubicweb import _
+
+# pylint: disable-msg=E0611,F0401
+from yams.buildobjs import (SubjectRelation, RelationType, EntityType,
+                            String, Datetime, Int, RelationDefinition)
+from yams.reader import context
+
+from cubicweb.schema import ERQLExpression
+
+
+class Email(EntityType):
+    """electronic mail"""
+    subject   = String(fulltextindexed=True)
+    date      = Datetime(description=_('UTC time on which the mail was sent'))
+    messageid = String(required=True, indexed=True)
+    headers   = String(description=_('raw headers'))
+
+    sender     = SubjectRelation('EmailAddress', cardinality='?*')
+    # an email with only Bcc is acceptable, don't require any recipients
+    recipients = SubjectRelation('EmailAddress')
+    cc         = SubjectRelation('EmailAddress')
+
+    parts       = SubjectRelation('EmailPart', cardinality='*1', composite='subject')
+    attachment  = SubjectRelation('File')
+
+    reply_to    = SubjectRelation('Email', cardinality='?*')
+    cites       = SubjectRelation('Email')
+    in_thread   = SubjectRelation('EmailThread', cardinality='?*')
+
+
+class EmailPart(EntityType):
+    """an email attachment"""
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests',), # XXX if E parts X, U has_read_permission E
+        'add':    ('managers', ERQLExpression('E parts X, U has_update_permission E'),),
+        'delete': ('managers', ERQLExpression('E parts X, U has_update_permission E')),
+        'update': ('managers', 'owners',),
+        }
+
+    content  = String(fulltextindexed=True)
+    content_format = String(required=True, maxsize=50)
+    ordernum = Int(required=True)
+    alternative = SubjectRelation('EmailPart', symmetric=True)
+
+
+class EmailThread(EntityType):
+    """discussion thread"""
+    title = String(required=True, indexed=True, fulltextindexed=True)
+    see_also = SubjectRelation('EmailThread')
+    forked_from = SubjectRelation('EmailThread', cardinality='?*')
+
+class parts(RelationType):
+    """ """
+    fulltext_container = 'subject'
+
+class sender(RelationType):
+    """ """
+    inlined = True
+
+class in_thread(RelationType):
+    """ """
+    inlined = True
+
+class reply_to(RelationType):
+    """ """
+    inlined = True
+
+class generated_by(RelationType):
+    """mark an entity as generated from an email"""
+    cardinality = '?*'
+    subject = ('TrInfo',)
+    object = 'Email'
+
+# if comment is installed
+if 'Comment' in context.defined:
+    class comment_generated_by(RelationDefinition):
+        subject = 'Comment'
+        name = 'generated_by'
+        object = 'Email'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_file/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_file/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,27 @@
+from yams.buildobjs import EntityType, String, Bytes, RichString
+
+
+class File(EntityType):
+    """a downloadable file which may contains binary data"""
+    title = String(fulltextindexed=True, maxsize=256)
+    data = Bytes(required=True, description='file to upload')
+    data_format = String(
+        required=True, maxsize=128,
+        description=('MIME type of the file. Should be dynamically set '
+                     'at upload time.'))
+    data_encoding = String(
+        maxsize=32,
+        description=('encoding of the file when it applies (e.g. text). '
+                     'Should be dynamically set at upload time.'))
+    data_name = String(
+        required=True, fulltextindexed=True,
+        description=('name of the file. Should be dynamically set '
+                     'at upload time.'))
+    data_hash = String(
+        maxsize=256,  # max len of currently available hash alg + prefix is 140
+        description=('hash of the file. May be set at upload time.'),
+        __permissions__={'read': ('managers', 'users', 'guests'),
+                         'add': (),
+                         'update': ()})
+    description = RichString(fulltextindexed=True, internationalizable=True,
+                             default_format='text/rest')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_localperms/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_localperms/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,42 @@
+from yams.buildobjs import EntityType, RelationType, RelationDefinition, String
+from cubicweb.schema import PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS
+
+
+class CWPermission(EntityType):
+    """entity type that may be used to construct some advanced security
+    configuration
+    """
+    __permissions__ = PUB_SYSTEM_ENTITY_PERMS
+
+    name = String(required=True, indexed=True, internationalizable=True,
+                  maxsize=100, description=(
+                      'name or identifier of the permission'))
+    label = String(required=True, internationalizable=True, maxsize=100,
+                   description=('distinct label to distinguate between other '
+                                'permission entity of the same name'))
+
+
+class granted_permission(RelationType):
+    """explicitly granted permission on an entity"""
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    # XXX cardinality = '*1'
+
+
+class require_permission(RelationType):
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+
+
+class require_group(RelationDefinition):
+    """groups to which the permission is granted"""
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    subject = 'CWPermission'
+    object = 'CWGroup'
+
+
+class has_group_permission(RelationDefinition):
+    """short cut relation for 'U in_group G, P require_group G' for efficiency
+    reason. This relation is set automatically, you should not set this.
+    """
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    subject = 'CWUser'
+    object = 'CWPermission'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_tag/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/cubicweb_tag/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+from yams.buildobjs import EntityType, String, SubjectRelation, RelationType
+
+
+class Tag(EntityType):
+    """tags are used by users to mark entities.
+    When you include the Tag entity, all application specific entities
+    may then be tagged using the "tags" relation.
+    """
+    name = String(required=True, fulltextindexed=True, unique=True,
+                  maxsize=128)
+    # when using this component, add the Tag tag X relation for each type that
+    # should be taggeable
+    tags = SubjectRelation('Tag', description="tagged objects")
+
+
+class tags(RelationType):
+    """indicates that an entity is classified by a given tag"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/migratedapp/cubicweb_basket/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/migratedapp/cubicweb_basket/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,21 @@
+from yams.buildobjs import EntityType, RelationDefinition, String, RichString
+from cubicweb.schema import ERQLExpression
+
+
+class Basket(EntityType):
+    """a basket contains a set of other entities"""
+    __permissions__ = {
+        'read':   ('managers', ERQLExpression('X owned_by U'),),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', 'owners',),
+        'update': ('managers', 'owners',),
+        }
+
+    name = String(required=True, indexed=True, internationalizable=True,
+                  maxsize=128)
+    description = RichString(fulltextindexed=True)
+
+
+class in_basket(RelationDefinition):
+    subject = '*'
+    object = 'Basket'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/migratedapp/cubicweb_card/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/migratedapp/cubicweb_card/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+from yams.buildobjs import EntityType, String, RichString
+
+
+class Card(EntityType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', 'users'),
+        'delete': ('managers', 'owners'),
+        'update': ('managers', 'owners',),
+        }
+
+    title = String(required=True, fulltextindexed=True, maxsize=256)
+    synopsis = String(fulltextindexed=True, maxsize=512,
+                      description=("an abstract for this card"))
+    content = RichString(fulltextindexed=True, internationalizable=True,
+                         default_format='text/rest')
+    wikiid = String(maxsize=64, unique=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/migratedapp/cubicweb_comment/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/migratedapp/cubicweb_comment/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,26 @@
+from yams.buildobjs import (EntityType, RelationType, SubjectRelation,
+                            RichString)
+from cubicweb.schema import RRQLExpression
+
+
+class Comment(EntityType):
+    """a comment is a reply about another entity"""
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests',),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', 'owners',),
+        'update': ('managers', 'owners',),
+        }
+    content = RichString(required=True, fulltextindexed=True)
+    comments = SubjectRelation('Comment', cardinality='1*', composite='object')
+
+
+class comments(RelationType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', RRQLExpression('S owned_by U'),),
+        }
+    inlined = True
+    composite = 'object'
+    cardinality = '1*'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/migratedapp/cubicweb_file/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/migratedapp/cubicweb_file/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,27 @@
+from yams.buildobjs import EntityType, String, Bytes, RichString
+
+
+class File(EntityType):
+    """a downloadable file which may contains binary data"""
+    title = String(fulltextindexed=True, maxsize=256)
+    data = Bytes(required=True, description='file to upload')
+    data_format = String(
+        required=True, maxsize=128,
+        description=('MIME type of the file. Should be dynamically set '
+                     'at upload time.'))
+    data_encoding = String(
+        maxsize=32,
+        description=('encoding of the file when it applies (e.g. text). '
+                     'Should be dynamically set at upload time.'))
+    data_name = String(
+        required=True, fulltextindexed=True,
+        description=('name of the file. Should be dynamically set '
+                     'at upload time.'))
+    data_hash = String(
+        maxsize=256,  # max len of currently available hash alg + prefix is 140
+        description=('hash of the file. May be set at upload time.'),
+        __permissions__={'read': ('managers', 'users', 'guests'),
+                         'add': (),
+                         'update': ()})
+    description = RichString(fulltextindexed=True, internationalizable=True,
+                             default_format='text/rest')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/migratedapp/cubicweb_localperms/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/migratedapp/cubicweb_localperms/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,42 @@
+from yams.buildobjs import EntityType, RelationType, RelationDefinition, String
+from cubicweb.schema import PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS
+
+
+class CWPermission(EntityType):
+    """entity type that may be used to construct some advanced security
+    configuration
+    """
+    __permissions__ = PUB_SYSTEM_ENTITY_PERMS
+
+    name = String(required=True, indexed=True, internationalizable=True,
+                  maxsize=100, description=(
+                      'name or identifier of the permission'))
+    label = String(required=True, internationalizable=True, maxsize=100,
+                   description=('distinct label to distinguate between other '
+                                'permission entity of the same name'))
+
+
+class granted_permission(RelationType):
+    """explicitly granted permission on an entity"""
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    # XXX cardinality = '*1'
+
+
+class require_permission(RelationType):
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+
+
+class require_group(RelationDefinition):
+    """groups to which the permission is granted"""
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    subject = 'CWPermission'
+    object = 'CWGroup'
+
+
+class has_group_permission(RelationDefinition):
+    """short cut relation for 'U in_group G, P require_group G' for efficiency
+    reason. This relation is set automatically, you should not set this.
+    """
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    subject = 'CWUser'
+    object = 'CWPermission'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/migratedapp/cubicweb_tag/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data-migractions/migratedapp/cubicweb_tag/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+from yams.buildobjs import EntityType, String, SubjectRelation, RelationType
+
+
+class Tag(EntityType):
+    """tags are used by users to mark entities.
+    When you include the Tag entity, all application specific entities
+    may then be tagged using the "tags" relation.
+    """
+    name = String(required=True, fulltextindexed=True, unique=True,
+                  maxsize=128)
+    # when using this component, add the Tag tag X relation for each type that
+    # should be taggeable
+    tags = SubjectRelation('Tag', description="tagged objects")
+
+
+class tags(RelationType):
+    """indicates that an entity is classified by a given tag"""
--- a/cubicweb/server/test/data-schema2sql/schema/Company.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/test/data-schema2sql/schema/Company.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,6 +18,8 @@
 from yams.buildobjs import EntityType, RelationType, RelationDefinition, \
      SubjectRelation, String
 
+from cubicweb import _
+
 class Company(EntityType):
     name = String()
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data/cubicweb_basket/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data/cubicweb_basket/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,21 @@
+from yams.buildobjs import EntityType, RelationDefinition, String, RichString
+from cubicweb.schema import ERQLExpression
+
+
+class Basket(EntityType):
+    """a basket contains a set of other entities"""
+    __permissions__ = {
+        'read':   ('managers', ERQLExpression('X owned_by U'),),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', 'owners',),
+        'update': ('managers', 'owners',),
+        }
+
+    name = String(required=True, indexed=True, internationalizable=True,
+                  maxsize=128)
+    description = RichString(fulltextindexed=True)
+
+
+class in_basket(RelationDefinition):
+    subject = '*'
+    object = 'Basket'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data/cubicweb_card/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data/cubicweb_card/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+from yams.buildobjs import EntityType, String, RichString
+
+
+class Card(EntityType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', 'users'),
+        'delete': ('managers', 'owners'),
+        'update': ('managers', 'owners',),
+        }
+
+    title = String(required=True, fulltextindexed=True, maxsize=256)
+    synopsis = String(fulltextindexed=True, maxsize=512,
+                      description=("an abstract for this card"))
+    content = RichString(fulltextindexed=True, internationalizable=True,
+                         default_format='text/rest')
+    wikiid = String(maxsize=64, unique=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data/cubicweb_comment/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data/cubicweb_comment/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,26 @@
+from yams.buildobjs import (EntityType, RelationType, SubjectRelation,
+                            RichString)
+from cubicweb.schema import RRQLExpression
+
+
+class Comment(EntityType):
+    """a comment is a reply about another entity"""
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests',),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', 'owners',),
+        'update': ('managers', 'owners',),
+        }
+    content = RichString(required=True, fulltextindexed=True)
+    comments = SubjectRelation('Comment', cardinality='1*', composite='object')
+
+
+class comments(RelationType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', RRQLExpression('S owned_by U'),),
+        }
+    inlined = True
+    composite = 'object'
+    cardinality = '1*'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data/cubicweb_file/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data/cubicweb_file/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,27 @@
+from yams.buildobjs import EntityType, String, Bytes, RichString
+
+
+class File(EntityType):
+    """a downloadable file which may contains binary data"""
+    title = String(fulltextindexed=True, maxsize=256)
+    data = Bytes(required=True, description='file to upload')
+    data_format = String(
+        required=True, maxsize=128,
+        description=('MIME type of the file. Should be dynamically set '
+                     'at upload time.'))
+    data_encoding = String(
+        maxsize=32,
+        description=('encoding of the file when it applies (e.g. text). '
+                     'Should be dynamically set at upload time.'))
+    data_name = String(
+        required=True, fulltextindexed=True,
+        description=('name of the file. Should be dynamically set '
+                     'at upload time.'))
+    data_hash = String(
+        maxsize=256,  # max len of currently available hash alg + prefix is 140
+        description=('hash of the file. May be set at upload time.'),
+        __permissions__={'read': ('managers', 'users', 'guests'),
+                         'add': (),
+                         'update': ()})
+    description = RichString(fulltextindexed=True, internationalizable=True,
+                             default_format='text/rest')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data/cubicweb_localperms/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data/cubicweb_localperms/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,42 @@
+from yams.buildobjs import EntityType, RelationType, RelationDefinition, String
+from cubicweb.schema import PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS
+
+
+class CWPermission(EntityType):
+    """entity type that may be used to construct some advanced security
+    configuration
+    """
+    __permissions__ = PUB_SYSTEM_ENTITY_PERMS
+
+    name = String(required=True, indexed=True, internationalizable=True,
+                  maxsize=100, description=(
+                      'name or identifier of the permission'))
+    label = String(required=True, internationalizable=True, maxsize=100,
+                   description=('distinct label to distinguate between other '
+                                'permission entity of the same name'))
+
+
+class granted_permission(RelationType):
+    """explicitly granted permission on an entity"""
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    # XXX cardinality = '*1'
+
+
+class require_permission(RelationType):
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+
+
+class require_group(RelationDefinition):
+    """groups to which the permission is granted"""
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    subject = 'CWPermission'
+    object = 'CWGroup'
+
+
+class has_group_permission(RelationDefinition):
+    """short cut relation for 'U in_group G, P require_group G' for efficiency
+    reason. This relation is set automatically, you should not set this.
+    """
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    subject = 'CWUser'
+    object = 'CWPermission'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data/cubicweb_tag/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/data/cubicweb_tag/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+from yams.buildobjs import EntityType, String, SubjectRelation, RelationType
+
+
+class Tag(EntityType):
+    """tags are used by users to mark entities.
+    When you include the Tag entity, all application specific entities
+    may then be tagged using the "tags" relation.
+    """
+    name = String(required=True, fulltextindexed=True, unique=True,
+                  maxsize=128)
+    # when using this component, add the Tag tag X relation for each type that
+    # should be taggeable
+    tags = SubjectRelation('Tag', description="tagged objects")
+
+
+class tags(RelationType):
+    """indicates that an entity is classified by a given tag"""
--- a/cubicweb/server/test/unittest_checkintegrity.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/test/unittest_checkintegrity.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,15 +16,10 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
+from io import StringIO
 import sys
 import unittest
 
-from six import PY2
-if PY2:
-    from StringIO import StringIO
-else:
-    from io import StringIO
-
 from cubicweb import devtools  # noqa: E402
 from cubicweb.devtools.testlib import CubicWebTC  # noqa: E402
 from cubicweb.server.checkintegrity import check, check_indexes, reindex_entities  # noqa: E402
--- a/cubicweb/server/test/unittest_hook.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/test/unittest_hook.py	Fri Oct 18 23:39:03 2019 +0200
@@ -30,10 +30,6 @@
 
 class OperationsTC(CubicWebTC):
 
-    def setUp(self):
-        CubicWebTC.setUp(self)
-        self.hm = self.repo.hm
-
     def test_late_operation(self):
         with self.admin_access.repo_cnx() as cnx:
             l1 = hook.LateOperation(cnx)
@@ -63,9 +59,11 @@
     pass
 
 
-config = devtools.TestServerConfiguration('data', __file__)
-config.bootstrap_cubes()
-schema = config.load_schema()
+def setUpModule():
+    global config, schema
+    config = devtools.TestServerConfiguration('data', __file__)
+    config.bootstrap_cubes()
+    schema = config.load_schema()
 
 def tearDownModule(*args):
     global config, schema
--- a/cubicweb/server/test/unittest_ldapsource.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/test/unittest_ldapsource.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 Those tests expect to have slapd, python-ldap3 and ldapscripts packages installed.
 """
 
-from __future__ import print_function
-
 import os
 import sys
 import shutil
@@ -31,9 +29,6 @@
 import unittest
 from os.path import join
 
-from six import string_types
-from six.moves import range
-
 from cubicweb import AuthenticationError, ValidationError
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.devtools.httptest import get_available_port
@@ -180,7 +175,7 @@
         """
         modcmd = ['dn: %s' % dn, 'changetype: add']
         for key, values in mods.items():
-            if isinstance(values, string_types):
+            if isinstance(values, str):
                 values = [values]
             for value in values:
                 modcmd.append('%s: %s' % (key, value))
@@ -200,7 +195,7 @@
         modcmd = ['dn: %s' % dn, 'changetype: modify']
         for (kind, key), values in mods.items():
             modcmd.append('%s: %s' % (kind, key))
-            if isinstance(values, string_types):
+            if isinstance(values, str):
                 values = [values]
             for value in values:
                 modcmd.append('%s: %s' % (key, value))
--- a/cubicweb/server/test/unittest_migractions.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/test/unittest_migractions.py	Fri Oct 18 23:39:03 2019 +0200
@@ -22,6 +22,7 @@
 import sys
 from datetime import date
 from contextlib import contextmanager
+from tempfile import TemporaryDirectory
 
 from logilab.common import tempattr
 
@@ -30,11 +31,13 @@
 from cubicweb import (ConfigurationError, ValidationError,
                       ExecutionError, Binary)
 from cubicweb.devtools import startpgcluster, stoppgcluster
-from cubicweb.devtools.testlib import CubicWebTC, TemporaryDirectory
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.schema import constraint_name_for
 from cubicweb.server.sqlutils import SQL_PREFIX
 from cubicweb.server.migractions import ServerMigrationHelper
 from cubicweb.server.sources import storages
 from cubicweb.server.schema2sql import build_index_name
+from cubicweb.server.test.unittest_storage import StorageTC
 
 import cubicweb.devtools
 
@@ -59,8 +62,6 @@
 
 class MigrationConfig(cubicweb.devtools.TestServerConfiguration):
     default_sources = cubicweb.devtools.DEFAULT_PSQL_SOURCES
-    CUBES_PATH = cubicweb.devtools.TestServerConfiguration.CUBES_PATH + [
-        osp.join(HERE, 'data-migractions', 'cubes')]
 
 
 class MigrationTC(CubicWebTC):
@@ -94,10 +95,6 @@
             global migrschema
             migrschema = config.load_schema()
 
-    def setUp(self):
-        self.configcls.cls_adjust_sys_path()
-        super(MigrationTC, self).setUp()
-
     def tearDown(self):
         super(MigrationTC, self).tearDown()
         self.repo.vreg['etypes'].clear_caches()
@@ -616,7 +613,7 @@
             self.assertEqual(len(constraints), 1, constraints)
             rdef = migrschema['promo'].rdefs['Personne', 'String']
             cstr = rdef.constraint_by_type('StaticVocabularyConstraint')
-            self.assertIn(cstr.name_for(rdef), constraints)
+            self.assertIn(constraint_name_for(cstr, rdef), constraints)
 
     def _erqlexpr_rset(self, cnx, action, ertype):
         rql = 'RQLExpression X WHERE ET is CWEType, ET %s_permission X, ET name %%(name)s' % action
@@ -702,8 +699,8 @@
                                  ['Bookmark', 'EmailThread', 'Folder', 'Note'])
                 self.assertEqual(sorted(schema['see_also'].objects()),
                                  ['Bookmark', 'EmailThread', 'Folder', 'Note'])
-                from cubes.fakeemail.__pkginfo__ import version as email_version
-                from cubes.file.__pkginfo__ import version as file_version
+                from cubicweb_fakeemail.__pkginfo__ import version as email_version
+                from cubicweb_file.__pkginfo__ import version as file_version
                 self.assertEqual(
                     cnx.execute('Any V WHERE X value V, X pkey "system.version.fakeemail"')[0][0],
                     email_version)
@@ -838,6 +835,20 @@
                 storages.unset_attribute_storage(self.repo, 'Personne', 'photo')
 
 
+class MigrationStorageCommandsTC(StorageTC, MigrationCommandsTC):
+
+    def test_change_bfss_path(self):
+        with self.mh() as (cnx, mh):
+            file1 = mh.cmd_create_entity('File', data_name=u"foo.pdf",
+                                         data=Binary(b"xxx"), data_format=u'text/plain')
+            mh.commit()
+            current_dir = osp.dirname(self.fspath(cnx, file1))
+
+            mh.update_bfss_path(current_dir, 'loutre', commit=True)
+
+            self.assertEqual(u'loutre', osp.dirname(self.fspath(cnx, file1)))
+
+
 class MigrationCommandsComputedTC(MigrationTC):
     """ Unit tests for computed relations and attributes
     """
--- a/cubicweb/server/test/unittest_postgres.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/test/unittest_postgres.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,8 +19,6 @@
 from datetime import datetime
 from threading import Thread
 
-from six.moves import range
-
 import logilab.database as lgdb
 from cubicweb import ValidationError
 from cubicweb.devtools import PostgresApptestConfiguration, startpgcluster, stoppgcluster
--- a/cubicweb/server/test/unittest_querier.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/test/unittest_querier.py	Fri Oct 18 23:39:03 2019 +0200
@@ -25,8 +25,6 @@
 
 import pytz
 
-from six import PY2, integer_types, binary_type, text_type
-
 from rql import BadRQLQuery
 from rql.utils import register_function, FunctionDescr
 
@@ -134,8 +132,8 @@
 
     def assertRQLEqual(self, expected, got):
         from rql import parse
-        self.assertMultiLineEqual(text_type(parse(expected)),
-                                  text_type(parse(got)))
+        self.assertMultiLineEqual(str(parse(expected)),
+                                  str(parse(got)))
 
     def test_preprocess_security(self):
         with self.user_groups_session('users') as cnx:
@@ -176,7 +174,7 @@
                                            'ET': 'CWEType', 'ETN': 'String'}])
             rql, solutions = partrqls[1]
             self.assertRQLEqual(rql,  'Any ETN,X WHERE X is ET, ET name ETN, ET is CWEType, '
-                                'X is IN(BaseTransition, Bookmark, CWAttribute, CWCache, CWComputedRType, '
+                                'X is IN(BaseTransition, Bookmark, CWAttribute, CWComputedRType, '
                                 '        CWConstraint, CWConstraintType, CWEType, CWGroup, CWPermission, CWProperty, '
                                 '        CWRType, CWRelation, CWSource, CWUniqueTogetherConstraint, CWUser, Card, '
                                 '        Comment, Division, Email, EmailPart, EmailThread, ExternalUri, File, Folder, '
@@ -188,7 +186,6 @@
                                    {'X': 'Card', 'ETN': 'String', 'ET': 'CWEType'},
                                    {'X': 'Comment', 'ETN': 'String', 'ET': 'CWEType'},
                                    {'X': 'Division', 'ETN': 'String', 'ET': 'CWEType'},
-                                   {'X': 'CWCache', 'ETN': 'String', 'ET': 'CWEType'},
                                    {'X': 'CWComputedRType', 'ETN': 'String', 'ET': 'CWEType'},
                                    {'X': 'CWConstraint', 'ETN': 'String', 'ET': 'CWEType'},
                                    {'X': 'CWConstraintType', 'ETN': 'String', 'ET': 'CWEType'},
@@ -266,9 +263,6 @@
         self.assertEqual(rset.description[0][0], 'Datetime')
         rset = self.qexecute('Any %(x)s', {'x': 1})
         self.assertEqual(rset.description[0][0], 'Int')
-        if PY2:
-            rset = self.qexecute('Any %(x)s', {'x': long(1)})
-            self.assertEqual(rset.description[0][0], 'Int')
         rset = self.qexecute('Any %(x)s', {'x': True})
         self.assertEqual(rset.description[0][0], 'Boolean')
         rset = self.qexecute('Any %(x)s', {'x': 1.0})
@@ -328,7 +322,7 @@
     def test_typed_eid(self):
         # should return an empty result set
         rset = self.qexecute('Any X WHERE X eid %(x)s', {'x': '1'})
-        self.assertIsInstance(rset[0][0], integer_types)
+        self.assertIsInstance(rset[0][0], int)
 
     def test_bytes_storage(self):
         feid = self.qexecute('INSERT File X: X data_name "foo.pdf", '
@@ -615,16 +609,16 @@
         self.assertListEqual(rset.rows,
                               [[u'description_format', 13],
                                [u'description', 14],
-                               [u'name', 19],
-                               [u'created_by', 45],
-                               [u'creation_date', 45],
-                               [u'cw_source', 45],
-                               [u'cwuri', 45],
-                               [u'in_basket', 45],
-                               [u'is', 45],
-                               [u'is_instance_of', 45],
-                               [u'modification_date', 45],
-                               [u'owned_by', 45]])
+                               [u'name', 18],
+                               [u'created_by', 44],
+                               [u'creation_date', 44],
+                               [u'cw_source', 44],
+                               [u'cwuri', 44],
+                               [u'in_basket', 44],
+                               [u'is', 44],
+                               [u'is_instance_of', 44],
+                               [u'modification_date', 44],
+                               [u'owned_by', 44]])
 
     def test_select_aggregat_having_dumb(self):
         # dumb but should not raise an error
@@ -904,14 +898,14 @@
         rset = self.qexecute('Any X, "toto" ORDERBY X WHERE X is CWGroup')
         self.assertEqual(rset.rows,
                          [list(x) for x in zip((2,3,4,5), ('toto','toto','toto','toto',))])
-        self.assertIsInstance(rset[0][1], text_type)
+        self.assertIsInstance(rset[0][1], str)
         self.assertEqual(rset.description,
                          list(zip(('CWGroup', 'CWGroup', 'CWGroup', 'CWGroup'),
                                   ('String', 'String', 'String', 'String',))))
         rset = self.qexecute('Any X, %(value)s ORDERBY X WHERE X is CWGroup', {'value': 'toto'})
         self.assertEqual(rset.rows,
                          list(map(list, zip((2,3,4,5), ('toto','toto','toto','toto',)))))
-        self.assertIsInstance(rset[0][1], text_type)
+        self.assertIsInstance(rset[0][1], str)
         self.assertEqual(rset.description,
                          list(zip(('CWGroup', 'CWGroup', 'CWGroup', 'CWGroup'),
                                   ('String', 'String', 'String', 'String',))))
@@ -1074,10 +1068,10 @@
     def test_insert_4ter(self):
         peid = self.qexecute("INSERT Personne X: X nom 'bidule'")[0][0]
         seid = self.qexecute("INSERT Societe Y: Y nom 'toto', X travaille Y WHERE X eid %(x)s",
-                             {'x': text_type(peid)})[0][0]
+                             {'x': str(peid)})[0][0]
         self.assertEqual(len(self.qexecute('Any X, Y WHERE X travaille Y')), 1)
         self.qexecute("INSERT Personne X: X nom 'chouette', X travaille Y WHERE Y eid %(x)s",
-                      {'x': text_type(seid)})
+                      {'x': str(seid)})
         self.assertEqual(len(self.qexecute('Any X, Y WHERE X travaille Y')), 2)
 
     def test_insert_5(self):
@@ -1283,7 +1277,7 @@
         rset = self.qexecute("INSERT Personne X, Societe Y: X nom 'bidule', Y nom 'toto'")
         eid1, eid2 = rset[0][0], rset[0][1]
         self.qexecute("SET X travaille Y WHERE X eid %(x)s, Y eid %(y)s",
-                      {'x': text_type(eid1), 'y': text_type(eid2)})
+                      {'x': str(eid1), 'y': str(eid2)})
         rset = self.qexecute('Any X, Y WHERE X travaille Y')
         self.assertEqual(len(rset.rows), 1)
 
@@ -1333,7 +1327,7 @@
         eid1, eid2 = rset[0][0], rset[0][1]
         rset = self.qexecute("SET X travaille Y WHERE X eid %(x)s, Y eid %(y)s, "
                             "NOT EXISTS(Z ecrit_par X)",
-                            {'x': text_type(eid1), 'y': text_type(eid2)})
+                            {'x': str(eid1), 'y': str(eid2)})
         self.assertEqual(tuplify(rset.rows), [(eid1, eid2)])
 
     def test_update_query_error(self):
@@ -1380,7 +1374,7 @@
             cursor = cnx.cnxset.cu
             cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
                            % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
-            passwd = binary_type(cursor.fetchone()[0])
+            passwd = bytes(cursor.fetchone()[0])
             self.assertEqual(passwd, crypt_password('toto', passwd))
         rset = self.qexecute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
                             {'pwd': Binary(passwd)})
@@ -1397,7 +1391,7 @@
             cursor = cnx.cnxset.cu
             cursor.execute("SELECT %supassword from %sCWUser WHERE %slogin='bob'"
                            % (SQL_PREFIX, SQL_PREFIX, SQL_PREFIX))
-            passwd = binary_type(cursor.fetchone()[0])
+            passwd = bytes(cursor.fetchone()[0])
             self.assertEqual(passwd, crypt_password('tutu', passwd))
             rset = cnx.execute("Any X WHERE X is CWUser, X login 'bob', X upassword %(pwd)s",
                                {'pwd': Binary(passwd)})
--- a/cubicweb/server/test/unittest_repository.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/test/unittest_repository.py	Fri Oct 18 23:39:03 2019 +0200
@@ -22,8 +22,6 @@
 import logging
 import unittest
 
-from six.moves import range
-
 from yams.constraints import UniqueConstraint
 from yams import register_base_type, unregister_base_type
 
--- a/cubicweb/server/test/unittest_rql2sql.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/test/unittest_rql2sql.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,7 +16,6 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """unit tests for module cubicweb.server.sources.rql2sql"""
-from __future__ import print_function
 
 import sys
 import unittest
--- a/cubicweb/server/test/unittest_security.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,692 +0,0 @@
-# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""functional tests for server'security"""
-
-from six.moves import range
-
-from logilab.common.testlib import unittest_main
-
-from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb import Unauthorized, ValidationError, QueryError, Binary
-from cubicweb.schema import ERQLExpression
-from cubicweb.server.querier import get_local_checks, check_relations_read_access
-from cubicweb.server.utils import _CRYPTO_CTX
-
-
-class BaseSecurityTC(CubicWebTC):
-
-    def setup_database(self):
-        super(BaseSecurityTC, self).setup_database()
-        with self.admin_access.client_cnx() as cnx:
-            self.create_user(cnx, u'iaminusersgrouponly')
-            hash = _CRYPTO_CTX.encrypt('oldpassword', scheme='des_crypt')
-            self.create_user(cnx, u'oldpassword', password=Binary(hash.encode('ascii')))
-
-
-class LowLevelSecurityFunctionTC(BaseSecurityTC):
-
-    def test_check_relation_read_access(self):
-        rql = u'Personne U WHERE U nom "managers"'
-        rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0]
-        nom = self.repo.schema['Personne'].rdef('nom')
-        with self.temporary_permissions((nom, {'read': ('users', 'managers')})):
-            with self.admin_access.repo_cnx() as cnx:
-                self.repo.vreg.solutions(cnx, rqlst, None)
-                check_relations_read_access(cnx, rqlst, {})
-            with self.new_access(u'anon').repo_cnx() as cnx:
-                self.assertRaises(Unauthorized,
-                                  check_relations_read_access,
-                                  cnx, rqlst, {})
-                self.assertRaises(Unauthorized, cnx.execute, rql)
-
-    def test_get_local_checks(self):
-        rql = u'Personne U WHERE U nom "managers"'
-        rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0]
-        with self.temporary_permissions(Personne={'read': ('users', 'managers')}):
-            with self.admin_access.repo_cnx() as cnx:
-                self.repo.vreg.solutions(cnx, rqlst, None)
-                solution = rqlst.solutions[0]
-                localchecks = get_local_checks(cnx, rqlst, solution)
-                self.assertEqual({}, localchecks)
-            with self.new_access(u'anon').repo_cnx() as cnx:
-                self.assertRaises(Unauthorized,
-                                  get_local_checks,
-                                  cnx, rqlst, solution)
-                self.assertRaises(Unauthorized, cnx.execute, rql)
-
-    def test_upassword_not_selectable(self):
-        with self.admin_access.repo_cnx() as cnx:
-            self.assertRaises(Unauthorized,
-                              cnx.execute, 'Any X,P WHERE X is CWUser, X upassword P')
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            self.assertRaises(Unauthorized,
-                              cnx.execute, 'Any X,P WHERE X is CWUser, X upassword P')
-
-    def test_update_password(self):
-        """Ensure that if a user's password is stored with a deprecated hash,
-        it will be updated on next login
-        """
-        with self.repo.internal_cnx() as cnx:
-            oldhash = cnx.system_sql("SELECT cw_upassword FROM cw_CWUser "
-                                     "WHERE cw_login = 'oldpassword'").fetchone()[0]
-            oldhash = self.repo.system_source.binary_to_str(oldhash)
-            self.repo.authenticate_user(cnx, 'oldpassword', password='oldpassword')
-            newhash = cnx.system_sql("SELECT cw_upassword FROM cw_CWUser "
-                                     "WHERE cw_login = 'oldpassword'").fetchone()[0]
-            newhash = self.repo.system_source.binary_to_str(newhash)
-            self.assertNotEqual(oldhash, newhash)
-            self.assertTrue(newhash.startswith(b'$6$'))
-            self.repo.authenticate_user(cnx, 'oldpassword', password='oldpassword')
-            newnewhash = cnx.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE "
-                                        "cw_login = 'oldpassword'").fetchone()[0]
-            newnewhash = self.repo.system_source.binary_to_str(newnewhash)
-            self.assertEqual(newhash, newnewhash)
-
-
-class SecurityRewritingTC(BaseSecurityTC):
-    def hijack_source_execute(self):
-        def syntax_tree_search(*args, **kwargs):
-            self.query = (args, kwargs)
-            return []
-        self.repo.system_source.syntax_tree_search = syntax_tree_search
-
-    def tearDown(self):
-        self.repo.system_source.__dict__.pop('syntax_tree_search', None)
-        super(SecurityRewritingTC, self).tearDown()
-
-    def test_not_relation_read_security(self):
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            cnx.user.groups  # fill the cache before screwing syntax_tree_search
-            self.hijack_source_execute()
-            cnx.execute('Any U WHERE NOT A todo_by U, A is Affaire')
-            self.assertEqual(self.query[0][1].as_string(),
-                             'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
-            cnx.execute('Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
-            self.assertEqual(self.query[0][1].as_string(),
-                             'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
-
-
-class SecurityTC(BaseSecurityTC):
-
-    def setUp(self):
-        super(SecurityTC, self).setUp()
-        # implicitly test manager can add some entities
-        with self.admin_access.repo_cnx() as cnx:
-            cnx.execute("INSERT Affaire X: X sujet 'cool'")
-            cnx.execute("INSERT Societe X: X nom 'logilab'")
-            cnx.execute("INSERT Personne X: X nom 'bidule'")
-            cnx.execute('INSERT CWGroup X: X name "staff"')
-            cnx.commit()
-
-    def test_insert_security(self):
-        with self.new_access(u'anon').repo_cnx() as cnx:
-            cnx.execute("INSERT Personne X: X nom 'bidule'")
-            self.assertRaises(Unauthorized, cnx.commit)
-            self.assertEqual(cnx.execute('Personne X').rowcount, 1)
-
-    def test_insert_security_2(self):
-        with self.new_access(u'anon').repo_cnx() as cnx:
-            cnx.execute("INSERT Affaire X")
-            self.assertRaises(Unauthorized, cnx.commit)
-            # anon has no read permission on Affaire entities, so
-            # rowcount == 0
-            self.assertEqual(cnx.execute('Affaire X').rowcount, 0)
-
-    def test_insert_rql_permission(self):
-        # test user can only add une affaire related to a societe he owns
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            cnx.execute("INSERT Affaire X: X sujet 'cool'")
-            self.assertRaises(Unauthorized, cnx.commit)
-        # test nothing has actually been inserted
-        with self.admin_access.repo_cnx() as cnx:
-            self.assertEqual(cnx.execute('Affaire X').rowcount, 1)
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            cnx.execute("INSERT Affaire X: X sujet 'cool'")
-            cnx.execute("INSERT Societe X: X nom 'chouette'")
-            cnx.execute("SET A concerne S WHERE A sujet 'cool', S nom 'chouette'")
-            cnx.commit()
-
-    def test_update_security_1(self):
-        with self.new_access(u'anon').repo_cnx() as cnx:
-            # local security check
-            cnx.execute( "SET X nom 'bidulechouette' WHERE X is Personne")
-            self.assertRaises(Unauthorized, cnx.commit)
-        with self.admin_access.repo_cnx() as cnx:
-            self.assertEqual(cnx.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
-
-    def test_update_security_2(self):
-        with self.temporary_permissions(Personne={'read': ('users', 'managers'),
-                                                  'add': ('guests', 'users', 'managers')}):
-            with self.new_access(u'anon').repo_cnx() as cnx:
-                self.assertRaises(Unauthorized, cnx.execute,
-                                  "SET X nom 'bidulechouette' WHERE X is Personne")
-        # test nothing has actually been inserted
-        with self.admin_access.repo_cnx() as cnx:
-            self.assertEqual(cnx.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
-
-    def test_update_security_3(self):
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            cnx.execute("INSERT Personne X: X nom 'biduuule'")
-            cnx.execute("INSERT Societe X: X nom 'looogilab'")
-            cnx.execute("SET X travaille S WHERE X nom 'biduuule', S nom 'looogilab'")
-
-    def test_insert_immutable_attribute_update(self):
-        with self.admin_access.repo_cnx() as cnx:
-            cnx.create_entity('Old', name=u'Babar')
-            cnx.commit()
-            # this should be equivalent
-            o = cnx.create_entity('Old')
-            o.cw_set(name=u'Celeste')
-            cnx.commit()
-
-    def test_update_rql_permission(self):
-        with self.admin_access.repo_cnx() as cnx:
-            cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
-            cnx.commit()
-        # test user can only update une affaire related to a societe he owns
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            cnx.execute("SET X sujet 'pascool' WHERE X is Affaire")
-            # this won't actually do anything since the selection query won't return anything
-            cnx.commit()
-            # to actually get Unauthorized exception, try to update an entity we can read
-            cnx.execute("SET X nom 'toto' WHERE X is Societe")
-            self.assertRaises(Unauthorized, cnx.commit)
-            cnx.execute("INSERT Affaire X: X sujet 'pascool'")
-            cnx.execute("INSERT Societe X: X nom 'chouette'")
-            cnx.execute("SET A concerne S WHERE A sujet 'pascool', S nom 'chouette'")
-            cnx.execute("SET X sujet 'habahsicestcool' WHERE X sujet 'pascool'")
-            cnx.commit()
-
-    def test_delete_security(self):
-        # FIXME: sample below fails because we don't detect "owner" can't delete
-        # user anyway, and since no user with login == 'bidule' exists, no
-        # exception is raised
-        #user._groups = {'guests':1}
-        #self.assertRaises(Unauthorized,
-        #                  self.o.execute, user, "DELETE CWUser X WHERE X login 'bidule'")
-        # check local security
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            self.assertRaises(Unauthorized, cnx.execute, "DELETE CWGroup Y WHERE Y name 'staff'")
-
-    def test_delete_rql_permission(self):
-        with self.admin_access.repo_cnx() as cnx:
-            cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
-            cnx.commit()
-        # test user can only dele une affaire related to a societe he owns
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            # this won't actually do anything since the selection query won't return anything
-            cnx.execute("DELETE Affaire X")
-            cnx.commit()
-            # to actually get Unauthorized exception, try to delete an entity we can read
-            self.assertRaises(Unauthorized, cnx.execute, "DELETE Societe S")
-            self.assertRaises(QueryError, cnx.commit) # can't commit anymore
-            cnx.rollback()
-            cnx.execute("INSERT Affaire X: X sujet 'pascool'")
-            cnx.execute("INSERT Societe X: X nom 'chouette'")
-            cnx.execute("SET A concerne S WHERE A sujet 'pascool', S nom 'chouette'")
-            cnx.commit()
-##         # this one should fail since it will try to delete two affaires, one authorized
-##         # and the other not
-##         self.assertRaises(Unauthorized, cnx.execute, "DELETE Affaire X")
-            cnx.execute("DELETE Affaire X WHERE X sujet 'pascool'")
-            cnx.commit()
-
-    def test_insert_relation_rql_permission(self):
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
-            # should raise Unauthorized since user don't own S though this won't
-            # actually do anything since the selection query won't return
-            # anything
-            cnx.commit()
-            # to actually get Unauthorized exception, try to insert a relation
-            # were we can read both entities
-            rset = cnx.execute('Personne P')
-            self.assertEqual(len(rset), 1)
-            ent = rset.get_entity(0, 0)
-            self.assertFalse(cnx.execute('Any P,S WHERE P travaille S,P is Personne, S is Societe'))
-            self.assertRaises(Unauthorized, ent.cw_check_perm, 'update')
-            self.assertRaises(Unauthorized,
-                              cnx.execute, "SET P travaille S WHERE P is Personne, S is Societe")
-            self.assertRaises(QueryError, cnx.commit) # can't commit anymore
-            cnx.rollback()
-            # test nothing has actually been inserted:
-            self.assertFalse(cnx.execute('Any P,S WHERE P travaille S,P is Personne, S is Societe'))
-            cnx.execute("INSERT Societe X: X nom 'chouette'")
-            cnx.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
-            cnx.commit()
-
-    def test_delete_relation_rql_permission(self):
-        with self.admin_access.repo_cnx() as cnx:
-            cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
-            cnx.commit()
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            # this won't actually do anything since the selection query won't return anything
-            cnx.execute("DELETE A concerne S")
-            cnx.commit()
-        with self.admin_access.repo_cnx() as cnx:
-            # to actually get Unauthorized exception, try to delete a relation we can read
-            eid = cnx.execute("INSERT Affaire X: X sujet 'pascool'")[0][0]
-            cnx.execute('SET X owned_by U WHERE X eid %(x)s, U login "iaminusersgrouponly"',
-                         {'x': eid})
-            cnx.execute("SET A concerne S WHERE A sujet 'pascool', S is Societe")
-            cnx.commit()
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            self.assertRaises(Unauthorized, cnx.execute, "DELETE A concerne S")
-            self.assertRaises(QueryError, cnx.commit) # can't commit anymore
-            cnx.rollback()
-            cnx.execute("INSERT Societe X: X nom 'chouette'")
-            cnx.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
-            cnx.commit()
-            cnx.execute("DELETE A concerne S WHERE S nom 'chouette'")
-            cnx.commit()
-
-
-    def test_user_can_change_its_upassword(self):
-        with self.admin_access.repo_cnx() as cnx:
-            ueid = self.create_user(cnx, u'user').eid
-        with self.new_access(u'user').repo_cnx() as cnx:
-            cnx.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
-                       {'x': ueid, 'passwd': b'newpwd'})
-            cnx.commit()
-        with self.repo.internal_cnx() as cnx:
-            self.repo.authenticate_user(cnx, 'user', password='newpwd')
-
-    def test_user_cant_change_other_upassword(self):
-        with self.admin_access.repo_cnx() as cnx:
-            ueid = self.create_user(cnx, u'otheruser').eid
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            cnx.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
-                       {'x': ueid, 'passwd': b'newpwd'})
-            self.assertRaises(Unauthorized, cnx.commit)
-
-    # read security test
-
-    def test_read_base(self):
-        with self.temporary_permissions(Personne={'read': ('users', 'managers')}):
-            with self.new_access(u'anon').repo_cnx() as cnx:
-                self.assertRaises(Unauthorized,
-                                  cnx.execute, 'Personne U where U nom "managers"')
-
-    def test_read_erqlexpr_base(self):
-        with self.admin_access.repo_cnx() as cnx:
-            eid = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
-            cnx.commit()
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            rset = cnx.execute('Affaire X')
-            self.assertEqual(rset.rows, [])
-            self.assertRaises(Unauthorized, cnx.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
-            # cache test
-            self.assertRaises(Unauthorized, cnx.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
-            aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
-            soc1 = cnx.execute("INSERT Societe X: X nom 'chouette'")[0][0]
-            cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
-            cnx.commit()
-            rset = cnx.execute('Any X WHERE X eid %(x)s', {'x': aff2})
-            self.assertEqual(rset.rows, [[aff2]])
-            # more cache test w/ NOT eid
-            rset = cnx.execute('Affaire X WHERE NOT X eid %(x)s', {'x': eid})
-            self.assertEqual(rset.rows, [[aff2]])
-            rset = cnx.execute('Affaire X WHERE NOT X eid %(x)s', {'x': aff2})
-            self.assertEqual(rset.rows, [])
-            # test can't update an attribute of an entity that can't be readen
-            self.assertRaises(Unauthorized, cnx.execute,
-                              'SET X sujet "hacked" WHERE X eid %(x)s', {'x': eid})
-
-
-    def test_entity_created_in_transaction(self):
-        affschema = self.schema['Affaire']
-        with self.temporary_permissions(Affaire={'read': affschema.permissions['add']}):
-            with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-                aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
-                # entity created in transaction are readable *by eid*
-                self.assertTrue(cnx.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
-                # XXX would be nice if it worked
-                rset = cnx.execute("Affaire X WHERE X sujet 'cool'")
-                self.assertEqual(len(rset), 0)
-                self.assertRaises(Unauthorized, cnx.commit)
-
-    def test_read_erqlexpr_has_text1(self):
-        with self.admin_access.repo_cnx() as cnx:
-            aff1 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
-            card1 = cnx.execute("INSERT Card X: X title 'cool'")[0][0]
-            cnx.execute('SET X owned_by U WHERE X eid %(x)s, U login "iaminusersgrouponly"',
-                        {'x': card1})
-            cnx.commit()
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
-            soc1 = cnx.execute("INSERT Societe X: X nom 'chouette'")[0][0]
-            cnx.execute("SET A concerne S WHERE A eid %(a)s, S eid %(s)s", {'a': aff2, 's': soc1})
-            cnx.commit()
-            self.assertRaises(Unauthorized, cnx.execute, 'Any X WHERE X eid %(x)s', {'x':aff1})
-            self.assertTrue(cnx.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
-            self.assertTrue(cnx.execute('Any X WHERE X eid %(x)s', {'x':card1}))
-            rset = cnx.execute("Any X WHERE X has_text 'cool'")
-            self.assertEqual(sorted(eid for eid, in rset.rows),
-                              [card1, aff2])
-
-    def test_read_erqlexpr_has_text2(self):
-        with self.admin_access.repo_cnx() as cnx:
-            cnx.execute("INSERT Personne X: X nom 'bidule'")
-            cnx.execute("INSERT Societe X: X nom 'bidule'")
-            cnx.commit()
-        with self.temporary_permissions(Personne={'read': ('managers',)}):
-            with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-                rset = cnx.execute('Any N WHERE N has_text "bidule"')
-                self.assertEqual(len(rset.rows), 1, rset.rows)
-                rset = cnx.execute('Any N WITH N BEING (Any N WHERE N has_text "bidule")')
-                self.assertEqual(len(rset.rows), 1, rset.rows)
-
-    def test_read_erqlexpr_optional_rel(self):
-        with self.admin_access.repo_cnx() as cnx:
-            cnx.execute("INSERT Personne X: X nom 'bidule'")
-            cnx.execute("INSERT Societe X: X nom 'bidule'")
-            cnx.commit()
-        with self.temporary_permissions(Personne={'read': ('managers',)}):
-            with self.new_access(u'anon').repo_cnx() as cnx:
-                rset = cnx.execute('Any N,U WHERE N has_text "bidule", N owned_by U?')
-                self.assertEqual(len(rset.rows), 1, rset.rows)
-
-    def test_read_erqlexpr_aggregat(self):
-        with self.admin_access.repo_cnx() as cnx:
-            cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
-            cnx.commit()
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            rset = cnx.execute('Any COUNT(X) WHERE X is Affaire')
-            self.assertEqual(rset.rows, [[0]])
-            aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
-            soc1 = cnx.execute("INSERT Societe X: X nom 'chouette'")[0][0]
-            cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
-            cnx.commit()
-            rset = cnx.execute('Any COUNT(X) WHERE X is Affaire')
-            self.assertEqual(rset.rows, [[1]])
-            rset = cnx.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN')
-            values = dict(rset)
-            self.assertEqual(values['Affaire'], 1)
-            self.assertEqual(values['Societe'], 2)
-            rset = cnx.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN '
-                              'WITH X BEING ((Affaire X) UNION (Societe X))')
-            self.assertEqual(len(rset), 2)
-            values = dict(rset)
-            self.assertEqual(values['Affaire'], 1)
-            self.assertEqual(values['Societe'], 2)
-
-
-    def test_attribute_security(self):
-        with self.admin_access.repo_cnx() as cnx:
-            # only managers should be able to edit the 'test' attribute of Personne entities
-            eid = cnx.execute("INSERT Personne X: X nom 'bidule', "
-                               "X web 'http://www.debian.org', X test TRUE")[0][0]
-            cnx.execute('SET X test FALSE WHERE X eid %(x)s', {'x': eid})
-            cnx.commit()
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            cnx.execute("INSERT Personne X: X nom 'bidule', "
-                       "X web 'http://www.debian.org', X test TRUE")
-            self.assertRaises(Unauthorized, cnx.commit)
-            cnx.execute("INSERT Personne X: X nom 'bidule', "
-                       "X web 'http://www.debian.org', X test FALSE")
-            self.assertRaises(Unauthorized, cnx.commit)
-            eid = cnx.execute("INSERT Personne X: X nom 'bidule', "
-                             "X web 'http://www.debian.org'")[0][0]
-            cnx.commit()
-            cnx.execute('SET X test FALSE WHERE X eid %(x)s', {'x': eid})
-            self.assertRaises(Unauthorized, cnx.commit)
-            cnx.execute('SET X test TRUE WHERE X eid %(x)s', {'x': eid})
-            self.assertRaises(Unauthorized, cnx.commit)
-            cnx.execute('SET X web "http://www.logilab.org" WHERE X eid %(x)s', {'x': eid})
-            cnx.commit()
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            cnx.execute('INSERT Frozable F: F name "Foo"')
-            cnx.commit()
-            cnx.execute('SET F name "Bar" WHERE F is Frozable')
-            cnx.commit()
-            cnx.execute('SET F name "BaBar" WHERE F is Frozable')
-            cnx.execute('SET F frozen True WHERE F is Frozable')
-            with self.assertRaises(Unauthorized):
-                cnx.commit()
-            cnx.rollback()
-            cnx.execute('SET F frozen True WHERE F is Frozable')
-            cnx.commit()
-            cnx.execute('SET F name "Bar" WHERE F is Frozable')
-            with self.assertRaises(Unauthorized):
-                cnx.commit()
-
-    def test_attribute_security_rqlexpr(self):
-        with self.admin_access.repo_cnx() as cnx:
-            # Note.para attribute editable by managers or if the note is in "todo" state
-            note = cnx.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
-            cnx.commit()
-            note.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
-            cnx.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid})
-            cnx.commit()
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            cnx.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note.eid})
-            self.assertRaises(Unauthorized, cnx.commit)
-            note2 = cnx.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
-            cnx.commit()
-            note2.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
-            cnx.commit()
-            self.assertEqual(len(cnx.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s',
-                                            {'x': note2.eid})),
-                              0)
-            cnx.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
-            self.assertRaises(Unauthorized, cnx.commit)
-            note2.cw_adapt_to('IWorkflowable').fire_transition('redoit')
-            cnx.commit()
-            cnx.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
-            cnx.commit()
-            cnx.execute("INSERT Note X: X something 'A'")
-            self.assertRaises(Unauthorized, cnx.commit)
-            cnx.execute("INSERT Note X: X para 'zogzog', X something 'A'")
-            cnx.commit()
-            note = cnx.execute("INSERT Note X").get_entity(0,0)
-            cnx.commit()
-            note.cw_set(something=u'B')
-            cnx.commit()
-            note.cw_set(something=None, para=u'zogzog')
-            cnx.commit()
-
-    def test_attribute_read_security(self):
-        # anon not allowed to see users'login, but they can see users
-        login_rdef = self.repo.schema['CWUser'].rdef('login')
-        with self.temporary_permissions((login_rdef, {'read': ('users', 'managers')}),
-                                        CWUser={'read': ('guests', 'users', 'managers')}):
-            with self.new_access(u'anon').repo_cnx() as cnx:
-                rset = cnx.execute('CWUser X')
-                self.assertTrue(rset)
-                x = rset.get_entity(0, 0)
-                x.complete()
-                self.assertEqual(x.login, None)
-                self.assertTrue(x.creation_date)
-                x = rset.get_entity(1, 0)
-                x.complete()
-                self.assertEqual(x.login, None)
-                self.assertTrue(x.creation_date)
-
-    def test_yams_inheritance_and_security_bug(self):
-        with self.temporary_permissions(Division={'read': ('managers',
-                                                           ERQLExpression('X owned_by U'))}):
-            with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-                rqlst = self.repo.vreg.rqlhelper.parse('Any X WHERE X is_instance_of Societe')
-                self.repo.vreg.solutions(cnx, rqlst, {})
-                querier = cnx.repo.querier
-                querier._annotate(rqlst)
-                plan = querier.plan_factory(rqlst, {}, cnx)
-                plan.preprocess(rqlst)
-                self.assertEqual(
-                    rqlst.as_string(),
-                    '(Any X WHERE X is IN(Societe, SubDivision)) UNION '
-                    '(Any X WHERE X is Division, EXISTS(X owned_by %(B)s))')
-
-
-class BaseSchemaSecurityTC(BaseSecurityTC):
-    """tests related to the base schema permission configuration"""
-
-    def test_user_can_delete_object_he_created(self):
-        # even if some other user have changed object'state
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            # due to security test, affaire has to concerne a societe the user owns
-            cnx.execute('INSERT Societe X: X nom "ARCTIA"')
-            cnx.execute('INSERT Affaire X: X ref "ARCT01", X concerne S WHERE S nom "ARCTIA"')
-            cnx.commit()
-        with self.admin_access.repo_cnx() as cnx:
-            affaire = cnx.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
-            affaire.cw_adapt_to('IWorkflowable').fire_transition('abort')
-            cnx.commit()
-            self.assertEqual(len(cnx.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01"')),
-                             1)
-            self.assertEqual(len(cnx.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",'
-                                              'X owned_by U, U login "admin"')),
-                             1) # TrInfo at the above state change
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            cnx.execute('DELETE Affaire X WHERE X ref "ARCT01"')
-            cnx.commit()
-            self.assertFalse(cnx.execute('Affaire X'))
-
-    def test_users_and_groups_non_readable_by_guests(self):
-        with self.repo.internal_cnx() as cnx:
-            admineid = cnx.execute('CWUser U WHERE U login "admin"').rows[0][0]
-        with self.new_access(u'anon').repo_cnx() as cnx:
-            anon = cnx.user
-            # anonymous user can only read itself
-            rset = cnx.execute('Any L WHERE X owned_by U, U login L')
-            self.assertEqual([['anon']], rset.rows)
-            rset = cnx.execute('CWUser X')
-            self.assertEqual([[anon.eid]], rset.rows)
-            # anonymous user can read groups (necessary to check allowed transitions for instance)
-            self.assertTrue(cnx.execute('CWGroup X'))
-            # should only be able to read the anonymous user, not another one
-            self.assertRaises(Unauthorized,
-                              cnx.execute, 'CWUser X WHERE X eid %(x)s', {'x': admineid})
-            rset = cnx.execute('CWUser X WHERE X eid %(x)s', {'x': anon.eid})
-            self.assertEqual([[anon.eid]], rset.rows)
-            # but can't modify it
-            cnx.execute('SET X login "toto" WHERE X eid %(x)s', {'x': anon.eid})
-            self.assertRaises(Unauthorized, cnx.commit)
-
-    def test_in_group_relation(self):
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            rql = u"DELETE U in_group G WHERE U login 'admin'"
-            self.assertRaises(Unauthorized, cnx.execute, rql)
-            rql = u"SET U in_group G WHERE U login 'admin', G name 'users'"
-            self.assertRaises(Unauthorized, cnx.execute, rql)
-
-    def test_owned_by(self):
-        with self.admin_access.repo_cnx() as cnx:
-            cnx.execute("INSERT Personne X: X nom 'bidule'")
-            cnx.commit()
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            rql = u"SET X owned_by U WHERE U login 'iaminusersgrouponly', X is Personne"
-            self.assertRaises(Unauthorized, cnx.execute, rql)
-
-    def test_bookmarked_by_guests_security(self):
-        with self.admin_access.repo_cnx() as cnx:
-            beid1 = cnx.execute('INSERT Bookmark B: B path "?vid=manage", B title "manage"')[0][0]
-            beid2 = cnx.execute('INSERT Bookmark B: B path "?vid=index", B title "index", '
-                                'B bookmarked_by U WHERE U login "anon"')[0][0]
-            cnx.commit()
-        with self.new_access(u'anon').repo_cnx() as cnx:
-            anoneid = cnx.user.eid
-            self.assertEqual(cnx.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
-                                         'B bookmarked_by U, U eid %s' % anoneid).rows,
-                              [['index', '?vid=index']])
-            self.assertEqual(cnx.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
-                                         'B bookmarked_by U, U eid %(x)s', {'x': anoneid}).rows,
-                              [['index', '?vid=index']])
-            # can read others bookmarks as well
-            self.assertEqual(cnx.execute('Any B where B is Bookmark, NOT B bookmarked_by U').rows,
-                              [[beid1]])
-            self.assertRaises(Unauthorized, cnx.execute,'DELETE B bookmarked_by U')
-            self.assertRaises(Unauthorized,
-                              cnx.execute, 'SET B bookmarked_by U WHERE U eid %(x)s, B eid %(b)s',
-                              {'x': anoneid, 'b': beid1})
-
-    def test_ambigous_ordered(self):
-        with self.new_access(u'anon').repo_cnx() as cnx:
-            names = [t for t, in cnx.execute('Any N ORDERBY lower(N) WHERE X name N')]
-            self.assertEqual(names, sorted(names, key=lambda x: x.lower()))
-
-    def test_in_state_without_update_perm(self):
-        """check a user change in_state without having update permission on the
-        subject
-        """
-        with self.admin_access.repo_cnx() as cnx:
-            eid = cnx.execute('INSERT Affaire X: X ref "ARCT01"')[0][0]
-            cnx.commit()
-        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
-            # needed to remove rql expr granting update perm to the user
-            affschema = self.schema['Affaire']
-            with self.temporary_permissions(Affaire={'update': affschema.get_groups('update'),
-                                                     'read': ('users',)}):
-                self.assertRaises(Unauthorized,
-                                  affschema.check_perm, cnx, 'update', eid=eid)
-                aff = cnx.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
-                aff.cw_adapt_to('IWorkflowable').fire_transition('abort')
-                cnx.commit()
-                # though changing a user state (even logged user) is reserved to managers
-                user = cnx.user
-                # XXX wether it should raise Unauthorized or ValidationError is not clear
-                # the best would probably ValidationError if the transition doesn't exist
-                # from the current state but Unauthorized if it exists but user can't pass it
-                self.assertRaises(ValidationError,
-                                  user.cw_adapt_to('IWorkflowable').fire_transition, 'deactivate')
-
-    def test_trinfo_security(self):
-        with self.admin_access.repo_cnx() as cnx:
-            aff = cnx.execute('INSERT Affaire X: X ref "ARCT01"').get_entity(0, 0)
-            iworkflowable = aff.cw_adapt_to('IWorkflowable')
-            cnx.commit()
-            iworkflowable.fire_transition('abort')
-            cnx.commit()
-            # can change tr info comment
-            cnx.execute('SET TI comment %(c)s WHERE TI wf_info_for X, X ref "ARCT01"',
-                         {'c': u'bouh!'})
-            cnx.commit()
-            aff.cw_clear_relation_cache('wf_info_for', 'object')
-            trinfo = iworkflowable.latest_trinfo()
-            self.assertEqual(trinfo.comment, 'bouh!')
-            # but not from_state/to_state
-            aff.cw_clear_relation_cache('wf_info_for', role='object')
-            self.assertRaises(Unauthorized, cnx.execute,
-                              'SET TI from_state S WHERE TI eid %(ti)s, S name "ben non"',
-                              {'ti': trinfo.eid})
-            self.assertRaises(Unauthorized, cnx.execute,
-                              'SET TI to_state S WHERE TI eid %(ti)s, S name "pitetre"',
-                              {'ti': trinfo.eid})
-
-    def test_emailaddress_security(self):
-        # check for prexisting email adresse
-        with self.admin_access.repo_cnx() as cnx:
-            if cnx.execute('Any X WHERE X is EmailAddress'):
-                rset = cnx.execute('Any X, U WHERE X is EmailAddress, U use_email X')
-                msg = ['Preexisting email readable by anon found!']
-                tmpl = '  - "%s" used by user "%s"'
-                for i in range(len(rset)):
-                    email, user = rset.get_entity(i, 0), rset.get_entity(i, 1)
-                    msg.append(tmpl % (email.dc_title(), user.dc_title()))
-                raise RuntimeError('\n'.join(msg))
-            # actual test
-            cnx.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
-            cnx.execute('INSERT EmailAddress X: X address "anon", '
-                         'U use_email X WHERE U login "anon"').get_entity(0, 0)
-            cnx.commit()
-            self.assertEqual(len(cnx.execute('Any X WHERE X is EmailAddress')), 2)
-        with self.new_access(u'anon').repo_cnx() as cnx:
-            self.assertEqual(len(cnx.execute('Any X WHERE X is EmailAddress')), 1)
-
-if __name__ == '__main__':
-    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/unittest_server_security.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,689 @@
+# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""functional tests for server'security"""
+
+from logilab.common.testlib import unittest_main
+
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb import Unauthorized, ValidationError, QueryError, Binary
+from cubicweb.schema import ERQLExpression
+from cubicweb.server.querier import get_local_checks, check_relations_read_access
+from cubicweb.server.utils import _CRYPTO_CTX
+
+
+class BaseSecurityTC(CubicWebTC):
+
+    def setup_database(self):
+        super(BaseSecurityTC, self).setup_database()
+        with self.admin_access.client_cnx() as cnx:
+            self.create_user(cnx, u'iaminusersgrouponly')
+            hash = _CRYPTO_CTX.encrypt('oldpassword', scheme='des_crypt')
+            self.create_user(cnx, u'oldpassword', password=Binary(hash.encode('ascii')))
+
+
+class LowLevelSecurityFunctionTC(BaseSecurityTC):
+
+    def test_check_relation_read_access(self):
+        rql = u'Personne U WHERE U nom "managers"'
+        rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0]
+        nom = self.repo.schema['Personne'].rdef('nom')
+        with self.temporary_permissions((nom, {'read': ('users', 'managers')})):
+            with self.admin_access.repo_cnx() as cnx:
+                self.repo.vreg.solutions(cnx, rqlst, None)
+                check_relations_read_access(cnx, rqlst, {})
+            with self.new_access(u'anon').repo_cnx() as cnx:
+                self.assertRaises(Unauthorized,
+                                  check_relations_read_access,
+                                  cnx, rqlst, {})
+                self.assertRaises(Unauthorized, cnx.execute, rql)
+
+    def test_get_local_checks(self):
+        rql = u'Personne U WHERE U nom "managers"'
+        rqlst = self.repo.vreg.rqlhelper.parse(rql).children[0]
+        with self.temporary_permissions(Personne={'read': ('users', 'managers')}):
+            with self.admin_access.repo_cnx() as cnx:
+                self.repo.vreg.solutions(cnx, rqlst, None)
+                solution = rqlst.solutions[0]
+                localchecks = get_local_checks(cnx, rqlst, solution)
+                self.assertEqual({}, localchecks)
+            with self.new_access(u'anon').repo_cnx() as cnx:
+                self.assertRaises(Unauthorized,
+                                  get_local_checks,
+                                  cnx, rqlst, solution)
+                self.assertRaises(Unauthorized, cnx.execute, rql)
+
+    def test_upassword_not_selectable(self):
+        with self.admin_access.repo_cnx() as cnx:
+            self.assertRaises(Unauthorized,
+                              cnx.execute, 'Any X,P WHERE X is CWUser, X upassword P')
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            self.assertRaises(Unauthorized,
+                              cnx.execute, 'Any X,P WHERE X is CWUser, X upassword P')
+
+    def test_update_password(self):
+        """Ensure that if a user's password is stored with a deprecated hash,
+        it will be updated on next login
+        """
+        with self.repo.internal_cnx() as cnx:
+            oldhash = cnx.system_sql("SELECT cw_upassword FROM cw_CWUser "
+                                     "WHERE cw_login = 'oldpassword'").fetchone()[0]
+            oldhash = self.repo.system_source.binary_to_str(oldhash)
+            self.repo.authenticate_user(cnx, 'oldpassword', password='oldpassword')
+            newhash = cnx.system_sql("SELECT cw_upassword FROM cw_CWUser "
+                                     "WHERE cw_login = 'oldpassword'").fetchone()[0]
+            newhash = self.repo.system_source.binary_to_str(newhash)
+            self.assertNotEqual(oldhash, newhash)
+            self.assertTrue(newhash.startswith(b'$6$'))
+            self.repo.authenticate_user(cnx, 'oldpassword', password='oldpassword')
+            newnewhash = cnx.system_sql("SELECT cw_upassword FROM cw_CWUser WHERE "
+                                        "cw_login = 'oldpassword'").fetchone()[0]
+            newnewhash = self.repo.system_source.binary_to_str(newnewhash)
+            self.assertEqual(newhash, newnewhash)
+
+
+class SecurityRewritingTC(BaseSecurityTC):
+    def hijack_source_execute(self):
+        def syntax_tree_search(*args, **kwargs):
+            self.query = (args, kwargs)
+            return []
+        self.repo.system_source.syntax_tree_search = syntax_tree_search
+
+    def tearDown(self):
+        self.repo.system_source.__dict__.pop('syntax_tree_search', None)
+        super(SecurityRewritingTC, self).tearDown()
+
+    def test_not_relation_read_security(self):
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            cnx.user.groups  # fill the cache before screwing syntax_tree_search
+            self.hijack_source_execute()
+            cnx.execute('Any U WHERE NOT A todo_by U, A is Affaire')
+            self.assertEqual(self.query[0][1].as_string(),
+                             'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
+            cnx.execute('Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
+            self.assertEqual(self.query[0][1].as_string(),
+                             'Any U WHERE NOT EXISTS(A todo_by U), A is Affaire')
+
+
+class SecurityTC(BaseSecurityTC):
+
+    def setUp(self):
+        super(SecurityTC, self).setUp()
+        # implicitly test manager can add some entities
+        with self.admin_access.repo_cnx() as cnx:
+            cnx.execute("INSERT Affaire X: X sujet 'cool'")
+            cnx.execute("INSERT Societe X: X nom 'logilab'")
+            cnx.execute("INSERT Personne X: X nom 'bidule'")
+            cnx.execute('INSERT CWGroup X: X name "staff"')
+            cnx.commit()
+
+    def test_insert_security(self):
+        with self.new_access(u'anon').repo_cnx() as cnx:
+            cnx.execute("INSERT Personne X: X nom 'bidule'")
+            self.assertRaises(Unauthorized, cnx.commit)
+            self.assertEqual(cnx.execute('Personne X').rowcount, 1)
+
+    def test_insert_security_2(self):
+        with self.new_access(u'anon').repo_cnx() as cnx:
+            cnx.execute("INSERT Affaire X")
+            self.assertRaises(Unauthorized, cnx.commit)
+            # anon has no read permission on Affaire entities, so
+            # rowcount == 0
+            self.assertEqual(cnx.execute('Affaire X').rowcount, 0)
+
+    def test_insert_rql_permission(self):
+        # test user can only add une affaire related to a societe he owns
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            cnx.execute("INSERT Affaire X: X sujet 'cool'")
+            self.assertRaises(Unauthorized, cnx.commit)
+        # test nothing has actually been inserted
+        with self.admin_access.repo_cnx() as cnx:
+            self.assertEqual(cnx.execute('Affaire X').rowcount, 1)
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            cnx.execute("INSERT Affaire X: X sujet 'cool'")
+            cnx.execute("INSERT Societe X: X nom 'chouette'")
+            cnx.execute("SET A concerne S WHERE A sujet 'cool', S nom 'chouette'")
+            cnx.commit()
+
+    def test_update_security_1(self):
+        with self.new_access(u'anon').repo_cnx() as cnx:
+            # local security check
+            cnx.execute( "SET X nom 'bidulechouette' WHERE X is Personne")
+            self.assertRaises(Unauthorized, cnx.commit)
+        with self.admin_access.repo_cnx() as cnx:
+            self.assertEqual(cnx.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
+
+    def test_update_security_2(self):
+        with self.temporary_permissions(Personne={'read': ('users', 'managers'),
+                                                  'add': ('guests', 'users', 'managers')}):
+            with self.new_access(u'anon').repo_cnx() as cnx:
+                self.assertRaises(Unauthorized, cnx.execute,
+                                  "SET X nom 'bidulechouette' WHERE X is Personne")
+        # test nothing has actually been inserted
+        with self.admin_access.repo_cnx() as cnx:
+            self.assertEqual(cnx.execute('Personne X WHERE X nom "bidulechouette"').rowcount, 0)
+
+    def test_update_security_3(self):
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            cnx.execute("INSERT Personne X: X nom 'biduuule'")
+            cnx.execute("INSERT Societe X: X nom 'looogilab'")
+            cnx.execute("SET X travaille S WHERE X nom 'biduuule', S nom 'looogilab'")
+
+    def test_insert_immutable_attribute_update(self):
+        with self.admin_access.repo_cnx() as cnx:
+            cnx.create_entity('Old', name=u'Babar')
+            cnx.commit()
+            # this should be equivalent
+            o = cnx.create_entity('Old')
+            o.cw_set(name=u'Celeste')
+            cnx.commit()
+
+    def test_update_rql_permission(self):
+        with self.admin_access.repo_cnx() as cnx:
+            cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
+            cnx.commit()
+        # test user can only update une affaire related to a societe he owns
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            cnx.execute("SET X sujet 'pascool' WHERE X is Affaire")
+            # this won't actually do anything since the selection query won't return anything
+            cnx.commit()
+            # to actually get Unauthorized exception, try to update an entity we can read
+            cnx.execute("SET X nom 'toto' WHERE X is Societe")
+            self.assertRaises(Unauthorized, cnx.commit)
+            cnx.execute("INSERT Affaire X: X sujet 'pascool'")
+            cnx.execute("INSERT Societe X: X nom 'chouette'")
+            cnx.execute("SET A concerne S WHERE A sujet 'pascool', S nom 'chouette'")
+            cnx.execute("SET X sujet 'habahsicestcool' WHERE X sujet 'pascool'")
+            cnx.commit()
+
+    def test_delete_security(self):
+        # FIXME: sample below fails because we don't detect "owner" can't delete
+        # user anyway, and since no user with login == 'bidule' exists, no
+        # exception is raised
+        #user._groups = {'guests':1}
+        #self.assertRaises(Unauthorized,
+        #                  self.o.execute, user, "DELETE CWUser X WHERE X login 'bidule'")
+        # check local security
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            self.assertRaises(Unauthorized, cnx.execute, "DELETE CWGroup Y WHERE Y name 'staff'")
+
+    def test_delete_rql_permission(self):
+        with self.admin_access.repo_cnx() as cnx:
+            cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
+            cnx.commit()
+        # test user can only dele une affaire related to a societe he owns
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            # this won't actually do anything since the selection query won't return anything
+            cnx.execute("DELETE Affaire X")
+            cnx.commit()
+            # to actually get Unauthorized exception, try to delete an entity we can read
+            self.assertRaises(Unauthorized, cnx.execute, "DELETE Societe S")
+            self.assertRaises(QueryError, cnx.commit) # can't commit anymore
+            cnx.rollback()
+            cnx.execute("INSERT Affaire X: X sujet 'pascool'")
+            cnx.execute("INSERT Societe X: X nom 'chouette'")
+            cnx.execute("SET A concerne S WHERE A sujet 'pascool', S nom 'chouette'")
+            cnx.commit()
+##         # this one should fail since it will try to delete two affaires, one authorized
+##         # and the other not
+##         self.assertRaises(Unauthorized, cnx.execute, "DELETE Affaire X")
+            cnx.execute("DELETE Affaire X WHERE X sujet 'pascool'")
+            cnx.commit()
+
+    def test_insert_relation_rql_permission(self):
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
+            # should raise Unauthorized since user don't own S though this won't
+            # actually do anything since the selection query won't return
+            # anything
+            cnx.commit()
+            # to actually get Unauthorized exception, try to insert a relation
+            # were we can read both entities
+            rset = cnx.execute('Personne P')
+            self.assertEqual(len(rset), 1)
+            ent = rset.get_entity(0, 0)
+            self.assertFalse(cnx.execute('Any P,S WHERE P travaille S,P is Personne, S is Societe'))
+            self.assertRaises(Unauthorized, ent.cw_check_perm, 'update')
+            self.assertRaises(Unauthorized,
+                              cnx.execute, "SET P travaille S WHERE P is Personne, S is Societe")
+            self.assertRaises(QueryError, cnx.commit) # can't commit anymore
+            cnx.rollback()
+            # test nothing has actually been inserted:
+            self.assertFalse(cnx.execute('Any P,S WHERE P travaille S,P is Personne, S is Societe'))
+            cnx.execute("INSERT Societe X: X nom 'chouette'")
+            cnx.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
+            cnx.commit()
+
+    def test_delete_relation_rql_permission(self):
+        with self.admin_access.repo_cnx() as cnx:
+            cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
+            cnx.commit()
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            # this won't actually do anything since the selection query won't return anything
+            cnx.execute("DELETE A concerne S")
+            cnx.commit()
+        with self.admin_access.repo_cnx() as cnx:
+            # to actually get Unauthorized exception, try to delete a relation we can read
+            eid = cnx.execute("INSERT Affaire X: X sujet 'pascool'")[0][0]
+            cnx.execute('SET X owned_by U WHERE X eid %(x)s, U login "iaminusersgrouponly"',
+                         {'x': eid})
+            cnx.execute("SET A concerne S WHERE A sujet 'pascool', S is Societe")
+            cnx.commit()
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            self.assertRaises(Unauthorized, cnx.execute, "DELETE A concerne S")
+            self.assertRaises(QueryError, cnx.commit) # can't commit anymore
+            cnx.rollback()
+            cnx.execute("INSERT Societe X: X nom 'chouette'")
+            cnx.execute("SET A concerne S WHERE A is Affaire, S nom 'chouette'")
+            cnx.commit()
+            cnx.execute("DELETE A concerne S WHERE S nom 'chouette'")
+            cnx.commit()
+
+
+    def test_user_can_change_its_upassword(self):
+        with self.admin_access.repo_cnx() as cnx:
+            ueid = self.create_user(cnx, u'user').eid
+        with self.new_access(u'user').repo_cnx() as cnx:
+            cnx.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
+                       {'x': ueid, 'passwd': b'newpwd'})
+            cnx.commit()
+        with self.repo.internal_cnx() as cnx:
+            self.repo.authenticate_user(cnx, 'user', password='newpwd')
+
+    def test_user_cant_change_other_upassword(self):
+        with self.admin_access.repo_cnx() as cnx:
+            ueid = self.create_user(cnx, u'otheruser').eid
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            cnx.execute('SET X upassword %(passwd)s WHERE X eid %(x)s',
+                       {'x': ueid, 'passwd': b'newpwd'})
+            self.assertRaises(Unauthorized, cnx.commit)
+
+    # read security test
+
+    def test_read_base(self):
+        with self.temporary_permissions(Personne={'read': ('users', 'managers')}):
+            with self.new_access(u'anon').repo_cnx() as cnx:
+                self.assertRaises(Unauthorized,
+                                  cnx.execute, 'Personne U where U nom "managers"')
+
+    def test_read_erqlexpr_base(self):
+        with self.admin_access.repo_cnx() as cnx:
+            eid = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+            cnx.commit()
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            rset = cnx.execute('Affaire X')
+            self.assertEqual(rset.rows, [])
+            self.assertRaises(Unauthorized, cnx.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
+            # cache test
+            self.assertRaises(Unauthorized, cnx.execute, 'Any X WHERE X eid %(x)s', {'x': eid})
+            aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+            soc1 = cnx.execute("INSERT Societe X: X nom 'chouette'")[0][0]
+            cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
+            cnx.commit()
+            rset = cnx.execute('Any X WHERE X eid %(x)s', {'x': aff2})
+            self.assertEqual(rset.rows, [[aff2]])
+            # more cache test w/ NOT eid
+            rset = cnx.execute('Affaire X WHERE NOT X eid %(x)s', {'x': eid})
+            self.assertEqual(rset.rows, [[aff2]])
+            rset = cnx.execute('Affaire X WHERE NOT X eid %(x)s', {'x': aff2})
+            self.assertEqual(rset.rows, [])
+            # test can't update an attribute of an entity that can't be readen
+            self.assertRaises(Unauthorized, cnx.execute,
+                              'SET X sujet "hacked" WHERE X eid %(x)s', {'x': eid})
+
+
+    def test_entity_created_in_transaction(self):
+        affschema = self.schema['Affaire']
+        with self.temporary_permissions(Affaire={'read': affschema.permissions['add']}):
+            with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+                aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+                # entity created in transaction are readable *by eid*
+                self.assertTrue(cnx.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
+                # XXX would be nice if it worked
+                rset = cnx.execute("Affaire X WHERE X sujet 'cool'")
+                self.assertEqual(len(rset), 0)
+                self.assertRaises(Unauthorized, cnx.commit)
+
+    def test_read_erqlexpr_has_text1(self):
+        with self.admin_access.repo_cnx() as cnx:
+            aff1 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+            card1 = cnx.execute("INSERT Card X: X title 'cool'")[0][0]
+            cnx.execute('SET X owned_by U WHERE X eid %(x)s, U login "iaminusersgrouponly"',
+                        {'x': card1})
+            cnx.commit()
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+            soc1 = cnx.execute("INSERT Societe X: X nom 'chouette'")[0][0]
+            cnx.execute("SET A concerne S WHERE A eid %(a)s, S eid %(s)s", {'a': aff2, 's': soc1})
+            cnx.commit()
+            self.assertRaises(Unauthorized, cnx.execute, 'Any X WHERE X eid %(x)s', {'x':aff1})
+            self.assertTrue(cnx.execute('Any X WHERE X eid %(x)s', {'x':aff2}))
+            self.assertTrue(cnx.execute('Any X WHERE X eid %(x)s', {'x':card1}))
+            rset = cnx.execute("Any X WHERE X has_text 'cool'")
+            self.assertEqual(sorted(eid for eid, in rset.rows),
+                              [card1, aff2])
+
+    def test_read_erqlexpr_has_text2(self):
+        with self.admin_access.repo_cnx() as cnx:
+            cnx.execute("INSERT Personne X: X nom 'bidule'")
+            cnx.execute("INSERT Societe X: X nom 'bidule'")
+            cnx.commit()
+        with self.temporary_permissions(Personne={'read': ('managers',)}):
+            with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+                rset = cnx.execute('Any N WHERE N has_text "bidule"')
+                self.assertEqual(len(rset.rows), 1, rset.rows)
+                rset = cnx.execute('Any N WITH N BEING (Any N WHERE N has_text "bidule")')
+                self.assertEqual(len(rset.rows), 1, rset.rows)
+
+    def test_read_erqlexpr_optional_rel(self):
+        with self.admin_access.repo_cnx() as cnx:
+            cnx.execute("INSERT Personne X: X nom 'bidule'")
+            cnx.execute("INSERT Societe X: X nom 'bidule'")
+            cnx.commit()
+        with self.temporary_permissions(Personne={'read': ('managers',)}):
+            with self.new_access(u'anon').repo_cnx() as cnx:
+                rset = cnx.execute('Any N,U WHERE N has_text "bidule", N owned_by U?')
+                self.assertEqual(len(rset.rows), 1, rset.rows)
+
+    def test_read_erqlexpr_aggregat(self):
+        with self.admin_access.repo_cnx() as cnx:
+            cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+            cnx.commit()
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            rset = cnx.execute('Any COUNT(X) WHERE X is Affaire')
+            self.assertEqual(rset.rows, [[0]])
+            aff2 = cnx.execute("INSERT Affaire X: X sujet 'cool'")[0][0]
+            soc1 = cnx.execute("INSERT Societe X: X nom 'chouette'")[0][0]
+            cnx.execute("SET A concerne S WHERE A is Affaire, S is Societe")
+            cnx.commit()
+            rset = cnx.execute('Any COUNT(X) WHERE X is Affaire')
+            self.assertEqual(rset.rows, [[1]])
+            rset = cnx.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN')
+            values = dict(rset)
+            self.assertEqual(values['Affaire'], 1)
+            self.assertEqual(values['Societe'], 2)
+            rset = cnx.execute('Any ETN, COUNT(X) GROUPBY ETN WHERE X is ET, ET name ETN '
+                              'WITH X BEING ((Affaire X) UNION (Societe X))')
+            self.assertEqual(len(rset), 2)
+            values = dict(rset)
+            self.assertEqual(values['Affaire'], 1)
+            self.assertEqual(values['Societe'], 2)
+
+
+    def test_attribute_security(self):
+        with self.admin_access.repo_cnx() as cnx:
+            # only managers should be able to edit the 'test' attribute of Personne entities
+            eid = cnx.execute("INSERT Personne X: X nom 'bidule', "
+                               "X web 'http://www.debian.org', X test TRUE")[0][0]
+            cnx.execute('SET X test FALSE WHERE X eid %(x)s', {'x': eid})
+            cnx.commit()
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            cnx.execute("INSERT Personne X: X nom 'bidule', "
+                       "X web 'http://www.debian.org', X test TRUE")
+            self.assertRaises(Unauthorized, cnx.commit)
+            cnx.execute("INSERT Personne X: X nom 'bidule', "
+                       "X web 'http://www.debian.org', X test FALSE")
+            self.assertRaises(Unauthorized, cnx.commit)
+            eid = cnx.execute("INSERT Personne X: X nom 'bidule', "
+                             "X web 'http://www.debian.org'")[0][0]
+            cnx.commit()
+            cnx.execute('SET X test FALSE WHERE X eid %(x)s', {'x': eid})
+            self.assertRaises(Unauthorized, cnx.commit)
+            cnx.execute('SET X test TRUE WHERE X eid %(x)s', {'x': eid})
+            self.assertRaises(Unauthorized, cnx.commit)
+            cnx.execute('SET X web "http://www.logilab.org" WHERE X eid %(x)s', {'x': eid})
+            cnx.commit()
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            cnx.execute('INSERT Frozable F: F name "Foo"')
+            cnx.commit()
+            cnx.execute('SET F name "Bar" WHERE F is Frozable')
+            cnx.commit()
+            cnx.execute('SET F name "BaBar" WHERE F is Frozable')
+            cnx.execute('SET F frozen True WHERE F is Frozable')
+            with self.assertRaises(Unauthorized):
+                cnx.commit()
+            cnx.rollback()
+            cnx.execute('SET F frozen True WHERE F is Frozable')
+            cnx.commit()
+            cnx.execute('SET F name "Bar" WHERE F is Frozable')
+            with self.assertRaises(Unauthorized):
+                cnx.commit()
+
+    def test_attribute_security_rqlexpr(self):
+        with self.admin_access.repo_cnx() as cnx:
+            # Note.para attribute editable by managers or if the note is in "todo" state
+            note = cnx.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
+            cnx.commit()
+            note.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
+            cnx.execute('SET X para "truc" WHERE X eid %(x)s', {'x': note.eid})
+            cnx.commit()
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            cnx.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note.eid})
+            self.assertRaises(Unauthorized, cnx.commit)
+            note2 = cnx.execute("INSERT Note X: X para 'bidule'").get_entity(0, 0)
+            cnx.commit()
+            note2.cw_adapt_to('IWorkflowable').fire_transition('markasdone')
+            cnx.commit()
+            self.assertEqual(len(cnx.execute('Any X WHERE X in_state S, S name "todo", X eid %(x)s',
+                                            {'x': note2.eid})),
+                              0)
+            cnx.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
+            self.assertRaises(Unauthorized, cnx.commit)
+            note2.cw_adapt_to('IWorkflowable').fire_transition('redoit')
+            cnx.commit()
+            cnx.execute("SET X para 'chouette' WHERE X eid %(x)s", {'x': note2.eid})
+            cnx.commit()
+            cnx.execute("INSERT Note X: X something 'A'")
+            self.assertRaises(Unauthorized, cnx.commit)
+            cnx.execute("INSERT Note X: X para 'zogzog', X something 'A'")
+            cnx.commit()
+            note = cnx.execute("INSERT Note X").get_entity(0,0)
+            cnx.commit()
+            note.cw_set(something=u'B')
+            cnx.commit()
+            note.cw_set(something=None, para=u'zogzog')
+            cnx.commit()
+
+    def test_attribute_read_security(self):
+        # anon not allowed to see users'login, but they can see users
+        login_rdef = self.repo.schema['CWUser'].rdef('login')
+        with self.temporary_permissions((login_rdef, {'read': ('users', 'managers')}),
+                                        CWUser={'read': ('guests', 'users', 'managers')}):
+            with self.new_access(u'anon').repo_cnx() as cnx:
+                rset = cnx.execute('CWUser X')
+                self.assertTrue(rset)
+                x = rset.get_entity(0, 0)
+                x.complete()
+                self.assertEqual(x.login, None)
+                self.assertTrue(x.creation_date)
+                x = rset.get_entity(1, 0)
+                x.complete()
+                self.assertEqual(x.login, None)
+                self.assertTrue(x.creation_date)
+
+    def test_yams_inheritance_and_security_bug(self):
+        with self.temporary_permissions(Division={'read': ('managers',
+                                                           ERQLExpression('X owned_by U'))}):
+            with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+                rqlst = self.repo.vreg.rqlhelper.parse('Any X WHERE X is_instance_of Societe')
+                self.repo.vreg.solutions(cnx, rqlst, {})
+                self.repo.vreg.rqlhelper.annotate(rqlst)
+                plan = cnx.repo.querier.plan_factory(rqlst, {}, cnx)
+                plan.preprocess(rqlst)
+                self.assertEqual(
+                    rqlst.as_string(),
+                    '(Any X WHERE X is IN(Societe, SubDivision)) UNION '
+                    '(Any X WHERE X is Division, EXISTS(X owned_by %(B)s))')
+
+
+class BaseSchemaSecurityTC(BaseSecurityTC):
+    """tests related to the base schema permission configuration"""
+
+    def test_user_can_delete_object_he_created(self):
+        # even if some other user have changed object'state
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            # due to security test, affaire has to concerne a societe the user owns
+            cnx.execute('INSERT Societe X: X nom "ARCTIA"')
+            cnx.execute('INSERT Affaire X: X ref "ARCT01", X concerne S WHERE S nom "ARCTIA"')
+            cnx.commit()
+        with self.admin_access.repo_cnx() as cnx:
+            affaire = cnx.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
+            affaire.cw_adapt_to('IWorkflowable').fire_transition('abort')
+            cnx.commit()
+            self.assertEqual(len(cnx.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01"')),
+                             1)
+            self.assertEqual(len(cnx.execute('TrInfo X WHERE X wf_info_for A, A ref "ARCT01",'
+                                              'X owned_by U, U login "admin"')),
+                             1) # TrInfo at the above state change
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            cnx.execute('DELETE Affaire X WHERE X ref "ARCT01"')
+            cnx.commit()
+            self.assertFalse(cnx.execute('Affaire X'))
+
+    def test_users_and_groups_non_readable_by_guests(self):
+        with self.repo.internal_cnx() as cnx:
+            admineid = cnx.execute('CWUser U WHERE U login "admin"').rows[0][0]
+        with self.new_access(u'anon').repo_cnx() as cnx:
+            anon = cnx.user
+            # anonymous user can only read itself
+            rset = cnx.execute('Any L WHERE X owned_by U, U login L')
+            self.assertEqual([['anon']], rset.rows)
+            rset = cnx.execute('CWUser X')
+            self.assertEqual([[anon.eid]], rset.rows)
+            # anonymous user can read groups (necessary to check allowed transitions for instance)
+            self.assertTrue(cnx.execute('CWGroup X'))
+            # should only be able to read the anonymous user, not another one
+            self.assertRaises(Unauthorized,
+                              cnx.execute, 'CWUser X WHERE X eid %(x)s', {'x': admineid})
+            rset = cnx.execute('CWUser X WHERE X eid %(x)s', {'x': anon.eid})
+            self.assertEqual([[anon.eid]], rset.rows)
+            # but can't modify it
+            cnx.execute('SET X login "toto" WHERE X eid %(x)s', {'x': anon.eid})
+            self.assertRaises(Unauthorized, cnx.commit)
+
+    def test_in_group_relation(self):
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            rql = u"DELETE U in_group G WHERE U login 'admin'"
+            self.assertRaises(Unauthorized, cnx.execute, rql)
+            rql = u"SET U in_group G WHERE U login 'admin', G name 'users'"
+            self.assertRaises(Unauthorized, cnx.execute, rql)
+
+    def test_owned_by(self):
+        with self.admin_access.repo_cnx() as cnx:
+            cnx.execute("INSERT Personne X: X nom 'bidule'")
+            cnx.commit()
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            rql = u"SET X owned_by U WHERE U login 'iaminusersgrouponly', X is Personne"
+            self.assertRaises(Unauthorized, cnx.execute, rql)
+
+    def test_bookmarked_by_guests_security(self):
+        with self.admin_access.repo_cnx() as cnx:
+            beid1 = cnx.execute('INSERT Bookmark B: B path "?vid=manage", B title "manage"')[0][0]
+            beid2 = cnx.execute('INSERT Bookmark B: B path "?vid=index", B title "index", '
+                                'B bookmarked_by U WHERE U login "anon"')[0][0]
+            cnx.commit()
+        with self.new_access(u'anon').repo_cnx() as cnx:
+            anoneid = cnx.user.eid
+            self.assertEqual(cnx.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
+                                         'B bookmarked_by U, U eid %s' % anoneid).rows,
+                              [['index', '?vid=index']])
+            self.assertEqual(cnx.execute('Any T,P ORDERBY lower(T) WHERE B is Bookmark,B title T,B path P,'
+                                         'B bookmarked_by U, U eid %(x)s', {'x': anoneid}).rows,
+                              [['index', '?vid=index']])
+            # can read others bookmarks as well
+            self.assertEqual(cnx.execute('Any B where B is Bookmark, NOT B bookmarked_by U').rows,
+                              [[beid1]])
+            self.assertRaises(Unauthorized, cnx.execute,'DELETE B bookmarked_by U')
+            self.assertRaises(Unauthorized,
+                              cnx.execute, 'SET B bookmarked_by U WHERE U eid %(x)s, B eid %(b)s',
+                              {'x': anoneid, 'b': beid1})
+
+    def test_ambigous_ordered(self):
+        with self.new_access(u'anon').repo_cnx() as cnx:
+            names = [t for t, in cnx.execute('Any N ORDERBY lower(N) WHERE X name N')]
+            self.assertEqual(names, sorted(names, key=lambda x: x.lower()))
+
+    def test_in_state_without_update_perm(self):
+        """check a user change in_state without having update permission on the
+        subject
+        """
+        with self.admin_access.repo_cnx() as cnx:
+            eid = cnx.execute('INSERT Affaire X: X ref "ARCT01"')[0][0]
+            cnx.commit()
+        with self.new_access(u'iaminusersgrouponly').repo_cnx() as cnx:
+            # needed to remove rql expr granting update perm to the user
+            affschema = self.schema['Affaire']
+            with self.temporary_permissions(Affaire={'update': affschema.get_groups('update'),
+                                                     'read': ('users',)}):
+                self.assertRaises(Unauthorized,
+                                  affschema.check_perm, cnx, 'update', eid=eid)
+                aff = cnx.execute('Any X WHERE X ref "ARCT01"').get_entity(0, 0)
+                aff.cw_adapt_to('IWorkflowable').fire_transition('abort')
+                cnx.commit()
+                # though changing a user state (even logged user) is reserved to managers
+                user = cnx.user
+                # XXX wether it should raise Unauthorized or ValidationError is not clear
+                # the best would probably ValidationError if the transition doesn't exist
+                # from the current state but Unauthorized if it exists but user can't pass it
+                self.assertRaises(ValidationError,
+                                  user.cw_adapt_to('IWorkflowable').fire_transition, 'deactivate')
+
+    def test_trinfo_security(self):
+        with self.admin_access.repo_cnx() as cnx:
+            aff = cnx.execute('INSERT Affaire X: X ref "ARCT01"').get_entity(0, 0)
+            iworkflowable = aff.cw_adapt_to('IWorkflowable')
+            cnx.commit()
+            iworkflowable.fire_transition('abort')
+            cnx.commit()
+            # can change tr info comment
+            cnx.execute('SET TI comment %(c)s WHERE TI wf_info_for X, X ref "ARCT01"',
+                         {'c': u'bouh!'})
+            cnx.commit()
+            aff.cw_clear_relation_cache('wf_info_for', 'object')
+            trinfo = iworkflowable.latest_trinfo()
+            self.assertEqual(trinfo.comment, 'bouh!')
+            # but not from_state/to_state
+            aff.cw_clear_relation_cache('wf_info_for', role='object')
+            self.assertRaises(Unauthorized, cnx.execute,
+                              'SET TI from_state S WHERE TI eid %(ti)s, S name "ben non"',
+                              {'ti': trinfo.eid})
+            self.assertRaises(Unauthorized, cnx.execute,
+                              'SET TI to_state S WHERE TI eid %(ti)s, S name "pitetre"',
+                              {'ti': trinfo.eid})
+
+    def test_emailaddress_security(self):
+        # check for prexisting email adresse
+        with self.admin_access.repo_cnx() as cnx:
+            if cnx.execute('Any X WHERE X is EmailAddress'):
+                rset = cnx.execute('Any X, U WHERE X is EmailAddress, U use_email X')
+                msg = ['Preexisting email readable by anon found!']
+                tmpl = '  - "%s" used by user "%s"'
+                for i in range(len(rset)):
+                    email, user = rset.get_entity(i, 0), rset.get_entity(i, 1)
+                    msg.append(tmpl % (email.dc_title(), user.dc_title()))
+                raise RuntimeError('\n'.join(msg))
+            # actual test
+            cnx.execute('INSERT EmailAddress X: X address "hop"').get_entity(0, 0)
+            cnx.execute('INSERT EmailAddress X: X address "anon", '
+                         'U use_email X WHERE U login "anon"').get_entity(0, 0)
+            cnx.commit()
+            self.assertEqual(len(cnx.execute('Any X WHERE X is EmailAddress')), 2)
+        with self.new_access(u'anon').repo_cnx() as cnx:
+            self.assertEqual(len(cnx.execute('Any X WHERE X is EmailAddress')), 1)
+
+if __name__ == '__main__':
+    unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/server/test/unittest_server_utils.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,426 @@
+# copyright 2003-2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""unit tests for module cubicweb.utils"""
+
+import base64
+import datetime
+import decimal
+import doctest
+import re
+from unittest import TestCase
+
+from cubicweb import Binary, Unauthorized
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.utils import (make_uid, UStringIO, RepeatList, HTMLHead,
+                            QueryCache)
+from cubicweb.entity import Entity
+
+try:
+    from cubicweb.utils import CubicWebJsonEncoder, json
+except ImportError:
+    json = None
+
+
+class MakeUidTC(TestCase):
+    def test_1(self):
+        self.assertNotEqual(make_uid('xyz'), make_uid('abcd'))
+        self.assertNotEqual(make_uid('xyz'), make_uid('xyz'))
+
+    def test_2(self):
+        d = set()
+        while len(d)<10000:
+            uid = make_uid('xyz')
+            if uid in d:
+                self.fail(len(d))
+            if re.match('\d', uid):
+                self.fail('make_uid must not return something begining with '
+                          'some numeric character, got %s' % uid)
+            d.add(uid)
+
+
+class TestQueryCache(TestCase):
+    def test_querycache(self):
+        c = QueryCache(ceiling=20)
+        # write only
+        for x in range(10):
+            c[x] = x
+        self.assertEqual(c._usage_report(),
+                         {'transientcount': 0,
+                          'itemcount': 10,
+                          'permanentcount': 0})
+        c = QueryCache(ceiling=10)
+        # we should also get a warning
+        for x in range(20):
+            c[x] = x
+        self.assertEqual(c._usage_report(),
+                         {'transientcount': 0,
+                          'itemcount': 10,
+                          'permanentcount': 0})
+        # write + reads
+        c = QueryCache(ceiling=20)
+        for n in range(4):
+            for x in range(10):
+                c[x] = x
+                c[x]
+        self.assertEqual(c._usage_report(),
+                         {'transientcount': 10,
+                          'itemcount': 10,
+                          'permanentcount': 0})
+        c = QueryCache(ceiling=20)
+        for n in range(17):
+            for x in range(10):
+                c[x] = x
+                c[x]
+        self.assertEqual(c._usage_report(),
+                         {'transientcount': 0,
+                          'itemcount': 10,
+                          'permanentcount': 10})
+        c = QueryCache(ceiling=20)
+        for n in range(17):
+            for x in range(10):
+                c[x] = x
+                if n % 2:
+                    c[x]
+                if x % 2:
+                    c[x]
+        self.assertEqual(c._usage_report(),
+                         {'transientcount': 5,
+                          'itemcount': 10,
+                          'permanentcount': 5})
+
+    def test_clear_on_overflow(self):
+        """Tests that only non-permanent items in the cache are wiped-out on ceiling overflow
+        """
+        c = QueryCache(ceiling=10)
+        # set 10 values
+        for x in range(10):
+            c[x] = x
+        # arrange for the first 5 to be permanent
+        for x in range(5):
+            for r in range(QueryCache._maxlevel + 2):
+                v = c[x]
+                self.assertEqual(v, x)
+        # Add the 11-th
+        c[10] = 10
+        self.assertEqual(c._usage_report(),
+                         {'transientcount': 0,
+                          'itemcount': 6,
+                          'permanentcount': 5})
+
+    def test_get_with_default(self):
+        """
+        Tests the capability of QueryCache for retrieving items with a default value
+        """
+        c = QueryCache(ceiling=20)
+        # set 10 values
+        for x in range(10):
+            c[x] = x
+        # arrange for the first 5 to be permanent
+        for x in range(5):
+            for r in range(QueryCache._maxlevel + 2):
+                v = c[x]
+                self.assertEqual(v, x)
+        self.assertEqual(c._usage_report(),
+                         {'transientcount': 0,
+                          'itemcount': 10,
+                          'permanentcount': 5})
+        # Test defaults for existing (including in permanents)
+        for x in range(10):
+            v = c.get(x, -1)
+            self.assertEqual(v, x)
+        # Test defaults for others
+        for x in range(10, 15):
+            v = c.get(x, -1)
+            self.assertEqual(v, -1)
+
+    def test_iterkeys(self):
+        """
+        Tests the iterating on keys in the cache
+        """
+        c = QueryCache(ceiling=20)
+        # set 10 values
+        for x in range(10):
+            c[x] = x
+        # arrange for the first 5 to be permanent
+        for x in range(5):
+            for r in range(QueryCache._maxlevel + 2):
+                v = c[x]
+                self.assertEqual(v, x)
+        self.assertEqual(c._usage_report(),
+                         {'transientcount': 0,
+                          'itemcount': 10,
+                          'permanentcount': 5})
+        keys = sorted(c)
+        for x in range(10):
+            self.assertEqual(x, keys[x])
+
+    def test_items(self):
+        """
+        Tests the iterating on key-value couples in the cache
+        """
+        c = QueryCache(ceiling=20)
+        # set 10 values
+        for x in range(10):
+            c[x] = x
+        # arrange for the first 5 to be permanent
+        for x in range(5):
+            for r in range(QueryCache._maxlevel + 2):
+                v = c[x]
+                self.assertEqual(v, x)
+        self.assertEqual(c._usage_report(),
+                         {'transientcount': 0,
+                          'itemcount': 10,
+                          'permanentcount': 5})
+        content = sorted(c.items())
+        for x in range(10):
+            self.assertEqual(x, content[x][0])
+            self.assertEqual(x, content[x][1])
+
+
+class UStringIOTC(TestCase):
+    def test_boolean_value(self):
+        self.assertTrue(UStringIO())
+
+
+class RepeatListTC(TestCase):
+
+    def test_base(self):
+        l = RepeatList(3, (1, 3))
+        self.assertEqual(l[0], (1, 3))
+        self.assertEqual(l[2], (1, 3))
+        self.assertEqual(l[-1], (1, 3))
+        self.assertEqual(len(l), 3)
+        # XXX
+        self.assertEqual(l[4], (1, 3))
+
+        self.assertFalse(RepeatList(0, None))
+
+    def test_slice(self):
+        l = RepeatList(3, (1, 3))
+        self.assertEqual(l[0:1], [(1, 3)])
+        self.assertEqual(l[0:4], [(1, 3)]*3)
+        self.assertEqual(l[:], [(1, 3)]*3)
+
+    def test_iter(self):
+        self.assertEqual(list(RepeatList(3, (1, 3))),
+                          [(1, 3)]*3)
+
+    def test_add(self):
+        l = RepeatList(3, (1, 3))
+        self.assertEqual(l + [(1, 4)], [(1, 3)]*3  + [(1, 4)])
+        self.assertEqual([(1, 4)] + l, [(1, 4)] + [(1, 3)]*3)
+        self.assertEqual(l + RepeatList(2, (2, 3)), [(1, 3)]*3 + [(2, 3)]*2)
+
+        x = l + RepeatList(2, (1, 3))
+        self.assertIsInstance(x, RepeatList)
+        self.assertEqual(len(x), 5)
+        self.assertEqual(x[0], (1, 3))
+
+        x = l + [(1, 3)] * 2
+        self.assertEqual(x, [(1, 3)] * 5)
+
+    def test_eq(self):
+        self.assertEqual(RepeatList(3, (1, 3)),
+                          [(1, 3)]*3)
+
+    def test_pop(self):
+        l = RepeatList(3, (1, 3))
+        l.pop(2)
+        self.assertEqual(l, [(1, 3)]*2)
+
+
+class JSONEncoderTC(TestCase):
+    def setUp(self):
+        if json is None:
+            self.skipTest('json not available')
+
+    def encode(self, value):
+        return json.dumps(value, cls=CubicWebJsonEncoder)
+
+    def test_encoding_dates(self):
+        self.assertEqual(self.encode(datetime.datetime(2009, 9, 9, 20, 30)),
+                          '"2009/09/09 20:30:00"')
+        self.assertEqual(self.encode(datetime.date(2009, 9, 9)),
+                          '"2009/09/09"')
+        self.assertEqual(self.encode(datetime.time(20, 30)),
+                          '"20:30:00"')
+
+    def test_encoding_decimal(self):
+        self.assertEqual(self.encode(decimal.Decimal('1.2')), '1.2')
+
+    def test_encoding_bare_entity(self):
+        e = Entity(None)
+        e.cw_attr_cache['pouet'] = 'hop'
+        e.eid = 2
+        self.assertEqual(json.loads(self.encode(e)),
+                          {'pouet': 'hop', 'eid': 2})
+
+    def test_encoding_entity_in_list(self):
+        e = Entity(None)
+        e.cw_attr_cache['pouet'] = 'hop'
+        e.eid = 2
+        self.assertEqual(json.loads(self.encode([e])),
+                          [{'pouet': 'hop', 'eid': 2}])
+
+    def test_encoding_binary(self):
+        for content in (b'he he', b'h\xe9 hxe9'):
+            with self.subTest(content=content):
+                encoded = self.encode(Binary(content))
+                self.assertEqual(base64.b64decode(encoded), content)
+
+    def test_encoding_unknown_stuff(self):
+        self.assertEqual(self.encode(TestCase), 'null')
+
+
+class HTMLHeadTC(CubicWebTC):
+
+    def htmlhead(self, datadir_url):
+        with self.admin_access.web_request() as req:
+            base_url = u'http://test.fr/data/'
+            req.datadir_url = base_url
+            head = HTMLHead(req)
+            return head
+
+    def test_concat_urls(self):
+        base_url = u'http://test.fr/data/'
+        head = self.htmlhead(base_url)
+        urls = [base_url + u'bob1.js',
+                base_url + u'bob2.js',
+                base_url + u'bob3.js']
+        result = head.concat_urls(urls)
+        expected = u'http://test.fr/data/??bob1.js,bob2.js,bob3.js'
+        self.assertEqual(result, expected)
+
+    def test_group_urls(self):
+        base_url = u'http://test.fr/data/'
+        head = self.htmlhead(base_url)
+        urls_spec = [(base_url + u'bob0.js', None),
+                     (base_url + u'bob1.js', None),
+                     (u'http://ext.com/bob2.js', None),
+                     (u'http://ext.com/bob3.js', None),
+                     (base_url + u'bob4.css', 'all'),
+                     (base_url + u'bob5.css', 'all'),
+                     (base_url + u'bob6.css', 'print'),
+                     (base_url + u'bob7.css', 'print'),
+                     (base_url + u'bob8.css', ('all', u'[if IE 8]')),
+                     (base_url + u'bob9.css', ('print', u'[if IE 8]'))
+                     ]
+        result = head.group_urls(urls_spec)
+        expected = [(base_url + u'??bob0.js,bob1.js', None),
+                    (u'http://ext.com/bob2.js', None),
+                    (u'http://ext.com/bob3.js', None),
+                    (base_url + u'??bob4.css,bob5.css', 'all'),
+                    (base_url + u'??bob6.css,bob7.css', 'print'),
+                    (base_url + u'bob8.css', ('all', u'[if IE 8]')),
+                    (base_url + u'bob9.css', ('print', u'[if IE 8]'))
+                    ]
+        self.assertEqual(list(result), expected)
+
+    def test_getvalue_with_concat(self):
+        self.config.global_set_option('concat-resources', True)
+        base_url = u'http://test.fr/data/'
+        head = self.htmlhead(base_url)
+        head.add_js(base_url + u'bob0.js')
+        head.add_js(base_url + u'bob1.js')
+        head.add_js(u'http://ext.com/bob2.js')
+        head.add_js(u'http://ext.com/bob3.js')
+        head.add_css(base_url + u'bob4.css')
+        head.add_css(base_url + u'bob5.css')
+        head.add_css(base_url + u'bob6.css', 'print')
+        head.add_css(base_url + u'bob7.css', 'print')
+        head.add_ie_css(base_url + u'bob8.css')
+        head.add_ie_css(base_url + u'bob9.css', 'print', u'[if lt IE 7]')
+        result = head.getvalue()
+        expected = u"""<head>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/??bob4.css,bob5.css"/>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/??bob6.css,bob7.css"/>
+<!--[if lt IE 8]>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob8.css"/>
+<!--[if lt IE 7]>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob9.css"/>
+<![endif]--> 
+<script type="text/javascript" src="http://test.fr/data/??bob0.js,bob1.js"></script>
+<script type="text/javascript" src="http://ext.com/bob2.js"></script>
+<script type="text/javascript" src="http://ext.com/bob3.js"></script>
+</head>
+"""
+        self.assertEqual(result, expected)
+
+    def test_getvalue_without_concat(self):
+        self.config.global_set_option('concat-resources', False)
+        try:
+            base_url = u'http://test.fr/data/'
+            head = self.htmlhead(base_url)
+            head.add_js(base_url + u'bob0.js')
+            head.add_js(base_url + u'bob1.js')
+            head.add_js(u'http://ext.com/bob2.js')
+            head.add_js(u'http://ext.com/bob3.js')
+            head.add_css(base_url + u'bob4.css')
+            head.add_css(base_url + u'bob5.css')
+            head.add_css(base_url + u'bob6.css', 'print')
+            head.add_css(base_url + u'bob7.css', 'print')
+            head.add_ie_css(base_url + u'bob8.css')
+            head.add_ie_css(base_url + u'bob9.css', 'print', u'[if lt IE 7]')
+            result = head.getvalue()
+            expected = u"""<head>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob4.css"/>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob5.css"/>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob6.css"/>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob7.css"/>
+<!--[if lt IE 8]>
+<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob8.css"/>
+<!--[if lt IE 7]>
+<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob9.css"/>
+<![endif]--> 
+<script type="text/javascript" src="http://test.fr/data/bob0.js"></script>
+<script type="text/javascript" src="http://test.fr/data/bob1.js"></script>
+<script type="text/javascript" src="http://ext.com/bob2.js"></script>
+<script type="text/javascript" src="http://ext.com/bob3.js"></script>
+</head>
+"""
+            self.assertEqual(result, expected)
+        finally:
+            self.config.global_set_option('concat-resources', True)
+
+
+def UnauthorizedTC(TestCase):
+
+    def _test(self, func):
+        self.assertEqual(func(Unauthorized()),
+                         'You are not allowed to perform this operation')
+        self.assertEqual(func(Unauthorized('a')),
+                         'a')
+        self.assertEqual(func(Unauthorized('a', 'b')),
+                         'You are not allowed to perform a operation on b')
+        self.assertEqual(func(Unauthorized('a', 'b', 'c')),
+                         'a b c')
+
+    def test_str(self):
+        self._test(str)
+
+
+
+def load_tests(loader, tests, ignore):
+    import cubicweb.utils
+    tests.addTests(doctest.DocTestSuite(cubicweb.utils))
+    return tests
+
+
+if __name__ == '__main__':
+    import unittest
+    unittest.main()
--- a/cubicweb/server/test/unittest_serverctl.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/test/unittest_serverctl.py	Fri Oct 18 23:39:03 2019 +0200
@@ -1,7 +1,6 @@
 import os.path as osp
 import shutil
-
-from mock import patch
+from unittest.mock import patch
 
 from cubicweb import ExecutionError
 from cubicweb.devtools import testlib, ApptestConfiguration
--- a/cubicweb/server/test/unittest_storage.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/test/unittest_storage.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,8 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """unit tests for module cubicweb.server.sources.storages"""
 
-from six import PY2
-
 from logilab.common.testlib import unittest_main, tag, Tags
 from cubicweb.devtools.testlib import CubicWebTC
 
@@ -79,7 +77,7 @@
     def fspath(self, cnx, entity):
         fspath = cnx.execute('Any fspath(D) WHERE F eid %(f)s, F data D',
                              {'f': entity.eid})[0][0].getvalue()
-        return fspath if PY2 else fspath.decode('utf-8')
+        return fspath.decode('utf-8')
 
     def test_bfss_wrong_fspath_usage(self):
         with self.admin_access.repo_cnx() as cnx:
--- a/cubicweb/server/test/unittest_undo.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/test/unittest_undo.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,8 +17,6 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
-from six import text_type
-
 from cubicweb import ValidationError
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.session import Connection
@@ -254,7 +252,7 @@
                 "%s doesn't exist anymore." % g.eid])
             with self.assertRaises(ValidationError) as cm:
                 cnx.commit()
-            cm.exception.translate(text_type)
+            cm.exception.translate(str)
             self.assertEqual(cm.exception.entity, self.totoeid)
             self.assertEqual(cm.exception.errors,
                               {'in_group-subject': u'at least one relation in_group is '
@@ -458,9 +456,9 @@
 class UndoExceptionInUnicode(CubicWebTC):
 
     # problem occurs in string manipulation for python < 2.6
-    def test___unicode__method(self):
+    def test___str__method(self):
         u = _UndoException(u"voilà")
-        self.assertIsInstance(text_type(u), text_type)
+        self.assertIsInstance(str(u), str)
 
 
 if __name__ == '__main__':
--- a/cubicweb/server/test/unittest_utils.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/test/unittest_utils.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,6 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Tests for cubicweb.server.utils module."""
 
+import sched
+
 from cubicweb.devtools import testlib
 from cubicweb.server import utils
 
@@ -40,7 +42,7 @@
         self.assertEqual(utils.crypt_password('yyy', ''), '')
 
     def test_schedule_periodic_task(self):
-        scheduler = utils.scheduler()
+        scheduler = sched.scheduler()
         this = []
 
         def fill_this(x):
--- a/cubicweb/server/utils.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/server/utils.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,24 +16,15 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """Some utilities for the CubicWeb server."""
-from __future__ import print_function
-
 
 from functools import wraps
-import sched
-import sys
 import logging
 from threading import Thread
 from getpass import getpass
 
-from six import PY2, text_type
-from six.moves import input
-
 from passlib.utils import handlers as uh, to_hash_str
 from passlib.context import CryptContext
 
-from logilab.common.deprecation import deprecated
-
 from cubicweb.md5crypt import crypt as md5crypt
 
 
@@ -60,9 +51,7 @@
 
 _CRYPTO_CTX = CryptContext(['sha512_crypt', CustomMD5Crypt, 'des_crypt', 'ldap_salted_sha1'],
                            deprecated=['cubicwebmd5crypt', 'des_crypt'])
-# for bw compat with passlib < 1.7
-if not hasattr(_CRYPTO_CTX, 'hash'):
-    _CRYPTO_CTX.hash = _CRYPTO_CTX.encrypt
+
 verify_and_update = _CRYPTO_CTX.verify_and_update
 
 
@@ -83,17 +72,6 @@
     return b''
 
 
-@deprecated('[3.22] no more necessary, directly get eschema.eid')
-def eschema_eid(cnx, eschema):
-    """get eid of the CWEType entity for the given yams type.
-
-    This used to be necessary because when the schema has been loaded from the
-    file-system, not from the database, (e.g. during tests), eschema.eid was
-    not set.
-    """
-    return eschema.eid
-
-
 DEFAULT_MSG = 'we need a manager connection on the repository \
 (the server doesn\'t have to run, even should better not)'
 
@@ -105,8 +83,6 @@
             print(msg)
         while not user:
             user = input('login: ')
-        if PY2:
-            user = text_type(user, sys.stdin.encoding)
     passwd = getpass('%s: ' % passwdmsg)
     if confirm:
         while True:
@@ -119,22 +95,6 @@
     return user, passwd
 
 
-if PY2:
-    import time  # noqa
-
-    class scheduler(sched.scheduler):
-        """Python2 version of sched.scheduler that matches Python3 API."""
-
-        def __init__(self, **kwargs):
-            kwargs.setdefault('timefunc', time.time)
-            kwargs.setdefault('delayfunc', time.sleep)
-            # sched.scheduler is an old-style class.
-            sched.scheduler.__init__(self, **kwargs)
-
-else:
-    scheduler = sched.scheduler
-
-
 def schedule_periodic_task(scheduler, interval, func, *args):
     """Enter a task with `func(*args)` as a periodic event in `scheduler`
     executing at `interval` seconds. Once executed, the task would re-schedule
--- a/cubicweb/skeleton/cubicweb_CUBENAME/__pkginfo__.py.tmpl	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/skeleton/cubicweb_CUBENAME/__pkginfo__.py.tmpl	Fri Oct 18 23:39:03 2019 +0200
@@ -20,6 +20,6 @@
 classifiers = [
     'Environment :: Web Environment',
     'Framework :: CubicWeb',
-    'Programming Language :: Python',
+    'Programming Language :: Python :: 3',
     'Programming Language :: JavaScript',
 ]
--- a/cubicweb/skeleton/debian/control.tmpl	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/skeleton/debian/control.tmpl	Fri Oct 18 23:39:03 2019 +0200
@@ -5,31 +5,12 @@
 Build-Depends:
  debhelper (>= 9),
  dh-python,
- python-all,
- python-setuptools,
- python-pytest,
- python-cubicweb,
  python3-all,
  python3-setuptools,
  python3-pytest,
- python3-cubicweb,
 Standards-Version: 4.3.0
-X-Python-Version: >= 2.7
 X-Python3-Version: >= 3.4
 
-Package: python-%(distname)s
-Architecture: all
-Depends:
- ${python:Depends},
- ${misc:Depends},
-Description: %(shortdesc)s
- CubicWeb is a semantic web application framework.
- .
- %(longdesc)s
- .
- This package will install all the components you need to run an application
- using the %(distname)s cube for Python 2.
-
 Package: python3-%(distname)s
 Architecture: all
 Depends:
@@ -41,4 +22,4 @@
  %(longdesc)s
  .
  This package will install all the components you need to run an application
- using the %(distname)s cube for Python 3.
+ using the %(distname)s cube.
--- a/cubicweb/skeleton/debian/rules.tmpl	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/skeleton/debian/rules.tmpl	Fri Oct 18 23:39:03 2019 +0200
@@ -4,4 +4,4 @@
 export PYBUILD_OPTION = --test-pytest
 
 %%:
-	dh $@ --with python2,python3 --buildsystem=pybuild
+	dh $@ --with python3 --buildsystem=pybuild
--- a/cubicweb/skeleton/debian/tests/pytest	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/skeleton/debian/tests/pytest	Fri Oct 18 23:39:03 2019 +0200
@@ -14,9 +14,9 @@
 
 ### Run tests
 
-for py in $(pyversions -r 2>/dev/null) $(py3versions -r 2>/dev/null); do
-       cd "$AUTOPKGTEST_TMP"
-       echo "Testing with $py:"
-       su nobody --shell /bin/sh \
-               -c "$py -m pytest -v"
+for py in $(py3versions -r 2>/dev/null); do
+	cd "$AUTOPKGTEST_TMP"
+	echo "Testing with $py:"
+	su nobody --shell /bin/sh \
+		-c "$py -m pytest -v"
 done
--- a/cubicweb/skeleton/tox.ini.tmpl	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/skeleton/tox.ini.tmpl	Fri Oct 18 23:39:03 2019 +0200
@@ -1,5 +1,5 @@
 [tox]
-envlist = py27,py3,flake8
+envlist = py3,flake8,check-manifest
 
 [testenv]
 deps =
@@ -14,5 +14,12 @@
   flake8
 commands = flake8
 
+[testenv:check-manifest]
+skip_install = true
+deps =
+  check-manifest
+commands =
+  {envpython} -m check_manifest {toxinidir}
+
 [flake8]
 exclude = cubicweb_%(cubename)s/migration/*,test/data/*,.tox/*
--- a/cubicweb/sobjects/ldapparser.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/sobjects/ldapparser.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 unlike ldapuser source, this source is copy based and will import ldap content
 (beside passwords for authentication) into the system source.
 """
-from six.moves import map, filter
-
 from logilab.common.decorators import cached, cachedproperty
 from logilab.common.shellutils import generate_password
 
--- a/cubicweb/sobjects/notification.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/sobjects/notification.py	Fri Oct 18 23:39:03 2019 +0200
@@ -22,8 +22,6 @@
 
 from itertools import repeat
 
-from six import text_type
-
 from logilab.common.textutils import normalize_text
 from logilab.common.registry import yes
 
@@ -179,7 +177,7 @@
     def context(self, **kwargs):
         entity = self.cw_rset.get_entity(self.cw_row or 0, self.cw_col or 0)
         for key, val in kwargs.items():
-            if val and isinstance(val, text_type) and val.strip():
+            if val and isinstance(val, str) and val.strip():
                 kwargs[key] = self._cw._(val)
         kwargs.update({'user': self.user_data['login'],
                        'eid': entity.eid,
@@ -252,7 +250,7 @@
 
 
 def format_value(value):
-    if isinstance(value, text_type):
+    if isinstance(value, str):
         return u'"%s"' % value
     return value
 
--- a/cubicweb/sobjects/services.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/sobjects/services.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,8 +19,6 @@
 
 import threading
 
-from six import text_type
-
 from cubicweb.server import Service
 from cubicweb.predicates import match_user_groups, match_kwargs
 
@@ -111,7 +109,7 @@
 
     def call(self, login, password, email=None, groups=None, **cwuserkwargs):
         cnx = self._cw
-        if isinstance(password, text_type):
+        if isinstance(password, str):
             # password should *always* be utf8 encoded
             password = password.encode('UTF8')
         cwuserkwargs['login'] = login
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/sobjects/test/data/cubicweb_card/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/sobjects/test/data/cubicweb_card/entities.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,15 @@
+from cubicweb.entities import AnyEntity, fetch_config
+
+
+class Card(AnyEntity):
+    __regid__ = 'Card'
+    rest_attr = 'wikiid'
+
+    fetch_attrs, cw_fetch_order = fetch_config(['title'])
+
+    def rest_path(self):
+        if self.wikiid:
+            return '%s/%s' % (str(self.e_schema).lower(),
+                              self._cw.url_quote(self.wikiid, safe='/'))
+        else:
+            return super(Card, self).rest_path()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/sobjects/test/data/cubicweb_card/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+from yams.buildobjs import EntityType, String, RichString
+
+
+class Card(EntityType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', 'users'),
+        'delete': ('managers', 'owners'),
+        'update': ('managers', 'owners',),
+        }
+
+    title = String(required=True, fulltextindexed=True, maxsize=256)
+    synopsis = String(fulltextindexed=True, maxsize=512,
+                      description=("an abstract for this card"))
+    content = RichString(fulltextindexed=True, internationalizable=True,
+                         default_format='text/rest')
+    wikiid = String(maxsize=64, unique=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/sobjects/test/data/cubicweb_comment/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/sobjects/test/data/cubicweb_comment/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,25 @@
+# pylint: disable=W0622
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""cubicweb-comment packaging information"""
+
+distname = "cubicweb-comment"
+modname = distname.split('-', 1)[1]
+
+numversion = (1, 4, 3)
+version = '.'.join(str(num) for num in numversion)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/sobjects/test/data/cubicweb_comment/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,26 @@
+from yams.buildobjs import (EntityType, RelationType, SubjectRelation,
+                            RichString)
+from cubicweb.schema import RRQLExpression
+
+
+class Comment(EntityType):
+    """a comment is a reply about another entity"""
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests',),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', 'owners',),
+        'update': ('managers', 'owners',),
+        }
+    content = RichString(required=True, fulltextindexed=True)
+    comments = SubjectRelation('Comment', cardinality='1*', composite='object')
+
+
+class comments(RelationType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', RRQLExpression('S owned_by U'),),
+        }
+    inlined = True
+    composite = 'object'
+    cardinality = '1*'
--- a/cubicweb/statsd_logger.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/statsd_logger.py	Fri Oct 18 23:39:03 2019 +0200
@@ -58,6 +58,7 @@
 
 import time
 import socket
+from contextlib import contextmanager
 
 _bucket = 'cubicweb'
 _address = None
@@ -87,19 +88,32 @@
     _socket = socket.socket(family, socket.SOCK_DGRAM)
 
 
+def teardown():
+    """Unconfigure the statsd endpoint
+
+    This is most likely only useful for unit tests"""
+    global _bucket, _address, _socket
+    _bucket = 'cubicweb'
+    _address = None
+    _socket = None
+
+
 def statsd_c(context, n=1):
     if _address is not None:
-        _socket.sendto('{0}.{1}:{2}|c\n'.format(_bucket, context, n), _address)
+        _socket.sendto('{0}.{1}:{2}|c\n'.format(_bucket, context, n).encode(),
+                       _address)
 
 
 def statsd_g(context, value):
     if _address is not None:
-        _socket.sendto('{0}.{1}:{2}|g\n'.format(_bucket, context, value), _address)
+        _socket.sendto('{0}.{1}:{2}|g\n'.format(_bucket, context, value).encode(),
+                       _address)
 
 
 def statsd_t(context, value):
     if _address is not None:
-        _socket.sendto('{0}.{1}:{2:.4f}|ms\n'.format(_bucket, context, value), _address)
+        _socket.sendto('{0}.{1}:{2:.4f}|ms\n'.format(_bucket, context, value).encode(),
+                       _address)
 
 
 class statsd_timeit(object):
@@ -125,7 +139,7 @@
         finally:
             dt = 1000 * (time.time() - t0)
             msg = '{0}.{1}:{2:.4f}|ms\n{0}.{1}:1|c\n'.format(
-                _bucket, self.__name__, dt)
+                _bucket, self.__name__, dt).encode()
             _socket.sendto(msg, _address)
 
     def __get__(self, obj, objtype):
@@ -134,3 +148,17 @@
             return self
         import functools
         return functools.partial(self.__call__, obj)
+
+
+@contextmanager
+def statsd_timethis(ctxmsg):
+    if _address is not None:
+        t0 = time.time()
+    try:
+        yield
+    finally:
+        if _address is not None:
+            dt = 1000 * (time.time() - t0)
+            msg = '{0}.{1}:{2:.4f}|ms\n{0}.{1}:1|c\n'.format(
+                _bucket, ctxmsg, dt).encode()
+            _socket.sendto(msg, _address)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data-rewrite/cubicweb_card/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data-rewrite/cubicweb_card/entities.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,15 @@
+from cubicweb.entities import AnyEntity, fetch_config
+
+
+class Card(AnyEntity):
+    __regid__ = 'Card'
+    rest_attr = 'wikiid'
+
+    fetch_attrs, cw_fetch_order = fetch_config(['title'])
+
+    def rest_path(self):
+        if self.wikiid:
+            return '%s/%s' % (str(self.e_schema).lower(),
+                              self._cw.url_quote(self.wikiid, safe='/'))
+        else:
+            return super(Card, self).rest_path()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data-rewrite/cubicweb_card/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+from yams.buildobjs import EntityType, String, RichString
+
+
+class Card(EntityType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', 'users'),
+        'delete': ('managers', 'owners'),
+        'update': ('managers', 'owners',),
+        }
+
+    title = String(required=True, fulltextindexed=True, maxsize=256)
+    synopsis = String(fulltextindexed=True, maxsize=512,
+                      description=("an abstract for this card"))
+    content = RichString(fulltextindexed=True, internationalizable=True,
+                         default_format='text/rest')
+    wikiid = String(maxsize=64, unique=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data-rewrite/cubicweb_localperms/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data-rewrite/cubicweb_localperms/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,42 @@
+from yams.buildobjs import EntityType, RelationType, RelationDefinition, String
+from cubicweb.schema import PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS
+
+
+class CWPermission(EntityType):
+    """entity type that may be used to construct some advanced security
+    configuration
+    """
+    __permissions__ = PUB_SYSTEM_ENTITY_PERMS
+
+    name = String(required=True, indexed=True, internationalizable=True,
+                  maxsize=100, description=(
+                      'name or identifier of the permission'))
+    label = String(required=True, internationalizable=True, maxsize=100,
+                   description=('distinct label to distinguate between other '
+                                'permission entity of the same name'))
+
+
+class granted_permission(RelationType):
+    """explicitly granted permission on an entity"""
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    # XXX cardinality = '*1'
+
+
+class require_permission(RelationType):
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+
+
+class require_group(RelationDefinition):
+    """groups to which the permission is granted"""
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    subject = 'CWPermission'
+    object = 'CWGroup'
+
+
+class has_group_permission(RelationDefinition):
+    """short cut relation for 'U in_group G, P require_group G' for efficiency
+    reason. This relation is set automatically, you should not set this.
+    """
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    subject = 'CWUser'
+    object = 'CWPermission'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_card/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_card/entities.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,15 @@
+from cubicweb.entities import AnyEntity, fetch_config
+
+
+class Card(AnyEntity):
+    __regid__ = 'Card'
+    rest_attr = 'wikiid'
+
+    fetch_attrs, cw_fetch_order = fetch_config(['title'])
+
+    def rest_path(self):
+        if self.wikiid:
+            return '%s/%s' % (str(self.e_schema).lower(),
+                              self._cw.url_quote(self.wikiid, safe='/'))
+        else:
+            return super(Card, self).rest_path()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_card/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+from yams.buildobjs import EntityType, String, RichString
+
+
+class Card(EntityType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', 'users'),
+        'delete': ('managers', 'owners'),
+        'update': ('managers', 'owners',),
+        }
+
+    title = String(required=True, fulltextindexed=True, maxsize=256)
+    synopsis = String(fulltextindexed=True, maxsize=512,
+                      description=("an abstract for this card"))
+    content = RichString(fulltextindexed=True, internationalizable=True,
+                         default_format='text/rest')
+    wikiid = String(maxsize=64, unique=True)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_comment/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_comment/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,25 @@
+# pylint: disable=W0622
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""cubicweb-comment packaging information"""
+
+distname = "cubicweb-comment"
+modname = distname.split('-', 1)[1]
+
+numversion = (1, 4, 3)
+version = '.'.join(str(num) for num in numversion)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_comment/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,26 @@
+from yams.buildobjs import (EntityType, RelationType, SubjectRelation,
+                            RichString)
+from cubicweb.schema import RRQLExpression
+
+
+class Comment(EntityType):
+    """a comment is a reply about another entity"""
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests',),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', 'owners',),
+        'update': ('managers', 'owners',),
+        }
+    content = RichString(required=True, fulltextindexed=True)
+    comments = SubjectRelation('Comment', cardinality='1*', composite='object')
+
+
+class comments(RelationType):
+    __permissions__ = {
+        'read':   ('managers', 'users', 'guests'),
+        'add':    ('managers', 'users',),
+        'delete': ('managers', RRQLExpression('S owned_by U'),),
+        }
+    inlined = True
+    composite = 'object'
+    cardinality = '1*'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_email/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_email/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,30 @@
+# pylint: disable=W0622
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""cubicweb-email packaging information"""
+
+distname = "cubicweb-email"
+modname = distname.split('-', 1)[1]
+
+numversion = (1, 4, 3)
+version = '.'.join(str(num) for num in numversion)
+
+
+__depends__ = {'cubicweb': None,
+               'cubicweb-file': None}
+__recommends__ = {'cubicweb-comment': None}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_email/entities.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,1 @@
+"test"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_email/hooks.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,1 @@
+"test"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_email/views/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,1 @@
+"test"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_file/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_file/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,25 @@
+# pylint: disable=W0622
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""cubicweb-file packaging information"""
+
+distname = "cubicweb-file"
+modname = distname.split('-', 1)[1]
+
+numversion = (1, 4, 3)
+version = '.'.join(str(num) for num in numversion)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_file/entities/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,1 @@
+"test"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_file/hooks/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,1 @@
+"test"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_file/views.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,1 @@
+"test"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_forge/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_forge/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,32 @@
+# pylint: disable=W0622
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""cubicweb-forge packaging information"""
+
+distname = "cubicweb-forge"
+modname = distname.split('-', 1)[1]
+
+numversion = (1, 4, 3)
+version = '.'.join(str(num) for num in numversion)
+
+
+__depends__ = {'cubicweb': None,
+               'cubicweb-file': None,
+               'cubicweb-email': None,
+               'cubicweb-comment': None,
+               }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_localperms/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_localperms/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,42 @@
+from yams.buildobjs import EntityType, RelationType, RelationDefinition, String
+from cubicweb.schema import PUB_SYSTEM_ENTITY_PERMS, PUB_SYSTEM_REL_PERMS
+
+
+class CWPermission(EntityType):
+    """entity type that may be used to construct some advanced security
+    configuration
+    """
+    __permissions__ = PUB_SYSTEM_ENTITY_PERMS
+
+    name = String(required=True, indexed=True, internationalizable=True,
+                  maxsize=100, description=(
+                      'name or identifier of the permission'))
+    label = String(required=True, internationalizable=True, maxsize=100,
+                   description=('distinct label to distinguate between other '
+                                'permission entity of the same name'))
+
+
+class granted_permission(RelationType):
+    """explicitly granted permission on an entity"""
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    # XXX cardinality = '*1'
+
+
+class require_permission(RelationType):
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+
+
+class require_group(RelationDefinition):
+    """groups to which the permission is granted"""
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    subject = 'CWPermission'
+    object = 'CWGroup'
+
+
+class has_group_permission(RelationDefinition):
+    """short cut relation for 'U in_group G, P require_group G' for efficiency
+    reason. This relation is set automatically, you should not set this.
+    """
+    __permissions__ = PUB_SYSTEM_REL_PERMS
+    subject = 'CWUser'
+    object = 'CWPermission'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_mycube/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,20 @@
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""mycube's __init__
+
+"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_mycube/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,22 @@
+# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+"""
+distname = 'cubicweb-mycube'
+version = '1.0.0'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_mycube/ccplugin.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,1 @@
+# simply there to test ccplugin module autoloading
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_tag/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_tag/entities.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,6 @@
+from cubicweb.entities import AnyEntity, fetch_config
+
+
+class Tag(AnyEntity):
+    __regid__ = 'Tag'
+    fetch_attrs, cw_fetch_order = fetch_config(['name'])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/data/cubicweb_tag/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+from yams.buildobjs import EntityType, String, SubjectRelation, RelationType
+
+
+class Tag(EntityType):
+    """tags are used by users to mark entities.
+    When you include the Tag entity, all application specific entities
+    may then be tagged using the "tags" relation.
+    """
+    name = String(required=True, fulltextindexed=True, unique=True,
+                  maxsize=128)
+    # when using this component, add the Tag tag X relation for each type that
+    # should be taggeable
+    tags = SubjectRelation('Tag', description="tagged objects")
+
+
+class tags(RelationType):
+    """indicates that an entity is classified by a given tag"""
--- a/cubicweb/test/data/legacy_cubes/comment/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- a/cubicweb/test/data/legacy_cubes/comment/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-# pylint: disable=W0622
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb-comment packaging information"""
-
-distname = "cubicweb-comment"
-modname = distname.split('-', 1)[1]
-
-numversion = (1, 4, 3)
-version = '.'.join(str(num) for num in numversion)
--- a/cubicweb/test/data/legacy_cubes/email/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- a/cubicweb/test/data/legacy_cubes/email/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-# pylint: disable=W0622
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb-email packaging information"""
-
-distname = "cubicweb-email"
-modname = distname.split('-', 1)[1]
-
-numversion = (1, 4, 3)
-version = '.'.join(str(num) for num in numversion)
-
-
-__depends__ = {'cubicweb': None,
-               'cubicweb-file': None}
-__recommends__ = {'cubicweb-comment': None}
--- a/cubicweb/test/data/legacy_cubes/email/entities.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-"test"
--- a/cubicweb/test/data/legacy_cubes/email/hooks.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-"test"
--- a/cubicweb/test/data/legacy_cubes/email/views/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-"test"
--- a/cubicweb/test/data/legacy_cubes/file/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- a/cubicweb/test/data/legacy_cubes/file/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-# pylint: disable=W0622
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb-file packaging information"""
-
-distname = "cubicweb-file"
-modname = distname.split('-', 1)[1]
-
-numversion = (1, 4, 3)
-version = '.'.join(str(num) for num in numversion)
--- a/cubicweb/test/data/legacy_cubes/file/entities/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-"test"
--- a/cubicweb/test/data/legacy_cubes/file/hooks/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-"test"
--- a/cubicweb/test/data/legacy_cubes/file/views.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-"test"
--- a/cubicweb/test/data/legacy_cubes/forge/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- a/cubicweb/test/data/legacy_cubes/forge/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-# pylint: disable=W0622
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb-forge packaging information"""
-
-distname = "cubicweb-forge"
-modname = distname.split('-', 1)[1]
-
-numversion = (1, 4, 3)
-version = '.'.join(str(num) for num in numversion)
-
-
-__depends__ = {'cubicweb': None,
-               'cubicweb-file': None,
-               'cubicweb-email': None,
-               'cubicweb-comment': None,
-               }
--- a/cubicweb/test/data/legacy_cubes/mycube/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""mycube's __init__
-
-"""
--- a/cubicweb/test/data/legacy_cubes/mycube/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-distname = 'cubicweb-mycube'
-version = '1.0.0'
--- a/cubicweb/test/data/legacy_cubes/mycube/ccplugin.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-# simply there to test ccplugin module autoloading
--- a/cubicweb/test/data/libpython/cubicweb_comment/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- a/cubicweb/test/data/libpython/cubicweb_comment/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-# pylint: disable=W0622
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb-comment packaging information"""
-
-distname = "cubicweb-comment"
-modname = distname.split('-', 1)[1]
-
-numversion = (1, 4, 3)
-version = '.'.join(str(num) for num in numversion)
--- a/cubicweb/test/data/libpython/cubicweb_email/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- a/cubicweb/test/data/libpython/cubicweb_email/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-# pylint: disable=W0622
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb-email packaging information"""
-
-distname = "cubicweb-email"
-modname = distname.split('-', 1)[1]
-
-numversion = (1, 4, 3)
-version = '.'.join(str(num) for num in numversion)
-
-
-__depends__ = {'cubicweb': None,
-               'cubicweb-file': None}
-__recommends__ = {'cubicweb-comment': None}
--- a/cubicweb/test/data/libpython/cubicweb_email/entities.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-"test"
--- a/cubicweb/test/data/libpython/cubicweb_email/hooks.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-"test"
--- a/cubicweb/test/data/libpython/cubicweb_email/views/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-"test"
--- a/cubicweb/test/data/libpython/cubicweb_file/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- a/cubicweb/test/data/libpython/cubicweb_file/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,25 +0,0 @@
-# pylint: disable=W0622
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb-file packaging information"""
-
-distname = "cubicweb-file"
-modname = distname.split('-', 1)[1]
-
-numversion = (1, 4, 3)
-version = '.'.join(str(num) for num in numversion)
--- a/cubicweb/test/data/libpython/cubicweb_file/entities/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-"test"
--- a/cubicweb/test/data/libpython/cubicweb_file/hooks/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-"test"
--- a/cubicweb/test/data/libpython/cubicweb_file/views.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-"test"
--- a/cubicweb/test/data/libpython/cubicweb_forge/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
--- a/cubicweb/test/data/libpython/cubicweb_forge/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,32 +0,0 @@
-# pylint: disable=W0622
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""cubicweb-forge packaging information"""
-
-distname = "cubicweb-forge"
-modname = distname.split('-', 1)[1]
-
-numversion = (1, 4, 3)
-version = '.'.join(str(num) for num in numversion)
-
-
-__depends__ = {'cubicweb': None,
-               'cubicweb-file': None,
-               'cubicweb-email': None,
-               'cubicweb-comment': None,
-               }
--- a/cubicweb/test/data/libpython/cubicweb_mycube/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""mycube's __init__
-
-"""
--- a/cubicweb/test/data/libpython/cubicweb_mycube/__pkginfo__.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-
-"""
-distname = 'cubicweb-mycube'
-version = '1.0.0'
--- a/cubicweb/test/data/libpython/cubicweb_mycube/ccplugin.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-# simply there to test ccplugin module autoloading
--- a/cubicweb/test/unittest_binary.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/test/unittest_binary.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 import os.path as osp
 import pickle
 
-from six import PY2
-
 from logilab.common.shellutils import tempdir
 
 from cubicweb import Binary
@@ -32,10 +30,7 @@
         Binary()
         Binary(b'toto')
         Binary(bytearray(b'toto'))
-        if PY2:
-            Binary(buffer('toto'))  # noqa: F821
-        else:
-            Binary(memoryview(b'toto'))
+        Binary(memoryview(b'toto'))
         with self.assertRaises((AssertionError, TypeError)):
             # TypeError is raised by BytesIO if python runs with -O
             Binary(u'toto')
@@ -44,10 +39,7 @@
         b = Binary()
         b.write(b'toto')
         b.write(bytearray(b'toto'))
-        if PY2:
-            b.write(buffer('toto'))  # noqa: F821
-        else:
-            b.write(memoryview(b'toto'))
+        b.write(memoryview(b'toto'))
         with self.assertRaises((AssertionError, TypeError)):
             # TypeError is raised by BytesIO if python runs with -O
             b.write(u'toto')
--- a/cubicweb/test/unittest_crypto.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/test/unittest_crypto.py	Fri Oct 18 23:39:03 2019 +0200
@@ -7,7 +7,7 @@
 
     def test_encrypt_decrypt_roundtrip(self):
         data = {'a': u'ah', 'b': [1, 2]}
-        seed = 'ssss'
+        seed = 's' * 16
         crypted = crypto.encrypt(data, seed)
         decrypted = crypto.decrypt(crypted, seed)
         self.assertEqual(decrypted, data)
--- a/cubicweb/test/unittest_cubes.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,155 +0,0 @@
-# copyright 2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Unit tests for "cubes" importer."""
-
-from contextlib import contextmanager
-import os
-from os import path
-import sys
-
-from six import PY2
-
-from cubicweb import _CubesImporter
-from cubicweb.cwconfig import CubicWebConfiguration
-from cubicweb.devtools.testlib import TemporaryDirectory, TestCase
-
-
-@contextmanager
-def temp_cube():
-    with TemporaryDirectory() as tempdir:
-        try:
-            libdir = path.join(tempdir, 'libpython')
-            cubedir = path.join(libdir, 'cubicweb_foo')
-            os.makedirs(cubedir)
-            check_code = ("import logging\n"
-                          "logging.getLogger('cubicweb_foo')"
-                          ".warn('imported %s', __name__)\n")
-            with open(path.join(cubedir, '__init__.py'), 'w') as f:
-                f.write("'cubicweb_foo application package'\n" + check_code)
-            with open(path.join(cubedir, 'bar.py'), 'w') as f:
-                f.write(check_code + 'baz = 1\n')
-            sys.path.append(libdir)
-            yield cubedir
-        finally:
-            sys.path.remove(libdir)
-
-
-class CubesImporterTC(TestCase):
-
-    def setUp(self):
-        # During discovery, CubicWebConfiguration.cls_adjust_sys_path may be
-        # called (probably because of cubicweb.devtools's __init__.py), so
-        # uninstall _CubesImporter.
-        for x in sys.meta_path:
-            if isinstance(x, _CubesImporter):
-                sys.meta_path.remove(x)
-        # Keep track of initial sys.path and sys.meta_path.
-        self.orig_sys_path = sys.path[:]
-        self.orig_sys_meta_path = sys.meta_path[:]
-
-    def tearDown(self):
-        # Cleanup any imported "cubes".
-        for name in list(sys.modules):
-            if name.startswith('cubes') or name.startswith('cubicweb_'):
-                del sys.modules[name]
-        # Restore sys.{meta_,}path
-        sys.path[:] = self.orig_sys_path
-        sys.meta_path[:] = self.orig_sys_meta_path
-
-    def test_importer_install(self):
-        _CubesImporter.install()
-        self.assertIsInstance(sys.meta_path[-1], _CubesImporter)
-
-    def test_config_installs_importer(self):
-        CubicWebConfiguration.cls_adjust_sys_path()
-        self.assertIsInstance(sys.meta_path[-1], _CubesImporter)
-
-    def test_import_cube_as_package_legacy_name(self):
-        """Check for import of an actual package-cube using legacy name"""
-        with temp_cube() as cubedir:
-            import cubicweb_foo  # noqa
-            del sys.modules['cubicweb_foo']
-            with self.assertRaises(ImportError):
-                import cubes.foo
-            CubicWebConfiguration.cls_adjust_sys_path()
-            import cubes.foo  # noqa
-            self.assertEqual(cubes.foo.__path__, [cubedir])
-            self.assertEqual(cubes.foo.__doc__,
-                             'cubicweb_foo application package')
-            # Import a submodule.
-            from cubes.foo import bar
-            self.assertEqual(bar.baz, 1)
-
-    def test_reload_cube(self):
-        """reloading cubes twice should return the same module"""
-        CubicWebConfiguration.cls_adjust_sys_path()
-        import cubes
-        if PY2:
-            new = reload(cubes)
-        else:
-            import importlib
-            new = importlib.reload(cubes)
-        self.assertIs(new, cubes)
-
-    def test_no_double_import(self):
-        """Check new and legacy import the same module once"""
-        with temp_cube():
-            CubicWebConfiguration.cls_adjust_sys_path()
-            with self.assertLogs('cubicweb_foo', 'WARNING') as cm:
-                from cubes.foo import bar
-                from cubicweb_foo import bar as bar2
-                self.assertIs(bar, bar2)
-                self.assertIs(sys.modules['cubes.foo'],
-                              sys.modules['cubicweb_foo'])
-            self.assertEqual(cm.output, [
-                'WARNING:cubicweb_foo:imported cubicweb_foo',
-                # module __name__ for subpackage differ along python version
-                # for PY2 it's based on how the module was imported "from
-                # cubes.foo import bar" and for PY3 based on __name__ of parent
-                # module "cubicweb_foo". Not sure if it's an issue, but PY3
-                # behavior looks better.
-                'WARNING:cubicweb_foo:imported ' + (
-                    'cubes.foo.bar' if PY2 else 'cubicweb_foo.bar')
-            ])
-
-    def test_import_legacy_cube(self):
-        """Check that importing a legacy cube works when sys.path got adjusted.
-        """
-        CubicWebConfiguration.cls_adjust_sys_path()
-        import cubes.card  # noqa
-
-    def test_import_cube_as_package_after_legacy_cube(self):
-        """Check import of a "cube as package" after a legacy cube."""
-        CubicWebConfiguration.cls_adjust_sys_path()
-        with temp_cube() as cubedir:
-            import cubes.card
-            import cubes.foo
-        self.assertEqual(cubes.foo.__path__, [cubedir])
-
-    def test_cube_inexistant(self):
-        """Check for import of an inexistant cube"""
-        CubicWebConfiguration.cls_adjust_sys_path()
-        with self.assertRaises(ImportError) as cm:
-            import cubes.doesnotexists  # noqa
-        msg = "No module named " + ("doesnotexists" if PY2 else "'cubes.doesnotexists'")
-        self.assertEqual(str(cm.exception), msg)
-
-
-if __name__ == '__main__':
-    import unittest
-    unittest.main()
--- a/cubicweb/test/unittest_cwconfig.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/test/unittest_cwconfig.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,22 +18,20 @@
 """cubicweb.cwconfig unit tests"""
 
 import contextlib
-import compileall
 import functools
 import sys
 import os
 import pkgutil
 from os.path import dirname, join
 from pkg_resources import EntryPoint, Distribution
+from tempfile import TemporaryDirectory
 import unittest
-
-from mock import patch
-from six import PY3
+from unittest.mock import patch
 
 from logilab.common.modutils import cleanup_sys_modules
 
 from cubicweb.devtools import ApptestConfiguration
-from cubicweb.devtools.testlib import BaseTestCase, TemporaryDirectory
+from cubicweb.devtools.testlib import BaseTestCase
 from cubicweb.cwconfig import (
     CubicWebConfiguration, _expand_modname)
 
@@ -61,33 +59,31 @@
 
 
 @contextlib.contextmanager
-def temp_config(appid, instance_dir, cubes_dir, cubes):
+def temp_config(appid, instance_dir, cubes):
     """context manager that create a config object with specified appid,
-    instance_dir, cubes_dir and cubes"""
+    instance_dir and cubes"""
     cls = CubicWebConfiguration
-    old = (cls._INSTANCES_DIR, cls.CUBES_DIR, cls.CUBES_PATH,
+    old = (cls._INSTANCES_DIR,
            sys.path[:], sys.meta_path[:])
     old_modules = set(sys.modules)
     try:
-        cls._INSTANCES_DIR, cls.CUBES_DIR, cls.CUBES_PATH = (
-            instance_dir, cubes_dir, [])
+        cls._INSTANCES_DIR = instance_dir
         config = cls(appid)
         config._cubes = cubes
         config.adjust_sys_path()
         yield config
     finally:
-        (cls._INSTANCES_DIR, cls.CUBES_DIR, cls.CUBES_PATH,
-         sys.path[:], sys.meta_path[:]) = old
+        (cls._INSTANCES_DIR, sys.path[:], sys.meta_path[:]) = old
         for module in set(sys.modules) - old_modules:
             del sys.modules[module]
 
 
 def iter_entry_points(group, name):
     """Mock pkg_resources.iter_entry_points to yield EntryPoint from
-    packages found in test/data/libpython even though these are not
+    packages found in test/data even though these are not
     installed.
     """
-    libpython = CubicWebConfigurationTC.datapath('libpython')
+    libpython = CubicWebConfigurationTC.datapath()
     prefix = 'cubicweb_'
     for pkgname in os.listdir(libpython):
         if not pkgname.startswith(prefix):
@@ -101,19 +97,18 @@
 
     @classmethod
     def setUpClass(cls):
-        sys.path.append(cls.datapath('libpython'))
+        sys.path.append(cls.datapath())
 
     @classmethod
     def tearDownClass(cls):
-        sys.path.remove(cls.datapath('libpython'))
+        sys.path.remove(cls.datapath())
 
     def setUp(self):
         self.config = ApptestConfiguration('data', __file__)
         self.config._cubes = ('email', 'file')
 
     def tearDown(self):
-        ApptestConfiguration.CUBES_PATH = []
-        cleanup_sys_modules([self.datapath('libpython')])
+        cleanup_sys_modules([self.datapath()])
 
     def test_migration_scripts_dir(self):
         mscripts = os.listdir(self.config.migration_scripts_dir())
@@ -124,17 +119,19 @@
     @patch('pkg_resources.iter_entry_points', side_effect=iter_entry_points)
     def test_available_cubes(self, mock_iter_entry_points):
         expected_cubes = [
-            'card', 'comment', 'cubicweb_comment', 'cubicweb_email', 'file',
-            'cubicweb_file', 'cubicweb_forge', 'localperms',
-            'cubicweb_mycube', 'tag',
+            'cubicweb_card',
+            'cubicweb_comment',
+            'cubicweb_email',
+            'cubicweb_file',
+            'cubicweb_forge',
+            'cubicweb_localperms',
+            'cubicweb_mycube',
+            'cubicweb_tag',
         ]
-        self._test_available_cubes(expected_cubes)
+        self.assertEqual(self.config.available_cubes(), expected_cubes)
         mock_iter_entry_points.assert_called_once_with(
             group='cubicweb.cubes', name=None)
 
-    def _test_available_cubes(self, expected_cubes):
-        self.assertEqual(self.config.available_cubes(), expected_cubes)
-
     def test_reorder_cubes(self):
         # forge depends on email and file and comment
         # email depends on file
@@ -187,71 +184,8 @@
         self.config.load_cwctl_plugins()
         mock_iter_entry_points.assert_called_once_with(
             group='cubicweb.cubes', name=None)
-        self.assertNotIn('cubes.mycube.ccplugin', sys.modules, sorted(sys.modules))
         self.assertIn('cubicweb_mycube.ccplugin', sys.modules, sorted(sys.modules))
 
-
-class CubicWebConfigurationWithLegacyCubesTC(CubicWebConfigurationTC):
-
-    @classmethod
-    def setUpClass(cls):
-        pass
-
-    @classmethod
-    def tearDownClass(cls):
-        pass
-
-    def setUp(self):
-        self.custom_cubes_dir = self.datapath('legacy_cubes')
-        cleanup_sys_modules([self.custom_cubes_dir, ApptestConfiguration.CUBES_DIR])
-        super(CubicWebConfigurationWithLegacyCubesTC, self).setUp()
-        self.config.__class__.CUBES_PATH = [self.custom_cubes_dir]
-        self.config.adjust_sys_path()
-
-    def tearDown(self):
-        ApptestConfiguration.CUBES_PATH = []
-
-    def test_available_cubes(self):
-        expected_cubes = sorted(set([
-            # local cubes
-            'comment', 'email', 'file', 'forge', 'mycube',
-            # test dependencies
-            'card', 'file', 'localperms', 'tag',
-        ]))
-        self._test_available_cubes(expected_cubes)
-
-    def test_reorder_cubes_recommends(self):
-        from cubes.comment import __pkginfo__ as comment_pkginfo
-        self._test_reorder_cubes_recommends(comment_pkginfo)
-
-    def test_cubes_path(self):
-        # make sure we don't import the email cube, but the stdlib email package
-        import email
-        self.assertNotEqual(dirname(email.__file__), self.config.CUBES_DIR)
-        self.config.__class__.CUBES_PATH = [self.custom_cubes_dir]
-        self.assertEqual(self.config.cubes_search_path(),
-                         [self.custom_cubes_dir, self.config.CUBES_DIR])
-        self.config.__class__.CUBES_PATH = [self.custom_cubes_dir,
-                                            self.config.CUBES_DIR, 'unexistant']
-        # filter out unexistant and duplicates
-        self.assertEqual(self.config.cubes_search_path(),
-                         [self.custom_cubes_dir,
-                          self.config.CUBES_DIR])
-        self.assertIn('mycube', self.config.available_cubes())
-        # test cubes python path
-        self.config.adjust_sys_path()
-        import cubes
-        self.assertEqual(cubes.__path__, self.config.cubes_search_path())
-        # this import should succeed once path is adjusted
-        from cubes import mycube
-        self.assertEqual(mycube.__path__, [join(self.custom_cubes_dir, 'mycube')])
-        # file cube should be overriden by the one found in data/cubes
-        sys.modules.pop('cubes.file')
-        if hasattr(cubes, 'file'):
-            del cubes.file
-        from cubes import file
-        self.assertEqual(file.__path__, [join(self.custom_cubes_dir, 'file')])
-
     def test_config_value_from_environment_str(self):
         self.assertIsNone(self.config['base-url'])
         os.environ['CW_BASE_URL'] = 'https://www.cubicweb.org'
@@ -279,11 +213,6 @@
         finally:
             del os.environ['CW_ALLOW_EMAIL_LOGIN']
 
-    def test_ccplugin_modname(self):
-        self.config.load_cwctl_plugins()
-        self.assertIn('cubes.mycube.ccplugin', sys.modules, sorted(sys.modules))
-        self.assertNotIn('cubicweb_mycube.ccplugin', sys.modules, sorted(sys.modules))
-
 
 class ModnamesTC(unittest.TestCase):
 
@@ -337,13 +266,6 @@
             join(tempdir, 'b', 'c.py'),
             join(tempdir, 'b', 'd', '__init__.py'),
         ):
-            if not PY3:
-                # ensure pyc file exists.
-                # Doesn't required for PY3 since it create __pycache__
-                # directory and will not import if source file doesn't
-                # exists.
-                compileall.compile_file(source, force=True)
-                self.assertTrue(os.path.exists(source + 'c'))
             # remove source file
             os.remove(source)
         self.assertEqual(list(_expand_modname('lib.c')), [])
@@ -368,9 +290,6 @@
             join(libdir, 'cubicweb_foo', 'schema', 'b.py'),
             # subpackages should not be loaded
             join(libdir, 'cubicweb_foo', 'schema', 'c', '__init__.py'),
-            join(libdir, 'cubes', '__init__.py'),
-            join(libdir, 'cubes', 'bar', '__init__.py'),
-            join(libdir, 'cubes', 'bar', 'schema.py'),
             join(libdir, '_instance_dir', 'data1', 'schema.py'),
             join(libdir, '_instance_dir', 'data2', 'noschema.py'),
         ):
@@ -380,24 +299,20 @@
             ('cubicweb', 'cubicweb.schemas.base'),
             ('cubicweb', 'cubicweb.schemas.workflow'),
             ('cubicweb', 'cubicweb.schemas.Bookmark'),
-            ('bar', 'cubes.bar.schema'),
             ('foo', 'cubicweb_foo.schema'),
             ('foo', 'cubicweb_foo.schema.a'),
             ('foo', 'cubicweb_foo.schema.b'),
         ]
         # app has schema file
-        instance_dir, cubes_dir = (
-            join(libdir, '_instance_dir'), join(libdir, 'cubes'))
-        with temp_config('data1', instance_dir, cubes_dir,
-                         ('foo', 'bar')) as config:
+        instance_dir = join(libdir, '_instance_dir')
+        with temp_config('data1', instance_dir, ('foo',)) as config:
             self.assertEqual(pkgutil.find_loader('schema').get_filename(),
                              join(libdir, '_instance_dir',
                                   'data1', 'schema.py'))
             self.assertEqual(config.schema_modnames(),
                              expected + [('data', 'schema')])
         # app doesn't have schema file
-        with temp_config('data2', instance_dir, cubes_dir,
-                         ('foo', 'bar')) as config:
+        with temp_config('data2', instance_dir, ('foo',)) as config:
             self.assertEqual(pkgutil.find_loader('schema').get_filename(),
                              join(libdir, 'schema.py'))
             self.assertEqual(config.schema_modnames(), expected)
@@ -414,15 +329,11 @@
             join(libdir, 'cubicweb_foo', 'entities', 'b', 'a.py'),
             join(libdir, 'cubicweb_foo', 'entities', 'b', 'c', '__init__.py'),
             join(libdir, 'cubicweb_foo', 'hooks.py'),
-            join(libdir, 'cubes', '__init__.py'),
-            join(libdir, 'cubes', 'bar', '__init__.py'),
-            join(libdir, 'cubes', 'bar', 'hooks.py'),
             join(libdir, '_instance_dir', 'data1', 'entities.py'),
             join(libdir, '_instance_dir', 'data2', 'hooks.py'),
         ):
             create_filepath(filepath)
-        instance_dir, cubes_dir = (
-            join(libdir, '_instance_dir'), join(libdir, 'cubes'))
+        instance_dir = join(libdir, '_instance_dir')
         expected = [
             'cubicweb.entities',
             'cubicweb.entities.adapters',
@@ -431,7 +342,6 @@
             'cubicweb.entities.schemaobjs',
             'cubicweb.entities.sources',
             'cubicweb.entities.wfobjs',
-            'cubes.bar.hooks',
             'cubicweb_foo.entities',
             'cubicweb_foo.entities.a',
             'cubicweb_foo.entities.b',
@@ -440,14 +350,12 @@
             'cubicweb_foo.hooks',
         ]
         # data1 has entities
-        with temp_config('data1', instance_dir, cubes_dir,
-                         ('foo', 'bar')) as config:
+        with temp_config('data1', instance_dir, ('foo',)) as config:
             config.cube_appobject_path = set(['entities', 'hooks'])
             self.assertEqual(config.appobjects_modnames(),
                              expected + ['entities'])
         # data2 has hooks
-        with temp_config('data2', instance_dir, cubes_dir,
-                         ('foo', 'bar')) as config:
+        with temp_config('data2', instance_dir, ('foo',)) as config:
             config.cube_appobject_path = set(['entities', 'hooks'])
             self.assertEqual(config.appobjects_modnames(),
                              expected + ['hooks'])
--- a/cubicweb/test/unittest_cwctl.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/test/unittest_cwctl.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,27 +18,28 @@
 import sys
 import os
 from os.path import join
-from io import StringIO, BytesIO
+from io import StringIO
 import unittest
+from unittest.mock import patch, MagicMock
 
-from six import PY2
+from logilab.common.clcommands import CommandLine
 
-from mock import patch
-
-from cubicweb.cwctl import ListCommand
+from cubicweb import utils, server
+from cubicweb.cwctl import ListCommand, InstanceCommand
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb.server.migractions import ServerMigrationHelper
+from cubicweb.cwconfig import CubicWebConfiguration as cwcfg
+from cubicweb.__pkginfo__ import version as cw_version
 
 import unittest_cwconfig
 
 
 class CubicWebCtlTC(unittest.TestCase):
-
     setUpClass = unittest_cwconfig.CubicWebConfigurationTC.setUpClass
     tearDownClass = unittest_cwconfig.CubicWebConfigurationTC.tearDownClass
 
     def setUp(self):
-        self.stream = BytesIO() if PY2 else StringIO()
+        self.stream = StringIO()
         sys.stdout = self.stream
 
     def tearDown(self):
@@ -81,5 +82,119 @@
                 mih.cmd_process_script(scriptname, None, scriptargs=args)
 
 
+class _TestCommand(InstanceCommand):
+    "I need some doc"
+    name = "test"
+    actionverb = 'failtested'
+
+    def test_instance(self, appid):
+        pass
+
+
+class _TestFailCommand(InstanceCommand):
+    "I need some doc"
+    name = "test_fail"
+    actionverb = 'tested'
+
+    def test_fail_instance(self, appid):
+        raise Exception()
+
+
+class InstanceCommandTest(unittest.TestCase):
+    def setUp(self):
+        self.CWCTL = CommandLine('cubicweb-ctl', 'The CubicWeb swiss-knife.',
+                                 version=cw_version, check_duplicated_command=False)
+        cwcfg.load_cwctl_plugins()
+        self.CWCTL.register(_TestCommand)
+        self.CWCTL.register(_TestFailCommand)
+
+        self.fake_config = MagicMock()
+        self.fake_config.global_set_option = MagicMock()
+
+        # pretend that this instance exists
+        config_patcher = patch.object(cwcfg, 'config_for', return_value=self.fake_config)
+        config_patcher.start()
+        self.addCleanup(config_patcher.stop)
+
+    @patch.object(_TestCommand, 'test_instance', return_value=0)
+    def test_getting_called(self, test_instance):
+        with self.assertRaises(SystemExit) as cm:
+            self.CWCTL.run(["test", "some_instance"])
+        self.assertEqual(cm.exception.code, 0)
+        test_instance.assert_called_with("some_instance")
+
+    @patch.object(utils, 'get_pdb')
+    def test_pdb_not_called(self, get_pdb):
+        # CWCTL will finish the program after that
+        with self.assertRaises(SystemExit) as cm:
+            self.CWCTL.run(["test", "some_instance"])
+        self.assertEqual(cm.exception.code, 0)
+
+        get_pdb.assert_not_called()
+
+    @patch.object(utils, 'get_pdb')
+    def test_pdb_called(self, get_pdb):
+        post_mortem = get_pdb.return_value.post_mortem
+        with self.assertRaises(SystemExit) as cm:
+            self.CWCTL.run(["test_fail", "some_instance", "--pdb"])
+        self.assertEqual(cm.exception.code, 8)
+
+        get_pdb.assert_called_once()
+        post_mortem.assert_called_once()
+
+        # we want post_mortem to actually receive the traceback
+        self.assertNotEqual(post_mortem.call_args, ((None,),))
+
+    @patch.dict(sys.modules, ipdb=MagicMock())
+    def test_ipdb_selected_and_called(self):
+        ipdb = sys.modules['ipdb']
+        with self.assertRaises(SystemExit) as cm:
+            self.CWCTL.run(["test_fail", "some_instance", "--pdb"])
+        self.assertEqual(cm.exception.code, 8)
+
+        ipdb.post_mortem.assert_called_once()
+
+    @patch.object(_TestFailCommand, 'test_fail_instance', side_effect=SystemExit(42))
+    def test_respect_return_error_code(self, test_fail_instance):
+        with self.assertRaises(SystemExit) as cm:
+            self.CWCTL.run(["test_fail", "some_instance"])
+        self.assertEqual(cm.exception.code, 42)
+
+        test_fail_instance.assert_called_once()
+
+    @patch.object(_TestFailCommand, 'test_fail_instance', side_effect=KeyboardInterrupt)
+    def test_error_code_keyboardinterupt_2(self, test_fail_instance):
+        with self.assertRaises(SystemExit) as cm:
+            self.CWCTL.run(["test_fail", "some_instance"])
+        self.assertEqual(cm.exception.code, 2)
+
+        test_fail_instance.assert_called_once()
+
+    def test_set_loglevel(self):
+        LOG_LEVELS = ('debug', 'info', 'warning', 'error')
+
+        for log_level in LOG_LEVELS:
+            with self.assertRaises(SystemExit) as cm:
+                self.CWCTL.run(["test", "some_instance", "--loglevel", log_level])
+            self.assertEqual(cm.exception.code, 0)
+
+            self.fake_config.global_set_option.assert_called_with('log-threshold',
+                                                                  log_level.upper())
+
+    @patch.object(server, "DEBUG", 0)
+    def test_set_dblevel(self):
+        DBG_FLAGS = ('RQL', 'SQL', 'REPO', 'HOOKS', 'OPS', 'SEC', 'MORE')
+
+        total_value = 0
+
+        for dbg_flag in DBG_FLAGS:
+            with self.assertRaises(SystemExit) as cm:
+                self.CWCTL.run(["test", "some_instance", "--dbglevel", dbg_flag])
+            self.assertEqual(cm.exception.code, 0)
+
+            total_value += getattr(server, "DBG_%s" % dbg_flag)
+            self.assertEqual(total_value, server.DEBUG)
+
+
 if __name__ == '__main__':
     unittest.main()
--- a/cubicweb/test/unittest_entity.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/test/unittest_entity.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 
 from datetime import datetime
 
-from six import text_type
-
 from logilab.common import tempattr
 from logilab.common.decorators import clear_cache
 
@@ -800,11 +798,11 @@
             # ambiguity test
             person2 = req.create_entity('Personne', prenom=u'remi', nom=u'doe')
             person.cw_clear_all_caches()
-            self.assertEqual(person.rest_path(), text_type(person.eid))
-            self.assertEqual(person2.rest_path(), text_type(person2.eid))
+            self.assertEqual(person.rest_path(), str(person.eid))
+            self.assertEqual(person2.rest_path(), str(person2.eid))
             # unique attr with None value (nom in this case)
             friend = req.create_entity('Ami', prenom=u'bob')
-            self.assertEqual(friend.rest_path(), text_type(friend.eid))
+            self.assertEqual(friend.rest_path(), str(friend.eid))
             # 'ref' below is created without the unique but not required
             # attribute, make sur that the unique _and_ required 'ean' is used
             # as the rest attribute
@@ -837,16 +835,6 @@
             note.cw_set(ecrit_par=person.eid)
             self.assertEqual(len(person.reverse_ecrit_par), 2)
 
-    def test_metainformation(self):
-        with self.admin_access.client_cnx() as cnx:
-            note = cnx.create_entity('Note', type=u'z')
-            cnx.commit()
-            note.cw_clear_all_caches()
-            metainf = note.cw_metainformation()
-            self.assertEqual(metainf, {'type': u'Note',
-                                       'extid': None,
-                                       'source': {'uri': 'system'}})
-
     def test_absolute_url_empty_field(self):
         with self.admin_access.web_request() as req:
             card = req.create_entity('Card', wikiid=u'', title=u'test')
--- a/cubicweb/test/unittest_migration.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/test/unittest_migration.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,11 +18,19 @@
 """cubicweb.migration unit tests"""
 
 from os.path import dirname, join
+from unittest.mock import patch
+
 from logilab.common.testlib import TestCase, unittest_main
 
-from cubicweb import devtools
+from cubicweb import devtools, utils
+from logilab.common.shellutils import ASK
 from cubicweb.cwconfig import CubicWebConfiguration
-from cubicweb.migration import filter_scripts, version_strictly_lower
+from cubicweb.migration import (
+    filter_scripts,
+    split_constraint,
+    version_strictly_lower,
+    MigrationHelper,
+)
 
 
 class Schema(dict):
@@ -113,5 +121,65 @@
                              [['activated']])
         repo.shutdown()
 
+def test_split_constraint():
+    assert split_constraint(">=0.1.0") == (">=", "0.1.0")
+    assert split_constraint(">= 0.1.0") == (">=", "0.1.0")
+    assert split_constraint(">0.1.1") == (">", "0.1.1")
+    assert split_constraint("> 0.1.1") == (">", "0.1.1")
+    assert split_constraint("<0.2.0") == ("<", "0.2.0")
+    assert split_constraint("< 0.2.0") == ("<", "0.2.0")
+    assert split_constraint("<=42.1.0") == ("<=", "42.1.0")
+    assert split_constraint("<= 42.1.0") == ("<=", "42.1.0")
+
+
+class WontColideWithOtherExceptionsException(Exception):
+    pass
+
+
+class MigrationHelperTC(TestCase):
+    @patch.object(utils, 'get_pdb')
+    @patch.object(ASK, 'ask', return_value="pdb")
+    def test_confirm_no_traceback(self, ask, get_pdb):
+        post_mortem = get_pdb.return_value.post_mortem
+        set_trace = get_pdb.return_value.set_trace
+
+        # we need to break after post_mortem is called otherwise we get
+        # infinite recursion
+        set_trace.side_effect = WontColideWithOtherExceptionsException
+
+        mh = MigrationHelper(config=None)
+
+        with self.assertRaises(WontColideWithOtherExceptionsException):
+            mh.confirm("some question")
+
+        get_pdb.assert_called_once()
+        set_trace.assert_called_once()
+        post_mortem.assert_not_called()
+
+    @patch.object(utils, 'get_pdb')
+    @patch.object(ASK, 'ask', return_value="pdb")
+    def test_confirm_got_traceback(self, ask, get_pdb):
+        post_mortem = get_pdb.return_value.post_mortem
+        set_trace = get_pdb.return_value.set_trace
+
+        # we need to break after post_mortem is called otherwise we get
+        # infinite recursion
+        post_mortem.side_effect = WontColideWithOtherExceptionsException
+
+        mh = MigrationHelper(config=None)
+
+        fake_traceback = object()
+
+        with self.assertRaises(WontColideWithOtherExceptionsException):
+            mh.confirm("some question", traceback=fake_traceback)
+
+        get_pdb.assert_called_once()
+        set_trace.assert_not_called()
+        post_mortem.assert_called_once()
+
+        # we want post_mortem to actually receive the traceback
+        self.assertEqual(post_mortem.call_args, ((fake_traceback,),))
+
+
 if __name__ == '__main__':
     unittest_main()
--- a/cubicweb/test/unittest_predicates.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/test/unittest_predicates.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 from operator import eq, lt, le, gt
 from contextlib import contextmanager
 
-from six.moves import range
-
 from logilab.common.testlib import TestCase, unittest_main
 from logilab.common.decorators import clear_cache
 
@@ -31,12 +29,10 @@
                                  multi_lines_rset, score_entity, is_in_state,
                                  rql_condition, relation_possible, match_form_params,
                                  paginated_rset)
-from cubicweb.selectors import on_transition # XXX on_transition is deprecated
 from cubicweb.view import EntityAdapter
 from cubicweb.web import action
 
 
-
 class ImplementsTC(CubicWebTC):
     def test_etype_priority(self):
         with self.admin_access.web_request() as req:
@@ -147,65 +143,6 @@
             self.assertEqual(str(cm.exception),
                              "wf_test: unknown state(s): unknown,weird")
 
-    def test_on_transition(self):
-        with self.statefull_stuff() as (req, wf_entity, rset, adapter):
-            for transition in ('validate', 'forsake'):
-                selector = on_transition(transition)
-                self.assertEqual(selector(None, req, rset=rset), 0)
-
-            adapter.fire_transition('validate')
-            req.cnx.commit(); wf_entity.cw_clear_all_caches()
-            self.assertEqual(adapter.state, 'validated')
-
-            clear_cache(rset, 'get_entity')
-
-            selector = on_transition("validate")
-            self.assertEqual(selector(None, req, rset=rset), 1)
-            selector = on_transition("validate", "forsake")
-            self.assertEqual(selector(None, req, rset=rset), 1)
-            selector = on_transition("forsake")
-            self.assertEqual(selector(None, req, rset=rset), 0)
-
-            adapter.fire_transition('forsake')
-            req.cnx.commit(); wf_entity.cw_clear_all_caches()
-            self.assertEqual(adapter.state, 'abandoned')
-
-            clear_cache(rset, 'get_entity')
-
-            selector = on_transition("validate")
-            self.assertEqual(selector(None, req, rset=rset), 0)
-            selector = on_transition("validate", "forsake")
-            self.assertEqual(selector(None, req, rset=rset), 1)
-            selector = on_transition("forsake")
-            self.assertEqual(selector(None, req, rset=rset), 1)
-
-    def test_on_transition_unvalid_names(self):
-        with self.statefull_stuff() as (req, wf_entity, rset, adapter):
-            selector = on_transition("unknown")
-            with self.assertRaises(ValueError) as cm:
-                selector(None, req, rset=rset)
-            self.assertEqual(str(cm.exception),
-                             "wf_test: unknown transition(s): unknown")
-            selector = on_transition("weird", "unknown", "validate", "weird")
-            with self.assertRaises(ValueError) as cm:
-                selector(None, req, rset=rset)
-            self.assertEqual(str(cm.exception),
-                             "wf_test: unknown transition(s): unknown,weird")
-
-    def test_on_transition_with_no_effect(self):
-        """selector will not be triggered with `change_state()`"""
-        with self.statefull_stuff() as (req, wf_entity, rset, adapter):
-            adapter.change_state('validated')
-            req.cnx.commit(); wf_entity.cw_clear_all_caches()
-            self.assertEqual(adapter.state, 'validated')
-
-            selector = on_transition("validate")
-            self.assertEqual(selector(None, req, rset=rset), 0)
-            selector = on_transition("validate", "forsake")
-            self.assertEqual(selector(None, req, rset=rset), 0)
-            selector = on_transition("forsake")
-            self.assertEqual(selector(None, req, rset=rset), 0)
-
 
 class RelationPossibleTC(CubicWebTC):
 
--- a/cubicweb/test/unittest_req.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/test/unittest_req.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,7 +18,7 @@
 
 from logilab.common.testlib import TestCase, unittest_main
 from cubicweb import ObjectNotFound
-from cubicweb.req import RequestSessionBase, FindEntityError
+from cubicweb.req import RequestSessionBase
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb import Unauthorized
 
@@ -62,64 +62,12 @@
         with self.admin_access.repo_cnx() as session:
             self.assertEqual(session.base_url(), base_url)
 
-    def test_secure_deprecated(self):
-        with self.admin_access.cnx() as cnx:
-            with self.assertWarns(DeprecationWarning):
-                cnx.base_url(secure=True)
-            with self.assertRaises(TypeError):
-                cnx.base_url(thing=42)
-            with self.assertWarns(DeprecationWarning):
-                cnx.build_url('ah', __secure__='whatever')
-
     def test_view_catch_ex(self):
         with self.admin_access.web_request() as req:
             rset = req.execute('CWUser X WHERE X login "hop"')
             self.assertEqual(req.view('oneline', rset, 'null'), '')
             self.assertRaises(ObjectNotFound, req.view, 'onelinee', rset, 'null')
 
-    def test_find_one_entity(self):
-        with self.admin_access.web_request() as req:
-            req.create_entity(
-                'CWUser', login=u'cdevienne', upassword=u'cdevienne',
-                surname=u'de Vienne', firstname=u'Christophe',
-                in_group=req.find('CWGroup', name=u'users').one())
-
-            req.create_entity(
-                'CWUser', login=u'adim', upassword='adim', surname=u'di mascio',
-                firstname=u'adrien',
-                in_group=req.find('CWGroup', name=u'users').one())
-
-            u = req.find_one_entity('CWUser', login=u'cdevienne')
-            self.assertEqual(u.firstname, u"Christophe")
-
-            with self.assertRaises(FindEntityError):
-                req.find_one_entity('CWUser', login=u'patanok')
-
-            with self.assertRaises(FindEntityError):
-                req.find_one_entity('CWUser')
-
-    def test_find_entities(self):
-        with self.admin_access.web_request() as req:
-            req.create_entity(
-                'CWUser', login=u'cdevienne', upassword=u'cdevienne',
-                surname=u'de Vienne', firstname=u'Christophe',
-                in_group=req.find('CWGroup', name=u'users').one())
-
-            req.create_entity(
-                'CWUser', login=u'adim', upassword='adim', surname=u'di mascio',
-                firstname=u'adrien',
-                in_group=req.find('CWGroup', name=u'users').one())
-
-            users = list(req.find_entities('CWUser', login=u'cdevienne'))
-            self.assertEqual(1, len(users))
-            self.assertEqual(users[0].firstname, u"Christophe")
-
-            users = list(req.find_entities('CWUser', login=u'patanok'))
-            self.assertEqual(0, len(users))
-
-            users = list(req.find_entities('CWUser'))
-            self.assertEqual(4, len(users))
-
     def test_find(self):
         with self.admin_access.web_request() as req:
             req.create_entity(
@@ -153,17 +101,17 @@
             users = list(rset.entities())
             self.assertEqual(len(users), 2)
 
-            with self.assertRaisesRegexp(
+            with self.assertRaisesRegex(
                 KeyError, "^'chapeau not in CWUser subject relations'$"
             ):
                 req.find('CWUser', chapeau=u"melon")
 
-            with self.assertRaisesRegexp(
+            with self.assertRaisesRegex(
                 KeyError, "^'buddy not in CWUser object relations'$"
             ):
                 req.find('CWUser', reverse_buddy=users[0])
 
-            with self.assertRaisesRegexp(
+            with self.assertRaisesRegex(
                 NotImplementedError, '^in_group: list of values are not supported$'
             ):
                 req.find('CWUser', in_group=[1, 2])
--- a/cubicweb/test/unittest_rqlrewrite.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/test/unittest_rqlrewrite.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,8 +16,6 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
-from six import string_types
-
 from logilab.common.testlib import mock_object
 from logilab.common.decorators import monkeypatch
 from yams import BadSchemaDefinition
@@ -88,7 +86,7 @@
                 snippet_varmap[snippet].update(varmap)
                 continue
             snippet_varmap[snippet] = varmap
-            if isinstance(snippet, string_types):
+            if isinstance(snippet, str):
                 snippet = mock_object(snippet_rqlst=parse(u'Any X WHERE ' + snippet).children[0],
                                       expression=u'Any X WHERE ' + snippet)
             rqlexprs.append(snippet)
@@ -602,12 +600,11 @@
     def process(self, rql, args=None):
         if args is None:
             args = {}
-        querier = self.repo.querier
         union = parse(rql)  # self.vreg.parse(rql, annotate=True)
         with self.admin_access.repo_cnx() as cnx:
             self.vreg.solutions(cnx, union, args)
-            querier._annotate(union)
-            plan = querier.plan_factory(union, args, cnx)
+            self.vreg.rqlhelper.annotate(union)
+            plan = self.repo.querier.plan_factory(union, args, cnx)
             plan.preprocess(union)
             return union
 
--- a/cubicweb/test/unittest_rset.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/test/unittest_rset.py	Fri Oct 18 23:39:03 2019 +0200
@@ -1,5 +1,5 @@
 # coding: utf-8
-# copyright 2003-2016 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# copyright 2003-2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
 #
 # This file is part of CubicWeb.
@@ -18,9 +18,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """unit tests for module cubicweb.rset"""
 
-from six import string_types
-from six.moves import cPickle as pickle
-from six.moves.urllib.parse import urlsplit
+import pickle
+from urllib.parse import urlsplit
 
 from rql import parse
 
@@ -444,6 +443,78 @@
             with self.assertRaises(MultipleResultsError):
                 req.execute('Any X WHERE X is CWUser').one()
 
+    def test_first(self):
+        with self.admin_access.web_request() as req:
+            req.create_entity('CWUser',
+                              login=u'cdevienne',
+                              upassword=u'cdevienne',
+                              surname=u'de Vienne',
+                              firstname=u'Christophe')
+            e = req.execute('Any X WHERE X login "cdevienne"').first()
+            self.assertEqual(e.surname, u'de Vienne')
+
+            e = req.execute(
+                'Any X, N WHERE X login "cdevienne", X surname N').first()
+            self.assertEqual(e.surname, u'de Vienne')
+
+            e = req.execute(
+                'Any N, X WHERE X login "cdevienne", X surname N').first(col=1)
+            self.assertEqual(e.surname, u'de Vienne')
+
+    def test_first_no_rows(self):
+        with self.admin_access.web_request() as req:
+            with self.assertRaises(NoResultError):
+                req.execute('Any X WHERE X login "patanok"').first()
+
+    def test_first_multiple_rows(self):
+        with self.admin_access.web_request() as req:
+            req.create_entity(
+                'CWUser', login=u'user1', upassword=u'cdevienne',
+                surname=u'de Vienne', firstname=u'Christophe')
+            req.create_entity(
+                'CWUser', login=u'user2', upassword='adim',
+                surname=u'di mascio', firstname=u'adrien')
+
+            e = req.execute('Any X ORDERBY X WHERE X is CWUser, '
+                            'X login LIKE "user%"').first()
+            self.assertEqual(e.login, 'user1')
+
+    def test_last(self):
+        with self.admin_access.web_request() as req:
+            req.create_entity('CWUser',
+                              login=u'cdevienne',
+                              upassword=u'cdevienne',
+                              surname=u'de Vienne',
+                              firstname=u'Christophe')
+            e = req.execute('Any X WHERE X login "cdevienne"').last()
+            self.assertEqual(e.surname, u'de Vienne')
+
+            e = req.execute(
+                'Any X, N WHERE X login "cdevienne", X surname N').last()
+            self.assertEqual(e.surname, u'de Vienne')
+
+            e = req.execute(
+                'Any N, X WHERE X login "cdevienne", X surname N').last(col=1)
+            self.assertEqual(e.surname, u'de Vienne')
+
+    def test_last_no_rows(self):
+        with self.admin_access.web_request() as req:
+            with self.assertRaises(NoResultError):
+                req.execute('Any X WHERE X login "patanok"').last()
+
+    def test_last_multiple_rows(self):
+        with self.admin_access.web_request() as req:
+            req.create_entity(
+                'CWUser', login=u'user1', upassword=u'cdevienne',
+                surname=u'de Vienne', firstname=u'Christophe')
+            req.create_entity(
+                'CWUser', login=u'user2', upassword='adim',
+                surname=u'di mascio', firstname=u'adrien')
+
+            e = req.execute('Any X ORDERBY X WHERE X is CWUser, '
+                            'X login LIKE "user%"').last()
+            self.assertEqual(e.login, 'user2')
+
     def test_related_entity_optional(self):
         with self.admin_access.web_request() as req:
             req.create_entity('Bookmark', title=u'aaaa', path=u'path')
@@ -583,17 +654,17 @@
     def test_str(self):
         with self.admin_access.web_request() as req:
             rset = req.execute('(Any X,N WHERE X is CWGroup, X name N)')
-            self.assertIsInstance(str(rset), string_types)
+            self.assertIsInstance(str(rset), str)
             self.assertEqual(len(str(rset).splitlines()), 1)
 
     def test_repr(self):
         with self.admin_access.web_request() as req:
             rset = req.execute('(Any X,N WHERE X is CWGroup, X name N)')
-            self.assertIsInstance(repr(rset), string_types)
+            self.assertIsInstance(repr(rset), str)
             self.assertTrue(len(repr(rset).splitlines()) > 1)
 
             rset = req.execute('(Any X WHERE X is CWGroup, X name "managers")')
-            self.assertIsInstance(str(rset), string_types)
+            self.assertIsInstance(str(rset), str)
             self.assertEqual(len(str(rset).splitlines()), 1)
 
     def test_slice(self):
--- a/cubicweb/test/unittest_rtags.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/test/unittest_rtags.py	Fri Oct 18 23:39:03 2019 +0200
@@ -137,18 +137,6 @@
                          {'key0': 'val00', 'key4': 'val4'})
 
 
-class DeprecatedInstanceWithoutModule(BaseTestCase):
-
-    def test_deprecated_instance_without_module(self):
-        class SubRelationTags(RelationTags):
-            pass
-        with self.assertWarnsRegex(
-            DeprecationWarning,
-            'instantiate SubRelationTags with __module__=__name__',
-        ):
-            SubRelationTags()
-
-
 if __name__ == '__main__':
     import unittest
     unittest.main()
--- a/cubicweb/test/unittest_schema.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/test/unittest_schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -171,7 +171,7 @@
         expected_entities = [
             'Ami', 'BaseTransition', 'BigInt', 'Bookmark', 'Boolean', 'Bytes', 'Card',
             'Date', 'Datetime', 'Decimal',
-            'CWCache', 'CWComputedRType', 'CWConstraint',
+            'CWComputedRType', 'CWConstraint',
             'CWConstraintType', 'CWDataImport', 'CWEType',
             'CWAttribute', 'CWGroup', 'EmailAddress',
             'CWRelation', 'CWPermission', 'CWProperty', 'CWRType', 'CWSession',
@@ -227,7 +227,7 @@
             'specializes', 'start_timestamp', 'state_of', 'status', 'subworkflow',
             'subworkflow_exit', 'subworkflow_state', 'surname', 'symmetric', 'synopsis',
 
-            'tags', 'timestamp', 'title', 'to_entity', 'to_state', 'transition_of', 'travaille',
+            'tags', 'title', 'to_entity', 'to_state', 'transition_of', 'travaille',
             'type',
 
             'upassword', 'update_permission', 'url', 'uri', 'use_email',
@@ -457,6 +457,20 @@
         self.assertNotEqual(ERQLExpression('X is CWUser', 'X', 0),
                             ERQLExpression('X is CWGroup', 'X', 0))
 
+    def test_has_update_permission(self):
+        expr = ERQLExpression('P use_email X, U has_update_permission P')
+        rql, found, keyarg = expr.transform_has_permission()
+        self.assertEqual(rql, 'Any X,P WHERE P use_email X, X eid %(x)s')
+        self.assertEqual(found, [(u'update', 1)])
+        self.assertEqual(keyarg, None)
+
+    def test_expression(self):
+        expr = ERQLExpression('U use_email X')
+        rql, found, keyarg = expr.transform_has_permission()
+        self.assertEqual(rql, 'Any X WHERE EXISTS(U use_email X, X eid %(x)s, U eid %(u)s)')
+        self.assertEqual(found, None)
+        self.assertEqual(keyarg, None)
+
 
 class GuessRrqlExprMainVarsTC(TestCase):
     def test_exists(self):
@@ -512,7 +526,6 @@
                      ('cw_source', 'BaseTransition', 'CWSource', 'object'),
                      ('cw_source', 'Bookmark', 'CWSource', 'object'),
                      ('cw_source', 'CWAttribute', 'CWSource', 'object'),
-                     ('cw_source', 'CWCache', 'CWSource', 'object'),
                      ('cw_source', 'CWComputedRType', 'CWSource', 'object'),
                      ('cw_source', 'CWConstraint', 'CWSource', 'object'),
                      ('cw_source', 'CWConstraintType', 'CWSource', 'object'),
@@ -576,32 +589,5 @@
                                          for r, role in schema[etype].composite_rdef_roles]))
 
 
-class CWShemaTC(CubicWebTC):
-
-    def test_transform_has_permission_match(self):
-        with self.admin_access.repo_cnx() as cnx:
-            eschema = cnx.vreg.schema['EmailAddress']
-            rql_exprs = eschema.get_rqlexprs('update')
-            self.assertEqual(len(rql_exprs), 1)
-            self.assertEqual(rql_exprs[0].expression,
-                             'P use_email X, U has_update_permission P')
-            rql, found, keyarg = rql_exprs[0].transform_has_permission()
-            self.assertEqual(rql, 'Any X,P WHERE P use_email X, X eid %(x)s')
-            self.assertEqual(found, [(u'update', 1)])
-            self.assertEqual(keyarg, None)
-
-    def test_transform_has_permission_no_match(self):
-        with self.admin_access.repo_cnx() as cnx:
-            eschema = cnx.vreg.schema['EmailAddress']
-            rql_exprs = eschema.get_rqlexprs('read')
-            self.assertEqual(len(rql_exprs), 1)
-            self.assertEqual(rql_exprs[0].expression,
-                             'U use_email X')
-            rql, found, keyarg = rql_exprs[0].transform_has_permission()
-            self.assertEqual(rql, 'Any X WHERE EXISTS(U use_email X, X eid %(x)s, U eid %(u)s)')
-            self.assertEqual(found, None)
-            self.assertEqual(keyarg, None)
-
-
 if __name__ == '__main__':
     unittest_main()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/test/unittest_statsd.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,136 @@
+# -*- coding: utf-8 -*-
+# copyright 2018 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of CubicWeb.
+#
+# CubicWeb is free software: you can redistribute it and/or modify it under the
+# terms of the GNU Lesser General Public License as published by the Free
+# Software Foundation, either version 2.1 of the License, or (at your option)
+# any later version.
+#
+# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
+"""unit tests for module cubicweb.statsd_logger"""
+
+import threading
+import socket
+import time
+import re
+
+from unittest import TestCase
+from cubicweb import statsd_logger as statsd
+
+
+UDP_PORT = None
+RUNNING = True
+SOCK = socket.socket(socket.AF_INET,
+                     socket.SOCK_DGRAM)
+SOCK.settimeout(0.1)
+STATSD = None
+DATA = []
+
+
+def statsd_rcv():
+    while RUNNING:
+        try:
+            data, addr = SOCK.recvfrom(1024)
+            if data:
+                rcv = [row.strip().decode() for row in data.splitlines()]
+                DATA.extend(rcv)
+        except socket.timeout:
+            pass
+
+
+def setUpModule(*args):
+    global UDP_PORT, STATSD
+    SOCK.bind(('127.0.0.1', 0))
+    UDP_PORT = SOCK.getsockname()[1]
+    STATSD = threading.Thread(target=statsd_rcv)
+    STATSD.start()
+    statsd.setup('test', ('127.0.0.1', UDP_PORT))
+
+
+def tearDownModule(*args):
+    global RUNNING
+    RUNNING = False
+    STATSD.join()
+    statsd.teardown()
+
+
+class StatsdTC(TestCase):
+
+    def setUp(self):
+        super(StatsdTC, self).setUp()
+        DATA[:] = []
+
+    def check_received(self, value):
+        for i in range(10):
+            if value in DATA:
+                break
+            time.sleep(0.01)
+        else:
+            self.assertIn(value, DATA)
+
+    def check_received_ms(self, value):
+        value = re.compile(value.replace('?', r'\d'))
+        for i in range(10):
+            if [x for x in DATA if value.match(x)]:
+                break
+            time.sleep(0.01)
+        else:
+            self.assertTrue([x for x in DATA if value.match(x)], DATA)
+
+    def test_statsd_c(self):
+        statsd.statsd_c('context')
+        self.check_received('test.context:1|c')
+        statsd.statsd_c('context', 10)
+        self.check_received('test.context:10|c')
+
+    def test_statsd_g(self):
+        statsd.statsd_g('context', 42)
+        self.check_received('test.context:42|g')
+        statsd.statsd_g('context', 'Igorrr')
+        self.check_received('test.context:Igorrr|g')
+
+    def test_statsd_t(self):
+        statsd.statsd_t('context', 1)
+        self.check_received('test.context:1.0000|ms')
+        statsd.statsd_t('context', 10)
+        self.check_received('test.context:10.0000|ms')
+        statsd.statsd_t('context', 0.12344)
+        self.check_received('test.context:0.1234|ms')
+        statsd.statsd_t('context', 0.12345)
+        self.check_received('test.context:0.1235|ms')
+
+    def test_decorator(self):
+
+        @statsd.statsd_timeit
+        def measure_me_please():
+            "some nice function"
+            return 42
+
+        self.assertEqual(measure_me_please.__doc__,
+                         "some nice function")
+
+        measure_me_please()
+        self.check_received_ms('test.measure_me_please:0.0???|ms')
+        self.check_received('test.measure_me_please:1|c')
+
+    def test_context_manager(self):
+
+        with statsd.statsd_timethis('cm'):
+            time.sleep(0.1)
+
+        self.check_received_ms('test.cm:100.????|ms')
+        self.check_received('test.cm:1|c')
+
+
+if __name__ == '__main__':
+    from unittest import main
+    main()
--- a/cubicweb/test/unittest_uilib.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/test/unittest_uilib.py	Fri Oct 18 23:39:03 2019 +0200
@@ -23,11 +23,7 @@
 
 import doctest
 import pkg_resources
-
-try:
-    from unittest import skipIf
-except ImportError:
-    from unittest2 import skipIf
+from unittest import skipIf
 
 from logilab.common.testlib import TestCase, unittest_main
 
--- a/cubicweb/test/unittest_utils.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,346 +0,0 @@
-# copyright 2003-2017 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""unit tests for module cubicweb.utils"""
-
-import base64
-import datetime
-import decimal
-import doctest
-import re
-try:
-    from unittest2 import TestCase
-except ImportError:  # Python3
-    from unittest import TestCase
-
-from six import PY2
-from six.moves import range
-
-from cubicweb import Binary, Unauthorized
-from cubicweb.devtools.testlib import CubicWebTC
-from cubicweb.utils import (make_uid, UStringIO, RepeatList, HTMLHead,
-                            QueryCache)
-from cubicweb.entity import Entity
-
-try:
-    from cubicweb.utils import CubicWebJsonEncoder, json
-except ImportError:
-    json = None
-
-
-class MakeUidTC(TestCase):
-    def test_1(self):
-        self.assertNotEqual(make_uid('xyz'), make_uid('abcd'))
-        self.assertNotEqual(make_uid('xyz'), make_uid('xyz'))
-
-    def test_2(self):
-        d = set()
-        while len(d)<10000:
-            uid = make_uid('xyz')
-            if uid in d:
-                self.fail(len(d))
-            if re.match('\d', uid):
-                self.fail('make_uid must not return something begining with '
-                          'some numeric character, got %s' % uid)
-            d.add(uid)
-
-
-class TestQueryCache(TestCase):
-    def test_querycache(self):
-        c = QueryCache(ceiling=20)
-        # write only
-        for x in range(10):
-            c[x] = x
-        self.assertEqual(c._usage_report(),
-                         {'transientcount': 0,
-                          'itemcount': 10,
-                          'permanentcount': 0})
-        c = QueryCache(ceiling=10)
-        # we should also get a warning
-        for x in range(20):
-            c[x] = x
-        self.assertEqual(c._usage_report(),
-                         {'transientcount': 0,
-                          'itemcount': 10,
-                          'permanentcount': 0})
-        # write + reads
-        c = QueryCache(ceiling=20)
-        for n in range(4):
-            for x in range(10):
-                c[x] = x
-                c[x]
-        self.assertEqual(c._usage_report(),
-                         {'transientcount': 10,
-                          'itemcount': 10,
-                          'permanentcount': 0})
-        c = QueryCache(ceiling=20)
-        for n in range(17):
-            for x in range(10):
-                c[x] = x
-                c[x]
-        self.assertEqual(c._usage_report(),
-                         {'transientcount': 0,
-                          'itemcount': 10,
-                          'permanentcount': 10})
-        c = QueryCache(ceiling=20)
-        for n in range(17):
-            for x in range(10):
-                c[x] = x
-                if n % 2:
-                    c[x]
-                if x % 2:
-                    c[x]
-        self.assertEqual(c._usage_report(),
-                         {'transientcount': 5,
-                          'itemcount': 10,
-                          'permanentcount': 5})
-
-class UStringIOTC(TestCase):
-    def test_boolean_value(self):
-        self.assertTrue(UStringIO())
-
-
-class RepeatListTC(TestCase):
-
-    def test_base(self):
-        l = RepeatList(3, (1, 3))
-        self.assertEqual(l[0], (1, 3))
-        self.assertEqual(l[2], (1, 3))
-        self.assertEqual(l[-1], (1, 3))
-        self.assertEqual(len(l), 3)
-        # XXX
-        self.assertEqual(l[4], (1, 3))
-
-        self.assertFalse(RepeatList(0, None))
-
-    def test_slice(self):
-        l = RepeatList(3, (1, 3))
-        self.assertEqual(l[0:1], [(1, 3)])
-        self.assertEqual(l[0:4], [(1, 3)]*3)
-        self.assertEqual(l[:], [(1, 3)]*3)
-
-    def test_iter(self):
-        self.assertEqual(list(RepeatList(3, (1, 3))),
-                          [(1, 3)]*3)
-
-    def test_add(self):
-        l = RepeatList(3, (1, 3))
-        self.assertEqual(l + [(1, 4)], [(1, 3)]*3  + [(1, 4)])
-        self.assertEqual([(1, 4)] + l, [(1, 4)] + [(1, 3)]*3)
-        self.assertEqual(l + RepeatList(2, (2, 3)), [(1, 3)]*3 + [(2, 3)]*2)
-
-        x = l + RepeatList(2, (1, 3))
-        self.assertIsInstance(x, RepeatList)
-        self.assertEqual(len(x), 5)
-        self.assertEqual(x[0], (1, 3))
-
-        x = l + [(1, 3)] * 2
-        self.assertEqual(x, [(1, 3)] * 5)
-
-    def test_eq(self):
-        self.assertEqual(RepeatList(3, (1, 3)),
-                          [(1, 3)]*3)
-
-    def test_pop(self):
-        l = RepeatList(3, (1, 3))
-        l.pop(2)
-        self.assertEqual(l, [(1, 3)]*2)
-
-
-class JSONEncoderTC(TestCase):
-    def setUp(self):
-        if json is None:
-            self.skipTest('json not available')
-
-    def encode(self, value):
-        return json.dumps(value, cls=CubicWebJsonEncoder)
-
-    def test_encoding_dates(self):
-        self.assertEqual(self.encode(datetime.datetime(2009, 9, 9, 20, 30)),
-                          '"2009/09/09 20:30:00"')
-        self.assertEqual(self.encode(datetime.date(2009, 9, 9)),
-                          '"2009/09/09"')
-        self.assertEqual(self.encode(datetime.time(20, 30)),
-                          '"20:30:00"')
-
-    def test_encoding_decimal(self):
-        self.assertEqual(self.encode(decimal.Decimal('1.2')), '1.2')
-
-    def test_encoding_bare_entity(self):
-        e = Entity(None)
-        e.cw_attr_cache['pouet'] = 'hop'
-        e.eid = 2
-        self.assertEqual(json.loads(self.encode(e)),
-                          {'pouet': 'hop', 'eid': 2})
-
-    def test_encoding_entity_in_list(self):
-        e = Entity(None)
-        e.cw_attr_cache['pouet'] = 'hop'
-        e.eid = 2
-        self.assertEqual(json.loads(self.encode([e])),
-                          [{'pouet': 'hop', 'eid': 2}])
-
-    def test_encoding_binary(self):
-        for content in (b'he he', b'h\xe9 hxe9'):
-            with self.subTest(content=content):
-                encoded = self.encode(Binary(content))
-                self.assertEqual(base64.b64decode(encoded), content)
-
-    def test_encoding_unknown_stuff(self):
-        self.assertEqual(self.encode(TestCase), 'null')
-
-
-class HTMLHeadTC(CubicWebTC):
-
-    def htmlhead(self, datadir_url):
-        with self.admin_access.web_request() as req:
-            base_url = u'http://test.fr/data/'
-            req.datadir_url = base_url
-            head = HTMLHead(req)
-            return head
-
-    def test_concat_urls(self):
-        base_url = u'http://test.fr/data/'
-        head = self.htmlhead(base_url)
-        urls = [base_url + u'bob1.js',
-                base_url + u'bob2.js',
-                base_url + u'bob3.js']
-        result = head.concat_urls(urls)
-        expected = u'http://test.fr/data/??bob1.js,bob2.js,bob3.js'
-        self.assertEqual(result, expected)
-
-    def test_group_urls(self):
-        base_url = u'http://test.fr/data/'
-        head = self.htmlhead(base_url)
-        urls_spec = [(base_url + u'bob0.js', None),
-                     (base_url + u'bob1.js', None),
-                     (u'http://ext.com/bob2.js', None),
-                     (u'http://ext.com/bob3.js', None),
-                     (base_url + u'bob4.css', 'all'),
-                     (base_url + u'bob5.css', 'all'),
-                     (base_url + u'bob6.css', 'print'),
-                     (base_url + u'bob7.css', 'print'),
-                     (base_url + u'bob8.css', ('all', u'[if IE 8]')),
-                     (base_url + u'bob9.css', ('print', u'[if IE 8]'))
-                     ]
-        result = head.group_urls(urls_spec)
-        expected = [(base_url + u'??bob0.js,bob1.js', None),
-                    (u'http://ext.com/bob2.js', None),
-                    (u'http://ext.com/bob3.js', None),
-                    (base_url + u'??bob4.css,bob5.css', 'all'),
-                    (base_url + u'??bob6.css,bob7.css', 'print'),
-                    (base_url + u'bob8.css', ('all', u'[if IE 8]')),
-                    (base_url + u'bob9.css', ('print', u'[if IE 8]'))
-                    ]
-        self.assertEqual(list(result), expected)
-
-    def test_getvalue_with_concat(self):
-        self.config.global_set_option('concat-resources', True)
-        base_url = u'http://test.fr/data/'
-        head = self.htmlhead(base_url)
-        head.add_js(base_url + u'bob0.js')
-        head.add_js(base_url + u'bob1.js')
-        head.add_js(u'http://ext.com/bob2.js')
-        head.add_js(u'http://ext.com/bob3.js')
-        head.add_css(base_url + u'bob4.css')
-        head.add_css(base_url + u'bob5.css')
-        head.add_css(base_url + u'bob6.css', 'print')
-        head.add_css(base_url + u'bob7.css', 'print')
-        head.add_ie_css(base_url + u'bob8.css')
-        head.add_ie_css(base_url + u'bob9.css', 'print', u'[if lt IE 7]')
-        result = head.getvalue()
-        expected = u"""<head>
-<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/??bob4.css,bob5.css"/>
-<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/??bob6.css,bob7.css"/>
-<!--[if lt IE 8]>
-<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob8.css"/>
-<!--[if lt IE 7]>
-<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob9.css"/>
-<![endif]--> 
-<script type="text/javascript" src="http://test.fr/data/??bob0.js,bob1.js"></script>
-<script type="text/javascript" src="http://ext.com/bob2.js"></script>
-<script type="text/javascript" src="http://ext.com/bob3.js"></script>
-</head>
-"""
-        self.assertEqual(result, expected)
-
-    def test_getvalue_without_concat(self):
-        self.config.global_set_option('concat-resources', False)
-        try:
-            base_url = u'http://test.fr/data/'
-            head = self.htmlhead(base_url)
-            head.add_js(base_url + u'bob0.js')
-            head.add_js(base_url + u'bob1.js')
-            head.add_js(u'http://ext.com/bob2.js')
-            head.add_js(u'http://ext.com/bob3.js')
-            head.add_css(base_url + u'bob4.css')
-            head.add_css(base_url + u'bob5.css')
-            head.add_css(base_url + u'bob6.css', 'print')
-            head.add_css(base_url + u'bob7.css', 'print')
-            head.add_ie_css(base_url + u'bob8.css')
-            head.add_ie_css(base_url + u'bob9.css', 'print', u'[if lt IE 7]')
-            result = head.getvalue()
-            expected = u"""<head>
-<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob4.css"/>
-<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob5.css"/>
-<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob6.css"/>
-<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob7.css"/>
-<!--[if lt IE 8]>
-<link rel="stylesheet" type="text/css" media="all" href="http://test.fr/data/bob8.css"/>
-<!--[if lt IE 7]>
-<link rel="stylesheet" type="text/css" media="print" href="http://test.fr/data/bob9.css"/>
-<![endif]--> 
-<script type="text/javascript" src="http://test.fr/data/bob0.js"></script>
-<script type="text/javascript" src="http://test.fr/data/bob1.js"></script>
-<script type="text/javascript" src="http://ext.com/bob2.js"></script>
-<script type="text/javascript" src="http://ext.com/bob3.js"></script>
-</head>
-"""
-            self.assertEqual(result, expected)
-        finally:
-            self.config.global_set_option('concat-resources', True)
-
-
-def UnauthorizedTC(TestCase):
-
-    def _test(self, func):
-        self.assertEqual(func(Unauthorized()),
-                         'You are not allowed to perform this operation')
-        self.assertEqual(func(Unauthorized('a')),
-                         'a')
-        self.assertEqual(func(Unauthorized('a', 'b')),
-                         'You are not allowed to perform a operation on b')
-        self.assertEqual(func(Unauthorized('a', 'b', 'c')),
-                         'a b c')
-
-    def test_str(self):
-        self._test(str)
-
-    if PY2:
-        def test_unicode(self):
-            self._test(unicode)
-
-
-def load_tests(loader, tests, ignore):
-    import cubicweb.utils
-    tests.addTests(doctest.DocTestSuite(cubicweb.utils))
-    return tests
-
-
-if __name__ == '__main__':
-    import unittest
-    unittest.main()
--- a/cubicweb/toolsutils.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/toolsutils.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,8 +16,6 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """some utilities for cubicweb command line tools"""
-from __future__ import print_function
-
 
 # XXX move most of this in logilab.common (shellutils ?)
 
@@ -39,8 +37,6 @@
     def symlink(*args):
         raise NotImplementedError
 
-from six import add_metaclass
-
 from logilab.common.clcommands import Command as BaseCommand
 from logilab.common.shellutils import ASK
 
@@ -239,8 +235,7 @@
         return cls
 
 
-@add_metaclass(metacmdhandler)
-class CommandHandler(object):
+class CommandHandler(object, metaclass=metacmdhandler):
     """configuration specific helper for cubicweb-ctl commands"""
 
     def __init__(self, config):
--- a/cubicweb/uilib.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/uilib.py	Fri Oct 18 23:39:03 2019 +0200
@@ -28,11 +28,8 @@
 import re
 from io import StringIO
 
-from six import PY2, PY3, text_type, binary_type, string_types, integer_types
-
 from logilab.mtconverter import xml_escape, html_unescape
 from logilab.common.date import ustrftime
-from logilab.common.deprecation import deprecated
 
 from cubicweb import _
 from cubicweb.utils import js_dumps
@@ -65,7 +62,7 @@
     return value
 
 def print_int(value, req, props, displaytime=True):
-    return text_type(value)
+    return str(value)
 
 def print_date(value, req, props, displaytime=True):
     return ustrftime(value, req.property_value('ui.date-format'))
@@ -95,7 +92,7 @@
 _('%d seconds')
 
 def print_timedelta(value, req, props, displaytime=True):
-    if isinstance(value, integer_types):
+    if isinstance(value, int):
         # `date - date`, unlike `datetime - datetime` gives an int
         # (number of days), not a timedelta
         # XXX should rql be fixed to return Int instead of Interval in
@@ -125,7 +122,7 @@
     return req._('no')
 
 def print_float(value, req, props, displaytime=True):
-    return text_type(req.property_value('ui.float-format') % value) # XXX cast needed ?
+    return str(req.property_value('ui.float-format') % value) # XXX cast needed ?
 
 PRINTERS = {
     'Bytes': print_bytes,
@@ -143,10 +140,6 @@
     'Interval': print_timedelta,
     }
 
-@deprecated('[3.14] use req.printable_value(attrtype, value, ...)')
-def printable_value(req, attrtype, value, props=None, displaytime=True):
-    return req.printable_value(attrtype, value, props, displaytime)
-
 def css_em_num_value(vreg, propname, default):
     """ we try to read an 'em' css property
     if we get another unit we're out of luck and resort to the given default
@@ -337,11 +330,10 @@
     def __init__(self, id, parent=None):
         self.id = id
         self.parent = parent
-    def __unicode__(self):
+    def __str__(self):
         if self.parent:
             return u'%s.%s' % (self.parent, self.id)
-        return text_type(self.id)
-    __str__ = __unicode__ if PY3 else lambda self: self.__unicode__().encode('utf-8')
+        return str(self.id)
     def __getattr__(self, attr):
         return _JSId(attr, self)
     def __call__(self, *args):
@@ -352,14 +344,13 @@
         assert isinstance(args, tuple)
         self.args = args
         self.parent = parent
-    def __unicode__(self):
+    def __str__(self):
         args = []
         for arg in self.args:
             args.append(js_dumps(arg))
         if self.parent:
             return u'%s(%s)' % (self.parent, ','.join(args))
         return ','.join(args)
-    __str__ = __unicode__ if PY3 else lambda self: self.__unicode__().encode('utf-8')
 
 class _JS(object):
     def __getattr__(self, attr):
@@ -392,7 +383,7 @@
                               'img', 'area', 'input', 'col'))
 
 def sgml_attributes(attrs):
-    return u' '.join(u'%s="%s"' % (attr, xml_escape(text_type(value)))
+    return u' '.join(u'%s="%s"' % (attr, xml_escape(str(value)))
                      for attr, value in sorted(attrs.items())
                      if value is not None)
 
@@ -410,7 +401,7 @@
         value += u' ' + sgml_attributes(attrs)
     if content:
         if escapecontent:
-            content = xml_escape(text_type(content))
+            content = xml_escape(str(content))
         value += u'>%s</%s>' % (content, tag)
     else:
         if tag in HTML4_EMPTY_TAGS:
@@ -439,7 +430,7 @@
     stream = StringIO() #UStringIO() don't want unicode assertion
     formater.format(layout, stream)
     res = stream.getvalue()
-    if isinstance(res, binary_type):
+    if isinstance(res, bytes):
         res = res.decode('UTF8')
     return res
 
@@ -448,16 +439,7 @@
 import traceback
 
 def exc_message(ex, encoding):
-    if PY3:
-        excmsg = str(ex)
-    else:
-        try:
-            excmsg = unicode(ex)
-        except Exception:
-            try:
-                excmsg = unicode(str(ex), encoding, 'replace')
-            except Exception:
-                excmsg = unicode(repr(ex), encoding, 'replace')
+    excmsg = str(ex)
     exctype = ex.__class__.__name__
     return u'%s: %s' % (exctype, excmsg)
 
@@ -469,8 +451,6 @@
         res.append(u'\tFile %s, line %s, function %s' % tuple(stackentry[:3]))
         if stackentry[3]:
             data = xml_escape(stackentry[3])
-            if PY2:
-                data = data.decode('utf-8', 'replace')
             res.append(u'\t  %s' % data)
     res.append(u'\n')
     try:
@@ -506,8 +486,6 @@
             xml_escape(stackentry[0]), stackentry[1], xml_escape(stackentry[2])))
         if stackentry[3]:
             string = xml_escape(stackentry[3])
-            if PY2:
-                string = string.decode('utf-8', 'replace')
             strings.append(u'&#160;&#160;%s<br/>\n' % (string))
         # add locals info for each entry
         try:
@@ -550,16 +528,8 @@
         self.wfunc(data)
 
     def writerow(self, row):
-        if PY3:
-            self.writer.writerow(row)
-            return
-        csvrow = []
-        for elt in row:
-            if isinstance(elt, text_type):
-                csvrow.append(elt.encode(self.encoding))
-            else:
-                csvrow.append(str(elt))
-        self.writer.writerow(csvrow)
+        self.writer.writerow(row)
+        return
 
     def writerows(self, rows):
         for row in rows:
@@ -575,7 +545,7 @@
     def __call__(self, function):
         def newfunc(*args, **kwargs):
             ret = function(*args, **kwargs)
-            if isinstance(ret, string_types):
+            if isinstance(ret, str):
                 return ret[:self.maxsize]
             return ret
         return newfunc
@@ -584,6 +554,6 @@
 def htmlescape(function):
     def newfunc(*args, **kwargs):
         ret = function(*args, **kwargs)
-        assert isinstance(ret, string_types)
+        assert isinstance(ret, str)
         return xml_escape(ret)
     return newfunc
--- a/cubicweb/utils.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/utils.py	Fri Oct 18 23:39:03 2019 +0200
@@ -25,24 +25,14 @@
 import random
 import re
 import json
-
-from six import PY3
-
 from operator import itemgetter
-if PY3:
-    from inspect import getfullargspec as getargspec
-else:
-    from inspect import getargspec
+from inspect import getfullargspec as getargspec
 from itertools import repeat
 from uuid import uuid4
-from warnings import warn
 from threading import Lock
 from logging import getLogger
 
-from six import text_type
-
 from logilab.mtconverter import xml_escape
-from logilab.common.deprecation import deprecated
 from logilab.common.date import ustrftime
 
 from cubicweb import Binary
@@ -108,7 +98,7 @@
     """
     def __init__(self, w, tag, closetag=None):
         self.written = False
-        self.tag = text_type(tag)
+        self.tag = tag
         self.closetag = closetag
         self.w = w
 
@@ -124,7 +114,7 @@
     def __exit__(self, exctype, value, traceback):
         if self.written is True:
             if self.closetag:
-                self.w(text_type(self.closetag))
+                self.w(self.closetag)
             else:
                 self.w(self.tag.replace('<', '</', 1))
 
@@ -206,8 +196,6 @@
     __nonzero__ = __bool__
 
     def write(self, value):
-        assert isinstance(value, text_type), u"unicode required not %s : %s"\
-                                     % (type(value).__name__, repr(value))
         if self.tracewrites:
             from traceback import format_stack
             stack = format_stack(None)[:-1]
@@ -442,25 +430,14 @@
         # keep main_stream's reference on req for easier text/html demoting
         req.main_stream = self
 
-    @deprecated('[3.17] there are no namespaces in html, xhtml is not served any longer')
-    def add_namespace(self, prefix, uri):
-        pass
-
-    @deprecated('[3.17] there are no namespaces in html, xhtml is not served any longer')
-    def set_namespaces(self, namespaces):
-        pass
-
     def add_htmlattr(self, attrname, attrvalue):
         self._htmlattrs.append( (attrname, attrvalue) )
 
     def set_htmlattrs(self, attrs):
         self._htmlattrs = attrs
 
-    def set_doctype(self, doctype, reset_xmldecl=None):
+    def set_doctype(self, doctype):
         self.doctype = doctype
-        if reset_xmldecl is not None:
-            warn('[3.17] xhtml is no more supported',
-                 DeprecationWarning, stacklevel=2)
 
     @property
     def htmltag(self):
@@ -588,6 +565,17 @@
     return 'javascript: ' + PERCENT_IN_URLQUOTE_RE.sub(r'%25', javascript_code)
 
 
+def get_pdb():
+    "return ipdb if its installed, otherwise pdb"
+    try:
+        import ipdb
+    except ImportError:
+        import pdb
+        return pdb
+    else:
+        return ipdb
+
+
 logger = getLogger('cubicweb.utils')
 
 class QueryCache(object):
@@ -632,6 +620,29 @@
         with self._lock:
             return len(self._data)
 
+    def items(self):
+        """Get an iterator over the dictionary's items: (key, value) pairs"""
+        with self._lock:
+            for k, v in self._data.items():
+                yield k, v
+
+    def get(self, k, default=None):
+        """Get the value associated to the specified key
+
+        :param k: The key to look for
+        :param default: The default value when the key is not found
+        :return: The associated value (or the default value)
+        """
+        try:
+            return self._data[k]
+        except KeyError:
+            return default
+
+    def __iter__(self):
+        with self._lock:
+            for k in iter(self._data):
+                yield k
+
     def __getitem__(self, k):
         with self._lock:
             if k in self._permanent:
@@ -689,10 +700,20 @@
                     break
                 level = v
         else:
-            # we removed cruft but everything is permanent
+            # we removed cruft
             if len(self._data) >= self._max:
-                logger.warning('Cache %s is full.' % id(self))
-                self._clear()
+                if len(self._permanent) >= self._max:
+                    # we really are full with permanents => clear
+                    logger.warning('Cache %s is full.' % id(self))
+                    self._clear()
+                else:
+                    # pathological case where _transient was probably empty ...
+                    # drop all non-permanents
+                    to_drop = set(self._data.keys()).difference(self._permanent)
+                    for k in to_drop:
+                        # should not be in _transient
+                        assert k not in self._transient
+                        self._data.pop(k, None)
 
     def _usage_report(self):
         with self._lock:
--- a/cubicweb/view.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/view.py	Fri Oct 18 23:39:03 2019 +0200
@@ -24,9 +24,6 @@
 from warnings import warn
 from functools import partial
 
-from six.moves import range
-
-from logilab.common.deprecation import deprecated
 from logilab.common.registry import yes
 from logilab.mtconverter import xml_escape
 
--- a/cubicweb/vregistry.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,23 +0,0 @@
-# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-from warnings import warn
-from logilab.common.deprecation import class_moved
-warn('[3.15] moved to logilab.common.registry', DeprecationWarning, stacklevel=2)
-from logilab.common.registry import *
-
-VRegistry = class_moved(RegistryStore, old_name='VRegistry', message='[3.15] VRegistry moved to logilab.common.registry as RegistryStore')
--- a/cubicweb/web/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,12 +19,9 @@
 publisher to get a full CubicWeb web application
 """
 
+from urllib.parse import quote as urlquote
 
 from cubicweb import _
-
-from six.moves.urllib.parse import quote as urlquote
-from logilab.common.deprecation import deprecated
-
 from cubicweb.web._exceptions import *
 from cubicweb.utils import json_dumps
 from cubicweb.uilib import eid_param
--- a/cubicweb/web/_exceptions.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/_exceptions.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,9 +18,7 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """exceptions used in the core of the CubicWeb web application"""
 
-
-
-from six.moves import http_client
+import http.client as http_client
 
 from cubicweb._exceptions import *
 from cubicweb.utils import json_dumps
@@ -28,15 +26,18 @@
 
 class DirectResponse(Exception):
     """Used to supply a twitted HTTP Response directly"""
+
     def __init__(self, response):
         self.response = response
 
+
 class InvalidSession(CubicWebException):
     """raised when a session id is found but associated session is not found or
     invalid"""
 
 # Publish related exception
 
+
 class PublishException(CubicWebException):
     """base class for publishing related exception"""
 
@@ -44,18 +45,23 @@
         self.status = kwargs.pop('status', http_client.OK)
         super(PublishException, self).__init__(*args, **kwargs)
 
+
 class LogOut(PublishException):
     """raised to ask for deauthentication of a logged in user"""
+
     def __init__(self, url=None):
         super(LogOut, self).__init__()
         self.url = url
 
+
 class Redirect(PublishException):
     """raised to redirect the http request"""
+
     def __init__(self, location, status=http_client.SEE_OTHER):
         super(Redirect, self).__init__(status=status)
         self.location = location
 
+
 class StatusResponse(PublishException):
 
     def __init__(self, status, content=''):
@@ -67,6 +73,7 @@
 
 # Publish related error
 
+
 class RequestError(PublishException):
     """raised when a request can't be served because of a bad input"""
 
@@ -82,13 +89,16 @@
         kwargs.setdefault('status', http_client.BAD_REQUEST)
         super(NothingToEdit, self).__init__(*args, **kwargs)
 
+
 class ProcessFormError(RequestError):
     """raised when posted data can't be processed by the corresponding field
     """
+
     def __init__(self, *args, **kwargs):
         kwargs.setdefault('status', http_client.BAD_REQUEST)
         super(ProcessFormError, self).__init__(*args, **kwargs)
 
+
 class NotFound(RequestError):
     """raised when something was not found. In most case,
        a 404 error should be returned"""
@@ -97,9 +107,11 @@
         kwargs.setdefault('status', http_client.NOT_FOUND)
         super(NotFound, self).__init__(*args, **kwargs)
 
+
 class RemoteCallFailed(RequestError):
     """raised when a json remote call fails
     """
+
     def __init__(self, reason='', status=http_client.INTERNAL_SERVER_ERROR):
         super(RemoteCallFailed, self).__init__(reason, status=status)
         self.reason = reason
--- a/cubicweb/web/application.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/application.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,15 +19,11 @@
 
 
 import contextlib
-from functools import wraps
+import http.client as http_client
 import json
 import sys
-from time import clock, time
+from time import process_time, time
 from contextlib import contextmanager
-from warnings import warn
-
-from six import PY2, text_type, binary_type
-from six.moves import http_client
 
 from rql import BadRQLQuery
 
@@ -38,42 +34,14 @@
 from cubicweb.repoapi import anonymous_cnx
 from cubicweb.web import cors
 from cubicweb.web import (
-    LOGGER, StatusResponse, DirectResponse, Redirect, NotFound, LogOut,
+    LOGGER, DirectResponse, Redirect, NotFound, LogOut,
     RemoteCallFailed, InvalidSession, RequestError, PublishException)
-from cubicweb.web.request import CubicWebRequestBase
 
 # make session manager available through a global variable so the debug view can
 # print information about web session
 SESSION_MANAGER = None
 
 
-def _deprecated_path_arg(func):
-    @wraps(func)
-    def wrapper(self, req, *args, **kwargs):
-        if args or 'path' in kwargs:
-            func_name = func.func_name if PY2 else func.__name__
-            warn('[3.24] path argument got removed from "%s" parameters' % func_name,
-                 DeprecationWarning)
-            path = args[0] if args else kwargs['path']
-            assert path == req.relative_path(False), \
-                'mismatching path, {0} vs {1}'.format(path, req.relative_path(False))
-        return func(self, req)
-    return wrapper
-
-
-def _deprecated_req_path_swapped(func):
-    @wraps(func)
-    def wrapper(self, req, *args, **kwargs):
-        if not isinstance(req, CubicWebRequestBase):
-            warn('[3.15] Application entry point arguments are now (req, path) '
-                 'not (path, req)', DeprecationWarning, 2)
-            path = req
-            req = args[0] if args else kwargs.pop('req')
-            args = (path, ) + args[1:]
-        return func(self, req, *args, **kwargs)
-    return wrapper
-
-
 @contextmanager
 def anonymized_request(req):
     from cubicweb.web.views.authentication import Session
@@ -227,7 +195,6 @@
 
     # publish methods #########################################################
 
-    @_deprecated_path_arg
     def log_handle_request(self, req):
         """wrapper around _publish to log all queries executed for a given
         accessed path
@@ -238,9 +205,10 @@
                 orig_execute = cnx.execute
 
                 def execute(rql, kwargs=None, build_descr=True):
-                    tstart, cstart = time(), clock()
+                    tstart, cstart = time(), process_time()
                     rset = orig_execute(rql, kwargs, build_descr=build_descr)
-                    cnx.executed_queries.append((rql, kwargs, time() - tstart, clock() - cstart))
+                    cnx.executed_queries.append((rql, kwargs, time() - tstart,
+                                                 process_time() - cstart))
                     return rset
 
                 return execute
@@ -253,14 +221,14 @@
             return set_cnx
 
         req.set_cnx = wrap_set_cnx(req.set_cnx)
-        tstart, cstart = time(), clock()
+        tstart, cstart = time(), process_time()
         try:
             return self.main_handle_request(req)
         finally:
             cnx = req.cnx
             if cnx and cnx.executed_queries:
                 with self._logfile_lock:
-                    tend, cend = time(), clock()
+                    tend, cend = time(), process_time()
                     try:
                         result = ['\n' + '*' * 80]
                         result.append('%s -- (%.3f sec, %.3f CPU sec)' % (
@@ -273,8 +241,6 @@
                     except Exception:
                         self.exception('error while logging queries')
 
-    @_deprecated_req_path_swapped
-    @_deprecated_path_arg
     def main_handle_request(self, req):
         """Process an HTTP request `req`
 
@@ -349,10 +315,9 @@
             # XXX ensure we don't actually serve content
             if not content:
                 content = self.need_login_content(req)
-        assert isinstance(content, binary_type)
+        assert isinstance(content, bytes)
         return content
 
-    @_deprecated_path_arg
     def core_handle(self, req):
         """method called by the main publisher to process <req> relative path
 
@@ -371,7 +336,7 @@
                    req.session.sessionid, list(req.form))
         # remove user callbacks on a new request (except for json controllers
         # to avoid callbacks being unregistered before they could be called)
-        tstart = clock()
+        tstart = process_time()
         commited = False
         try:
             # standard processing of the request
@@ -390,11 +355,6 @@
                 # Return directly an empty 200
                 req.status_out = 200
                 result = b''
-            except StatusResponse as ex:
-                warn('[3.16] StatusResponse is deprecated use req.status_out',
-                     DeprecationWarning, stacklevel=2)
-                result = ex.content
-                req.status_out = ex.status
             except Redirect as ex:
                 # Redirect may be raised by edit controller when everything went
                 # fine, so attempt to commit
@@ -445,7 +405,7 @@
                 except Exception:
                     pass  # ignore rollback error at this point
         self.add_undo_link_to_msg(req)
-        self.debug('query %s executed in %s sec', path, clock() - tstart)
+        self.debug('query %s executed in %s sec', path, process_time() - tstart)
         return result
 
     # Error handlers
@@ -520,7 +480,7 @@
         if req.status_out < 400:
             # don't overwrite it if it's already set
             req.status_out = status
-        json_dumper = getattr(ex, 'dumps', lambda: json.dumps({'reason': text_type(ex)}))
+        json_dumper = getattr(ex, 'dumps', lambda: json.dumps({'reason': str(ex)}))
         return json_dumper().encode('utf-8')
 
     # special case handling
--- a/cubicweb/web/box.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/box.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,19 +20,20 @@
 
 from cubicweb import _
 
-from six import add_metaclass
-
 from logilab.mtconverter import xml_escape
-from logilab.common.deprecation import class_deprecated, class_renamed
+from logilab.common.deprecation import class_deprecated
 
 from cubicweb import Unauthorized, role as get_role
 from cubicweb.schema import display_name
 from cubicweb.predicates import no_cnx, one_line_rset
 from cubicweb.view import View
-from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs
 from cubicweb.web.htmlwidgets import (BoxLink, BoxWidget, SideBoxWidget,
-                                      RawBoxItem, BoxSeparator)
+                                      RawBoxItem)
 from cubicweb.web.action import UnregisteredAction
+from cubicweb.web.component import (
+    EditRelationMixIn,
+    Separator,
+)
 
 
 def sort_by_category(actions, categories_in_order=None):
@@ -41,13 +42,13 @@
     actions_by_cat = {}
     for action in actions:
         actions_by_cat.setdefault(action.category, []).append(
-            (action.title, action) )
+            (action.title, action))
     for key, values in actions_by_cat.items():
         actions_by_cat[key] = [act for title, act in sorted(values, key=lambda x: x[0])]
     if categories_in_order:
         for cat in categories_in_order:
             if cat in actions_by_cat:
-                result.append( (cat, actions_by_cat[cat]) )
+                result.append((cat, actions_by_cat[cat]))
     for item in sorted(actions_by_cat.items()):
         result.append(item)
     return result
@@ -55,8 +56,7 @@
 
 # old box system, deprecated ###################################################
 
-@add_metaclass(class_deprecated)
-class BoxTemplate(View):
+class BoxTemplate(View, metaclass=class_deprecated):
     """base template for boxes, usually a (contextual) list of possible
     actions. Various classes attributes may be used to control the box
     rendering.
@@ -69,7 +69,9 @@
 
         box.render(self.w)
     """
-    __deprecation_warning__ = '[3.10] *BoxTemplate classes are deprecated, use *CtxComponent instead (%(cls)s)'
+    __deprecation_warning__ = (
+        '[3.10] *BoxTemplate classes are deprecated, use *CtxComponent instead (%(cls)s)'
+    )
 
     __registry__ = 'ctxcomponents'
     __select__ = ~no_cnx()
@@ -78,13 +80,13 @@
     cw_property_defs = {
         _('visible'): dict(type='Boolean', default=True,
                            help=_('display the box or not')),
-        _('order'):   dict(type='Int', default=99,
-                           help=_('display order of the box')),
+        _('order'): dict(type='Int', default=99,
+                         help=_('display order of the box')),
         # XXX 'incontext' boxes are handled by the default primary view
         _('context'): dict(type='String', default='left',
                            vocabulary=(_('left'), _('incontext'), _('right')),
                            help=_('context where this box should be displayed')),
-        }
+    }
     context = 'left'
 
     def sort_actions(self, actions):
@@ -161,8 +163,6 @@
         """classes inheriting from EntityBoxTemplate should define cell_call"""
         self.cell_call(row, col, **kwargs)
 
-from cubicweb.web.component import AjaxEditRelationCtxComponent, EditRelationMixIn
-
 
 class EditRelationBoxTemplate(EditRelationMixIn, EntityBoxTemplate):
     """base class for boxes which let add or remove entities linked
@@ -172,6 +172,7 @@
     class attributes.
     """
     rtype = None
+
     def cell_call(self, row, col, view=None, **kwargs):
         self._cw.add_js('cubicweb.ajax.js')
         entity = self.cw_rset.get_entity(row, col)
@@ -182,7 +183,7 @@
         unrelated = self.unrelated_boxitems(entity)
         box.extend(related)
         if related and unrelated:
-            box.append(BoxSeparator())
+            box.append(Separator())
         box.extend(unrelated)
         box.render(self.w)
 
@@ -190,8 +191,3 @@
         label = super(EditRelationBoxTemplate, self).box_item(
             entity, etarget, rql, label)
         return RawBoxItem(label, liclass=u'invisible')
-
-
-AjaxEditRelationBoxTemplate = class_renamed(
-    'AjaxEditRelationBoxTemplate', AjaxEditRelationCtxComponent,
-    '[3.10] AjaxEditRelationBoxTemplate has been renamed to AjaxEditRelationCtxComponent (%(cls)s)')
--- a/cubicweb/web/captcha.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/captcha.py	Fri Oct 18 23:39:03 2019 +0200
@@ -24,8 +24,6 @@
 from random import randint, choice
 from io import BytesIO
 
-from six.moves import range
-
 from PIL import Image, ImageFont, ImageDraw, ImageFilter
 
 
--- a/cubicweb/web/component.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/component.py	Fri Oct 18 23:39:03 2019 +0200
@@ -24,8 +24,6 @@
 
 from warnings import warn
 
-from six import PY3, add_metaclass, text_type
-
 from logilab.common.deprecation import class_deprecated, class_renamed, deprecated
 from logilab.mtconverter import xml_escape
 
@@ -239,12 +237,9 @@
         self.label = label
         self.attrs = attrs
 
-    def __unicode__(self):
+    def __str__(self):
         return tags.a(self.label, href=self.href, **self.attrs)
 
-    if PY3:
-        __str__ = __unicode__
-
     def render(self, w):
         w(tags.a(self.label, href=self.href, **self.attrs))
 
@@ -455,7 +450,7 @@
 
     @property
     def domid(self):
-        return domid(self.__regid__) + text_type(self.entity.eid)
+        return domid(self.__regid__) + str(self.entity.eid)
 
     def lazy_view_holder(self, w, entity, oid, registry='views'):
         """add a holder and return a URL that may be used to replace this
@@ -528,7 +523,7 @@
                                                     args['subject'],
                                                     args['object'])
         return u'[<a href="javascript: %s" class="action">%s</a>] %s' % (
-            xml_escape(text_type(jscall)), label, etarget.view('incontext'))
+            xml_escape(str(jscall)), label, etarget.view('incontext'))
 
     def related_boxitems(self, entity):
         return [self.box_item(entity, etarget, 'delete_relation', u'-')
@@ -545,7 +540,7 @@
         """returns the list of unrelated entities, using the entity's
         appropriate vocabulary function
         """
-        skip = set(text_type(e.eid) for e in entity.related(self.rtype, role(self),
+        skip = set(str(e.eid) for e in entity.related(self.rtype, role(self),
                                                           entities=True))
         skip.add(None)
         skip.add(INTERNAL_FIELD_VALUE)
@@ -663,7 +658,7 @@
                 if maydel:
                     if not js_css_added:
                         js_css_added = self.add_js_css()
-                    jscall = text_type(js.ajaxBoxRemoveLinkedEntity(
+                    jscall = str(js.ajaxBoxRemoveLinkedEntity(
                         self.__regid__, entity.eid, rentity.eid,
                         self.fname_remove,
                         self.removed_msg and _(self.removed_msg)))
@@ -678,7 +673,7 @@
         if mayadd:
             multiple = self.rdef.role_cardinality(self.role) in '*+'
             w(u'<table><tr><td>')
-            jscall = text_type(js.ajaxBoxShowSelector(
+            jscall = str(js.ajaxBoxShowSelector(
                 self.__regid__, entity.eid, self.fname_vocabulary,
                 self.fname_validate, self.added_msg and _(self.added_msg),
                 _(stdmsgs.BUTTON_OK[0]), _(stdmsgs.BUTTON_CANCEL[0]),
@@ -707,8 +702,7 @@
 
 # old contextual components, deprecated ########################################
 
-@add_metaclass(class_deprecated)
-class EntityVComponent(Component):
+class EntityVComponent(Component, metaclass=class_deprecated):
     """abstract base class for additinal components displayed in content
     headers and footer according to:
 
--- a/cubicweb/web/controller.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/controller.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,13 +17,8 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """abstract controller classe for CubicWeb web client"""
 
-
-
-from six import PY2
-
 from logilab.mtconverter import xml_escape
 from logilab.common.registry import yes
-from logilab.common.deprecation import deprecated
 
 from cubicweb.appobject import AppObject
 from cubicweb.mail import format_mail
@@ -89,8 +84,6 @@
         rql = req.form.get('rql')
         if rql:
             req.ensure_ro_rql(rql)
-            if PY2 and not isinstance(rql, unicode):
-                rql = unicode(rql, req.encoding)
             pp = req.vreg['components'].select_or_none('magicsearch', req)
             if pp is not None:
                 return pp.process_query(rql)
@@ -106,12 +99,6 @@
         if not self._edited_entity:
             self._edited_entity = entity
 
-    @deprecated('[3.18] call view.set_http_cache_headers then '
-                '.is_client_cache_valid() method and return instead')
-    def validate_cache(self, view):
-        view.set_http_cache_headers()
-        self._cw.validate_cache()
-
     def sendmail(self, recipient, subject, body):
         senderemail = self._cw.user.cw_adapt_to('IEmailable').get_email()
         msg = format_mail({'email' : senderemail,
--- a/cubicweb/web/cors.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/cors.py	Fri Oct 18 23:39:03 2019 +0200
@@ -29,7 +29,7 @@
 
 """
 
-from six.moves.urllib.parse import urlsplit
+from urllib.parse import urlsplit
 
 from cubicweb.web import LOGGER
 info = LOGGER.info
--- a/cubicweb/web/data/cubicweb.ajax.js	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/data/cubicweb.ajax.js	Fri Oct 18 23:39:03 2019 +0200
@@ -473,19 +473,8 @@
         }
         // else: rebuild full url by fetching cubicweb:rql, cubicweb:vid, etc.
         var rql = $fragment.attr('cubicweb:rql');
-        var items = $fragment.attr('cubicweb:vid').split('&');
-        var vid = items[0];
+        var vid = $fragment.attr('cubicweb:vid');
         var extraparams = {};
-        // case where vid='myvid&param1=val1&param2=val2': this is a deprecated abuse-case
-        if (items.length > 1) {
-            cw.log("[3.5] you're using extraargs in cubicweb:vid " +
-                   "attribute, this is deprecated, consider using " +
-                   "loadurl instead");
-            for (var j = 1; j < items.length; j++) {
-                var keyvalue = items[j].split('=');
-                extraparams[keyvalue[0]] = keyvalue[1];
-            }
-        }
         var actrql = $fragment.attr('cubicweb:actualrql');
         if (actrql) {
             extraparams['actualrql'] = actrql;
@@ -679,23 +668,6 @@
     jQuery('#lazy-' + divid).trigger('load_' + divid);
 }
 
-/* DEPRECATED *****************************************************************/
-
-// still used in cwo and keyword cubes at least
-reloadComponent = cw.utils.deprecatedFunction(
-    '[3.9] reloadComponent() is deprecated, use loadxhtml instead',
-    function(compid, rql, registry, nodeid, extraargs) {
-        registry = registry || 'components';
-        rql = rql || '';
-        nodeid = nodeid || (compid + 'Component');
-        extraargs = extraargs || {};
-        var node = cw.jqNode(nodeid);
-        return node.loadxhtml(AJAX_BASE_URL, ajaxFuncArgs('component', null, compid,
-                                                          rql, registry, extraargs));
-    }
-);
-
-
 function remoteExec(fname /* ... */) {
     setProgressCursor();
     var props = {
--- a/cubicweb/web/data/cubicweb.compat.js	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/data/cubicweb.compat.js	Fri Oct 18 23:39:03 2019 +0200
@@ -18,12 +18,3 @@
     }
     return result;
 }
-
-
-// skm cube still uses this
-getNodeAttribute = cw.utils.deprecatedFunction(
-    '[3.9] getNodeAttribute(node, attr) is deprecated, use $(node).attr(attr)',
-    function(node, attribute) {
-        return $(node).attr(attribute);
-    }
-);
--- a/cubicweb/web/data/cubicweb.htmlhelpers.js	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/data/cubicweb.htmlhelpers.js	Fri Oct 18 23:39:03 2019 +0200
@@ -10,17 +10,6 @@
 
 
 /**
- * .. function:: baseuri()
- *
- * returns the document's baseURI.
- */
-baseuri = cw.utils.deprecatedFunction(
-    "[3.20] baseuri() is deprecated, use BASE_URL instead",
-    function () {
-        return BASE_URL;
-    });
-
-/**
  * .. function:: setProgressCursor()
  *
  * set body's cursor to 'progress'
--- a/cubicweb/web/data/cubicweb.js	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/data/cubicweb.js	Fri Oct 18 23:39:03 2019 +0200
@@ -426,15 +426,6 @@
 // backward compat
 CubicWeb = cw;
 
-jQuery.extend(cw, {
-    require: cw.utils.deprecatedFunction(
-        '[3.9] CubicWeb.require() is not used anymore',
-        function(module) {}),
-    provide: cw.utils.deprecatedFunction(
-        '[3.9] CubicWeb.provide() is not used anymore',
-        function(module) {})
-});
-
 jQuery(document).ready(function() {
     $(cw).trigger('server-response', [false, document]);
 });
--- a/cubicweb/web/facet.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/facet.py	Fri Oct 18 23:39:03 2019 +0200
@@ -53,17 +53,13 @@
 from cubicweb import _
 
 from functools import reduce
-from warnings import warn
 from copy import deepcopy
 from datetime import datetime, timedelta
 
-from six import text_type, string_types
-
 from logilab.mtconverter import xml_escape
 from logilab.common.graph import has_path
 from logilab.common.decorators import cached, cachedproperty
 from logilab.common.date import datetime2ticks, ustrftime, ticks2datetime
-from logilab.common.deprecation import deprecated
 from logilab.common.registry import yes
 
 from rql import nodes, utils
@@ -89,12 +85,6 @@
     return req.vreg['facets'].object_by_id(facetid, req, select=select,
                                            filtered_variable=filtered_variable)
 
-@deprecated('[3.13] filter_hiddens moved to cubicweb.web.views.facets with '
-            'slightly modified prototype')
-def filter_hiddens(w, baserql, **kwargs):
-    from cubicweb.web.views.facets import filter_hiddens
-    return filter_hiddens(w, baserql, wdgs=kwargs.pop('facets'), **kwargs)
-
 
 ## rqlst manipulation functions used by facets ################################
 
@@ -164,15 +154,6 @@
     # global tree config: DISTINCT, LIMIT, OFFSET
     select.set_distinct(True)
 
-@deprecated('[3.13] use init_facets instead')
-def prepare_facets_rqlst(rqlst, args=None):
-    assert len(rqlst.children) == 1, 'FIXME: union not yet supported'
-    select = rqlst.children[0]
-    filtered_variable = get_filtered_variable(select)
-    baserql = select.as_string(args)
-    prepare_select(select, filtered_variable)
-    return filtered_variable, baserql
-
 def prepare_vocabulary_select(select, filtered_variable, rtype, role,
                               select_target_entity=True):
     """prepare a syntax tree to generate a filter vocabulary rql using the given
@@ -370,11 +351,6 @@
         return var
 
 
-_prepare_vocabulary_rqlst = deprecated('[3.13] renamed prepare_vocabulary_select')(
-    prepare_vocabulary_select)
-_cleanup_rqlst = deprecated('[3.13] renamed to cleanup_select')(cleanup_select)
-
-
 ## base facet classes ##########################################################
 
 class AbstractFacet(AppObject):
@@ -479,11 +455,6 @@
     def wdgclass(self):
         raise NotImplementedError
 
-    @property
-    @deprecated('[3.13] renamed .select')
-    def rqlst(self):
-        return self.select
-
 
 class VocabularyFacet(AbstractFacet):
     """This abstract class extend :class:`AbstractFacet` to use the
@@ -672,7 +643,7 @@
                 insert_attr_select_relation(
                     select, self.filtered_variable, self.rtype, self.role,
                     self.target_attr, select_target_entity=False)
-            values = [text_type(x) for x, in self.rqlexec(select.as_string())]
+            values = [str(x) for x, in self.rqlexec(select.as_string())]
         except Exception:
             self.exception('while computing values for %s', self)
             return []
@@ -723,7 +694,7 @@
         if self.i18nable:
             tr = self._cw._
         else:
-            tr = text_type
+            tr = str
         if self.rql_sort:
             values = [(tr(label), eid) for eid, label in rset]
         else:
@@ -743,20 +714,11 @@
 
     # internal utilities #######################################################
 
-    @cached
-    def _support_and_compat(self):
-        support = self.support_and
-        if callable(support):
-            warn('[3.13] %s.support_and is now a property' % self.__class__,
-                 DeprecationWarning)
-            support = support()
-        return support
-
     def value_restriction(self, restrvar, rel, value):
         # XXX handle rel is None case in RQLPathFacet?
         if self.restr_attr != 'eid':
             self.select.set_distinct(True)
-        if isinstance(value, string_types):
+        if isinstance(value, str):
             # only one value selected
             if value:
                 self.select.add_constant_restriction(
@@ -766,7 +728,7 @@
                 rel.parent.replace(rel, nodes.Not(rel))
         elif self.operator == 'OR':
             # set_distinct only if rtype cardinality is > 1
-            if self._support_and_compat():
+            if self.support_and:
                 self.select.set_distinct(True)
             # multiple ORed values: using IN is fine
             if '' in value:
@@ -920,7 +882,7 @@
         if self.i18nable:
             tr = self._cw._
         else:
-            tr = text_type
+            tr = str
         if self.rql_sort:
             return [(tr(value), value) for value, in rset]
         values = [(tr(value), value) for value, in rset]
@@ -1073,7 +1035,7 @@
         assert self.path and isinstance(self.path, (list, tuple)), \
             'path should be a list of 3-uples, not %s' % self.path
         for part in self.path:
-            if isinstance(part, string_types):
+            if isinstance(part, str):
                 part = part.split()
             assert len(part) == 3, \
                    'path should be a list of 3-uples, not %s' % part
@@ -1126,7 +1088,7 @@
             cleanup_select(select, self.filtered_variable)
             varmap, restrvar = self.add_path_to_select(skiplabel=True)
             select.append_selected(nodes.VariableRef(restrvar))
-            values = [text_type(x) for x, in self.rqlexec(select.as_string())]
+            values = [str(x) for x, in self.rqlexec(select.as_string())]
         except Exception:
             self.exception('while computing values for %s', self)
             return []
@@ -1149,7 +1111,7 @@
         varmap = {'X': self.filtered_variable}
         actual_filter_variable = None
         for part in self.path:
-            if isinstance(part, string_types):
+            if isinstance(part, str):
                 part = part.split()
             subject, rtype, object = part
             if skiplabel and object == self.label_variable:
@@ -1253,7 +1215,7 @@
         rset = self._range_rset()
         if rset:
             minv, maxv = rset[0]
-            return [(text_type(minv), minv), (text_type(maxv), maxv)]
+            return [(str(minv), minv), (str(maxv), maxv)]
         return []
 
     def possible_values(self):
@@ -1272,7 +1234,7 @@
 
     def formatvalue(self, value):
         """format `value` before in order to insert it in the RQL query"""
-        return text_type(value)
+        return str(value)
 
     def infvalue(self, min=False):
         if min:
@@ -1373,7 +1335,7 @@
         # *list* (see rqlexec implementation)
         if rset:
             minv, maxv = rset[0]
-            return [(text_type(minv), minv), (text_type(maxv), maxv)]
+            return [(str(minv), minv), (str(maxv), maxv)]
         return []
 
 
@@ -1392,7 +1354,7 @@
             skiplabel=True, skipattrfilter=True)
         restrel = None
         for part in self.path:
-            if isinstance(part, string_types):
+            if isinstance(part, str):
                 part = part.split()
             subject, rtype, object = part
             if object == self.filter_variable:
@@ -1516,7 +1478,7 @@
                        if not val or val & mask])
 
     def possible_values(self):
-        return [text_type(val) for label, val in self.vocabulary()]
+        return [str(val) for label, val in self.vocabulary()]
 
 
 ## html widets ################################################################
@@ -1542,7 +1504,7 @@
     def height(self):
         """ title, optional and/or dropdown, len(items) or upper limit """
         return (1.5 + # title + small magic constant
-                int(self.facet._support_and_compat() +
+                int(self.facet.support_and +
                     min(len(self.items), self.css_overflow_limit)))
 
     @property
@@ -1562,14 +1524,14 @@
             cssclass += ' hideFacetBody'
         w(u'<div class="%s" cubicweb:facetName="%s">%s</div>\n' %
           (cssclass, xml_escape(self.facet.__regid__), title))
-        if self.facet._support_and_compat():
+        if self.facet.support_and:
             self._render_and_or(w)
         cssclass = 'facetBody vocabularyFacet'
         if not self.facet.start_unfolded:
             cssclass += ' hidden'
         overflow = self.overflows
         if overflow:
-            if self.facet._support_and_compat():
+            if self.facet.support_and:
                 cssclass += ' vocabularyFacetBodyWithLogicalSelector'
             else:
                 cssclass += ' vocabularyFacetBody'
@@ -1595,7 +1557,7 @@
         if selected:
             cssclass += ' facetValueSelected'
         w(u'<div class="%s" cubicweb:value="%s">\n'
-          % (cssclass, xml_escape(text_type(value))))
+          % (cssclass, xml_escape(str(value))))
         # If it is overflowed one must add padding to compensate for the vertical
         # scrollbar; given current css values, 4 blanks work perfectly ...
         padding = u'&#160;' * self.scrollbar_padding_factor if overflow else u''
@@ -1754,7 +1716,7 @@
             imgsrc = self._cw.data_url(self.unselected_img)
             imgalt = self._cw._('not selected')
         w(u'<div class="%s" cubicweb:value="%s">\n'
-          % (cssclass, xml_escape(text_type(self.value))))
+          % (cssclass, xml_escape(str(self.value))))
         w(u'<div>')
         w(u'<img src="%s" alt="%s" cubicweb:unselimg="true" />&#160;' % (imgsrc, imgalt))
         w(u'<label class="facetTitle" cubicweb:facetName="%s">%s</label>'
--- a/cubicweb/web/form.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/form.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,13 +17,7 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """abstract form classes for CubicWeb web client"""
 
-
-from warnings import warn
-
-from six import add_metaclass
-
 from logilab.common.decorators import iclassmethod
-from logilab.common.deprecation import deprecated
 
 from cubicweb.appobject import AppObject
 from cubicweb.view import NOINDEX, NOFOLLOW
@@ -76,8 +70,7 @@
     found
     """
 
-@add_metaclass(metafieldsform)
-class Form(AppObject):
+class Form(AppObject, metaclass=metafieldsform):
     __registry__ = 'forms'
 
     parent_form = None
@@ -274,10 +267,7 @@
             try:
                 return self.form_valerror.errors.pop(field.role_name())
             except KeyError:
-                if field.role and field.name in self.form_valerror:
-                    warn('%s: errors key of attribute/relation should be suffixed by "-<role>"'
-                         % self.form_valerror.__class__, DeprecationWarning)
-                    return self.form_valerror.errors.pop(field.name)
+                pass
         return None
 
     def remaining_errors(self):
--- a/cubicweb/web/formfields.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/formfields.py	Fri Oct 18 23:39:03 2019 +0200
@@ -68,8 +68,6 @@
 
 import pytz
 
-from six import PY2, text_type, string_types
-
 from logilab.mtconverter import xml_escape
 from logilab.common import nullobject
 from logilab.common.date import ustrftime
@@ -236,15 +234,9 @@
             l.append('@%#x' % id(self))
         return u'%s>' % ' '.join(l)
 
-    def __unicode__(self):
+    def __str__(self):
         return self.as_string(False)
 
-    if PY2:
-        def __str__(self):
-            return self.as_string(False).encode('UTF8')
-    else:
-        __str__ = __unicode__
-
     def __repr__(self):
         return self.as_string(True)
 
@@ -290,7 +282,7 @@
             return u''
         if value is True:
             return u'1'
-        return text_type(value)
+        return str(value)
 
     def get_widget(self, form):
         """return the widget instance associated to this field"""
@@ -825,7 +817,7 @@
 
     def _process_form_value(self, form):
         value = form._cw.form.get(self.input_name(form))
-        if isinstance(value, text_type):
+        if isinstance(value, str):
             # file modified using a text widget
             return Binary(value.encode(self.encoding(form)))
         return super(EditableFileField, self)._process_form_value(form)
@@ -852,7 +844,7 @@
             self.widget.attrs.setdefault('size', self.default_text_input_size)
 
     def _ensure_correctly_typed(self, form, value):
-        if isinstance(value, string_types):
+        if isinstance(value, str):
             value = value.strip()
             if not value:
                 return None
@@ -934,7 +926,7 @@
         return self.format_single_value(req, 1.234)
 
     def _ensure_correctly_typed(self, form, value):
-        if isinstance(value, string_types):
+        if isinstance(value, str):
             value = value.strip()
             if not value:
                 return None
@@ -955,8 +947,7 @@
 
     def format_single_value(self, req, value):
         if value:
-            value = format_time(value.days * 24 * 3600 + value.seconds)
-            return text_type(value)
+            return format_time(value.days * 24 * 3600 + value.seconds)
         return u''
 
     def example_format(self, req):
@@ -966,7 +957,7 @@
         return u'20s, 10min, 24h, 4d'
 
     def _ensure_correctly_typed(self, form, value):
-        if isinstance(value, string_types):
+        if isinstance(value, str):
             value = value.strip()
             if not value:
                 return None
@@ -996,14 +987,14 @@
         return self.format_single_value(req, datetime.now())
 
     def _ensure_correctly_typed(self, form, value):
-        if isinstance(value, string_types):
+        if isinstance(value, str):
             value = value.strip()
             if not value:
                 return None
             try:
                 value = form._cw.parse_datetime(value, self.etype)
             except ValueError as ex:
-                raise ProcessFormError(text_type(ex))
+                raise ProcessFormError(str(ex))
         return value
 
 
@@ -1107,7 +1098,7 @@
         linkedto = form.linked_to.get((self.name, self.role))
         if linkedto:
             buildent = form._cw.entity_from_eid
-            return [(buildent(eid).view('combobox'), text_type(eid))
+            return [(buildent(eid).view('combobox'), str(eid))
                     for eid in linkedto]
         return []
 
@@ -1119,7 +1110,7 @@
         # vocabulary doesn't include current values, add them
         if form.edited_entity.has_eid():
             rset = form.edited_entity.related(self.name, self.role)
-            vocab += [(e.view('combobox'), text_type(e.eid))
+            vocab += [(e.view('combobox'), str(e.eid))
                       for e in rset.entities()]
         return vocab
 
@@ -1153,11 +1144,11 @@
             if entity.eid in done:
                 continue
             done.add(entity.eid)
-            res.append((entity.view('combobox'), text_type(entity.eid)))
+            res.append((entity.view('combobox'), str(entity.eid)))
         return res
 
     def format_single_value(self, req, value):
-        return text_type(value)
+        return str(value)
 
     def process_form_value(self, form):
         """process posted form and return correctly typed value"""
--- a/cubicweb/web/formwidgets.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/formwidgets.py	Fri Oct 18 23:39:03 2019 +0200
@@ -98,8 +98,6 @@
 from functools import reduce
 from datetime import date
 
-from six import text_type, string_types
-
 from logilab.mtconverter import xml_escape
 from logilab.common.date import todatetime
 
@@ -272,7 +270,7 @@
         """
         posted = form._cw.form
         val = posted.get(field.input_name(form, self.suffix))
-        if isinstance(val, string_types):
+        if isinstance(val, str):
             val = val.strip()
         return val
 
@@ -463,7 +461,7 @@
             options.append(u'</optgroup>')
         if 'size' not in attrs:
             if self._multiple:
-                size = text_type(min(self.default_size, len(vocab) or 1))
+                size = str(min(self.default_size, len(vocab) or 1))
             else:
                 size = u'1'
             attrs['size'] = size
@@ -727,7 +725,7 @@
         else:
             value = self.value
         attrs = self.attributes(form, field)
-        attrs.setdefault('size', text_type(self.default_size))
+        attrs.setdefault('size', str(self.default_size))
         return tags.input(name=field.input_name(form, self.suffix),
                           value=value, type='text', **attrs)
 
@@ -757,10 +755,11 @@
     :class:`JQueryTimePicker` widgets to define a date and time picker. Will
     return the date and time as python datetime instance.
     """
-    def __init__(self, initialtime=None, timesteps=15, **kwargs):
+    def __init__(self, initialtime=None, timesteps=15, separator=u':', **kwargs):
         super(JQueryDateTimePicker, self).__init__(**kwargs)
         self.initialtime = initialtime
         self.timesteps = timesteps
+        self.separator = separator
 
     def _render(self, form, field, renderer):
         """render the widget for the given `field` of `form`.
@@ -786,7 +785,7 @@
                 timestr = req.format_time(self.initialtime)
         datepicker = JQueryDatePicker(datestr=datestr, suffix='date')
         timepicker = JQueryTimePicker(timestr=timestr, timesteps=self.timesteps,
-                                      suffix='time')
+                                      separator=self.separator, suffix='time')
         return u'<div id="%s">%s%s</div>' % (field.dom_id(form),
                                              datepicker.render(form, field, renderer),
                                              timepicker.render(form, field, renderer))
@@ -801,7 +800,7 @@
         try:
             date = todatetime(req.parse_datetime(datestr, 'Date'))
         except ValueError as exc:
-            raise ProcessFormError(text_type(exc))
+            raise ProcessFormError(str(exc))
         timestr = req.form.get(field.input_name(form, 'time'))
         if timestr:
             timestr = timestr.strip()
@@ -810,7 +809,7 @@
         try:
             time = req.parse_datetime(timestr, 'Time')
         except ValueError as exc:
-            raise ProcessFormError(text_type(exc))
+            raise ProcessFormError(str(exc))
         return date.replace(hour=time.hour, minute=time.minute, second=time.second)
 
 
@@ -1014,12 +1013,12 @@
         req = form._cw
         values = {}
         path = req.form.get(field.input_name(form, 'path'))
-        if isinstance(path, string_types):
+        if isinstance(path, str):
             path = path.strip()
         if path is None:
             path = u''
         fqs = req.form.get(field.input_name(form, 'fqs'))
-        if isinstance(fqs, string_types):
+        if isinstance(fqs, str):
             fqs = fqs.strip() or None
             if fqs:
                 for i, line in enumerate(fqs.split('\n')):
--- a/cubicweb/web/htmlwidgets.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/htmlwidgets.py	Fri Oct 18 23:39:03 2019 +0200
@@ -24,9 +24,6 @@
 import random
 from math import floor
 
-from six import add_metaclass
-from six.moves import range
-
 from logilab.mtconverter import xml_escape
 from logilab.common.deprecation import class_deprecated
 
@@ -118,8 +115,7 @@
         self.w(u'</div>')
 
 
-@add_metaclass(class_deprecated)
-class SideBoxWidget(BoxWidget):
+class SideBoxWidget(BoxWidget, metaclass=class_deprecated):
     """default CubicWeb's sidebox widget"""
     __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
 
@@ -210,8 +206,7 @@
         self.w(u'</ul></div></div>')
 
 
-@add_metaclass(class_deprecated)
-class BoxField(HTMLWidget):
+class BoxField(HTMLWidget, metaclass=class_deprecated):
     """couples label / value meant to be displayed in a box"""
     __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
     def __init__(self, label, value):
@@ -224,8 +219,7 @@
                % (self.label, self.value))
 
 
-@add_metaclass(class_deprecated)
-class BoxSeparator(HTMLWidget):
+class BoxSeparator(HTMLWidget, metaclass=class_deprecated):
     """a menu separator"""
     __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
 
@@ -233,8 +227,7 @@
         self.w(u'</ul><hr class="boxSeparator"/><ul>')
 
 
-@add_metaclass(class_deprecated)
-class BoxLink(HTMLWidget):
+class BoxLink(HTMLWidget, metaclass=class_deprecated):
     """a link in a box"""
     __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
     def __init__(self, href, label, _class='', title='', ident='', escape=False):
@@ -256,8 +249,7 @@
             self.w(u'<li class="%s">%s</li>\n' % (self._class, link))
 
 
-@add_metaclass(class_deprecated)
-class BoxHtml(HTMLWidget):
+class BoxHtml(HTMLWidget, metaclass=class_deprecated):
     """a form in a box"""
     __deprecation_warning__ = '[3.10] class %(cls)s is deprecated'
     def __init__(self, rawhtml):
--- a/cubicweb/web/http_headers.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/http_headers.py	Fri Oct 18 23:39:03 2019 +0200
@@ -6,9 +6,7 @@
 from calendar import timegm
 import base64
 import re
-
-from six import string_types
-from six.moves.urllib.parse import urlparse
+from urllib.parse import urlparse
 
 
 def dashCapitalize(s):
@@ -383,7 +381,7 @@
 
 def unique(seq):
     '''if seq is not a string, check it's a sequence of one element and return it'''
-    if isinstance(seq, string_types):
+    if isinstance(seq, str):
         return seq
     if len(seq) != 1:
         raise ValueError('single value required, not %s' % seq)
@@ -455,10 +453,10 @@
 
     """
     if (value in (True, 1) or
-            isinstance(value, string_types) and value.lower() == 'true'):
+            isinstance(value, str) and value.lower() == 'true'):
         return 'true'
     if (value in (False, 0) or
-            isinstance(value, string_types) and value.lower() == 'false'):
+            isinstance(value, str) and value.lower() == 'false'):
         return 'false'
     raise ValueError("Invalid true/false header value: %s" % value)
 
--- a/cubicweb/web/httpcache.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/httpcache.py	Fri Oct 18 23:39:03 2019 +0200
@@ -121,7 +121,7 @@
     """return the date/time where this view should be considered as
     modified. Take care of possible related objects modifications.
 
-    /!\ must return GMT time /!\
+    /!\\ must return GMT time /!\\
     """
     # XXX check view module's file modification time in dev mod ?
     ctime = datetime.utcnow()
--- a/cubicweb/web/propertysheet.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/propertysheet.py	Fri Oct 18 23:39:03 2019 +0200
@@ -47,7 +47,7 @@
         self.reset()
         context['sheet'] = self
         context['lazystr'] = self.lazystr
-        self._percent_rgx = re.compile('%(?!\()')
+        self._percent_rgx = re.compile(r'%(?!\()')
 
     def lazystr(self, str):
         return lazystr(str, self)
--- a/cubicweb/web/request.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/request.py	Fri Oct 18 23:39:03 2019 +0200
@@ -23,18 +23,14 @@
 from hashlib import sha1  # pylint: disable=E0611
 from calendar import timegm
 from datetime import date, datetime
-from warnings import warn
+import http.client
 from io import BytesIO
-
-from six import PY2, text_type, string_types
-from six.moves import http_client
-from six.moves.urllib.parse import urlsplit, quote as urlquote
-from six.moves.http_cookies import SimpleCookie
+from urllib.parse import urlsplit, quote as urlquote
+from http.cookies import SimpleCookie
 
 from rql.utils import rqlvar_maker
 
 from logilab.common.decorators import cached
-from logilab.common.deprecation import deprecated
 
 from cubicweb import AuthenticationError
 from cubicweb.req import RequestSessionBase
@@ -162,16 +158,6 @@
             self.html_headers.define_var('pageid', pid, override=False)
         self.pageid = pid
 
-    def _get_json_request(self):
-        warn('[3.15] self._cw.json_request is deprecated, use self._cw.ajax_request instead',
-             DeprecationWarning, stacklevel=2)
-        return self.ajax_request
-    def _set_json_request(self, value):
-        warn('[3.15] self._cw.json_request is deprecated, use self._cw.ajax_request instead',
-             DeprecationWarning, stacklevel=2)
-        self.ajax_request = value
-    json_request = property(_get_json_request, _set_json_request)
-
     @property
     def authmode(self):
         """Authentification mode of the instance
@@ -220,12 +206,8 @@
         encoding = self.encoding
         for param, val in params.items():
             if isinstance(val, (tuple, list)):
-                if PY2:
-                    val = [unicode(x, encoding) for x in val]
                 if len(val) == 1:
                     val = val[0]
-            elif PY2 and isinstance(val, str):
-                val = unicode(val, encoding)
             if param in self.no_script_form_params and val:
                 val = self.no_script_form_param(param, val)
             if param == '_cwmsgid':
@@ -286,7 +268,7 @@
                 return None
 
     def set_message(self, msg):
-        assert isinstance(msg, text_type)
+        assert isinstance(msg, str)
         self.reset_message()
         self._msg = msg
 
@@ -299,7 +281,7 @@
 
     def set_redirect_message(self, msg):
         # TODO - this should probably be merged with append_to_redirect_message
-        assert isinstance(msg, text_type)
+        assert isinstance(msg, str)
         msgid = self.redirect_message_id()
         self.session.data[msgid] = msg
         return msgid
@@ -386,7 +368,7 @@
             eids = form['eid']
         except KeyError:
             raise NothingToEdit(self._('no selected entities'))
-        if isinstance(eids, string_types):
+        if isinstance(eids, str):
             eids = (eids,)
         for peid in eids:
             if withtype:
@@ -472,12 +454,6 @@
         Give maxage = None to have a "session" cookie expiring when the
         client close its browser
         """
-        if isinstance(name, SimpleCookie):
-            warn('[3.13] set_cookie now takes name and value as two first '
-                 'argument, not anymore cookie object and name',
-                 DeprecationWarning, stacklevel=2)
-            secure = name[value]['secure']
-            name, value = value, name[value].value
         if maxage: # don't check is None, 0 may be specified
             assert expires is None, 'both max age and expires cant be specified'
             expires = maxage + time.time()
@@ -494,12 +470,8 @@
                         expires=expires, secure=secure, httponly=httponly)
         self.headers_out.addHeader('Set-cookie', cookie)
 
-    def remove_cookie(self, name, bwcompat=None):
+    def remove_cookie(self, name):
         """remove a cookie by expiring it"""
-        if bwcompat is not None:
-            warn('[3.13] remove_cookie now take only a name as argument',
-                 DeprecationWarning, stacklevel=2)
-            name = bwcompat
         self.set_cookie(name, '', maxage=0, expires=date(2000, 1, 1))
 
     def set_content_type(self, content_type, filename=None, encoding=None,
@@ -545,7 +517,7 @@
         :param localfile: if True, the default data dir prefix is added to the
                           JS filename
         """
-        if isinstance(jsfiles, string_types):
+        if isinstance(jsfiles, str):
             jsfiles = (jsfiles,)
         for jsfile in jsfiles:
             if localfile:
@@ -565,7 +537,7 @@
                        the css inclusion. cf:
                        http://msdn.microsoft.com/en-us/library/ms537512(VS.85).aspx
         """
-        if isinstance(cssfiles, string_types):
+        if isinstance(cssfiles, str):
             cssfiles = (cssfiles,)
         if ieonly:
             if self.ie_browser():
@@ -635,7 +607,7 @@
         lang_prefix = ''
         if self.lang is not None and self.vreg.config.get('language-mode') == 'url-prefix':
             lang_prefix = '%s/' % self.lang
-        return lang_prefix + path
+        return lang_prefix + str(path)
 
     def url(self, includeparams=True):
         """return currently accessed url"""
@@ -688,23 +660,14 @@
             # overwrite headers_out to forge a brand new not-modified response
             self.headers_out = self._forge_cached_headers()
             if self.http_method() in ('HEAD', 'GET'):
-                self.status_out = http_client.NOT_MODIFIED
+                self.status_out = http.client.NOT_MODIFIED
             else:
-                self.status_out = http_client.PRECONDITION_FAILED
+                self.status_out = http.client.PRECONDITION_FAILED
             # XXX replace by True once validate_cache bw compat method is dropped
             return self.status_out
         # XXX replace by False once validate_cache bw compat method is dropped
         return None
 
-    @deprecated('[3.18] use .is_client_cache_valid() method instead')
-    def validate_cache(self):
-        """raise a `StatusResponse` exception if a cached page along the way
-        exists and is still usable.
-        """
-        status_code = self.is_client_cache_valid()
-        if status_code is not None:
-            raise StatusResponse(status_code)
-
     # abstract methods to override according to the web front-end #############
 
     def http_method(self):
@@ -812,26 +775,13 @@
         values = _parse_accept_header(accepteds, value_parser, value_sort_key)
         return (raw_value for (raw_value, parsed_value, score) in values)
 
-    @deprecated('[3.17] demote_to_html is deprecated as we always serve html')
-    def demote_to_html(self):
-        """helper method to dynamically set request content type to text/html
-
-        The global doctype and xmldec must also be changed otherwise the browser
-        will display '<[' at the beginning of the page
-        """
-        pass
-
-
     # xml doctype #############################################################
 
-    def set_doctype(self, doctype, reset_xmldecl=None):
+    def set_doctype(self, doctype):
         """helper method to dynamically change page doctype
 
         :param doctype: the new doctype, e.g. '<!DOCTYPE html>'
         """
-        if reset_xmldecl is not None:
-            warn('[3.17] reset_xmldecl is deprecated as we only serve html',
-                 DeprecationWarning, stacklevel=2)
         self.main_stream.set_doctype(doctype)
 
     # page data management ####################################################
@@ -878,16 +828,6 @@
         useragent = self.useragent()
         return useragent and 'MSIE' in useragent
 
-    @deprecated('[3.17] xhtml_browser is deprecated (xhtml is no longer served)')
-    def xhtml_browser(self):
-        """return True if the browser is considered as xhtml compatible.
-
-        If the instance is configured to always return text/html and not
-        application/xhtml+xml, this method will always return False, even though
-        this is semantically different
-        """
-        return False
-
     def html_content_type(self):
         return 'text/html'
 
--- a/cubicweb/web/schemaviewer.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/schemaviewer.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,13 +20,11 @@
 
 from cubicweb import _
 
-from six import string_types
-
 from logilab.common.ureports import Section, Title, Table, Link, Span, Text
 
 from yams.schema2dot import CARD_MAP
 from yams.schema import RelationDefinitionSchema
-from operator import attrgetter
+from operator import attrgetter, itemgetter
 
 TYPE_GETTER = attrgetter('type')
 
@@ -44,7 +42,7 @@
             self._ = req._
         else:
             encoding = 'ascii'
-            self._ = unicode
+            self._ = str
         self.encoding = encoding
 
     # no self.req managements
@@ -91,7 +89,7 @@
             relations = [rschema for rschema in sorted(schema.relations(), key=TYPE_GETTER)
                          if not (rschema.final or rschema.type in skiptypes)]
             keys = [(rschema.type, rschema) for rschema in relations]
-            for key, rschema in sorted(keys, cmp=(lambda x, y: cmp(x[1], y[1]))):
+            for key, rschema in sorted(keys, key=itemgetter(1)):
                 relstr = self.visit_relationschema(rschema)
                 rsection.append(relstr)
         return layout
@@ -99,7 +97,8 @@
     def _entity_attributes_data(self, eschema):
         _ = self._
         data = [_('attribute'), _('type'), _('default'), _('constraints')]
-        attributes = sorted(eschema.attribute_definitions(), cmp=(lambda x, y: cmp(x[0].type, y[0].type)))
+        attributes = sorted(eschema.attribute_definitions(),
+                            key=lambda el: el[0].type)
         for rschema, aschema in attributes:
             rdef = eschema.rdef(rschema)
             if not self.may_read(rdef):
@@ -122,7 +121,6 @@
             data.append(', '.join(str(constr) for constr in constraints))
         return data
 
-
     def stereotype(self, name):
         return Span((' <<%s>>' % name,), klass='stereotype')
 
@@ -130,7 +128,7 @@
         """get a layout for an entity schema"""
         etype = eschema.type
         layout = Section(children=' ', klass='clear')
-        layout.append(Link(etype,'&#160;' , id=etype)) # anchor
+        layout.append(Link(etype, '&#160;', id=etype))  # anchor
         title = self.format_eschema(eschema)
         boxchild = [Section(children=(title,), klass='title')]
         data = []
@@ -142,8 +140,8 @@
         first = True
 
         rel_defs = sorted(eschema.relation_definitions(),
-                          cmp=(lambda x, y: cmp((x[0].type, x[0].cardinality),
-                          (y[0].type, y[0].cardinality))))
+                          key=lambda el: el[0].type)
+
         for rschema, targetschemas, role in rel_defs:
             if rschema.type in skiptypes:
                 continue
@@ -197,7 +195,7 @@
         if rschema_objects:
             # might be empty
             properties = [p for p in RelationDefinitionSchema.rproperty_defs(rschema_objects[0])
-                          if not p in ('cardinality', 'composite', 'eid')]
+                          if p not in ('cardinality', 'composite', 'eid')]
         else:
             properties = []
         data += [_(prop) for prop in properties]
@@ -222,13 +220,13 @@
                     elif isinstance(val, dict):
                         for key, value in val.items():
                             if isinstance(value, (list, tuple)):
-                                val[key] = ', '.join(sorted( str(v) for v in value))
+                                val[key] = ', '.join(sorted(str(v) for v in value))
                         val = str(val)
 
                     elif isinstance(val, (list, tuple)):
                         val = sorted(val)
                         val = ', '.join(str(v) for v in val)
-                    elif val and isinstance(val, string_types):
+                    elif val and isinstance(val, str):
                         val = _(val)
                     else:
                         val = str(val)
@@ -240,6 +238,6 @@
 
     def to_string(self, value):
         """used to converte arbitrary values to encoded string"""
-        if isinstance(value, unicode):
+        if isinstance(value, str):
             return value.encode(self.encoding, 'replace')
         return str(value)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/test/data/cubicweb_blog/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/test/data/cubicweb_blog/entities.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,7 @@
+from cubicweb.entities import AnyEntity, fetch_config
+
+
+class BlogEntry(AnyEntity):
+    __regid__ = 'BlogEntry'
+    fetch_attrs, cw_fetch_order = fetch_config(
+        ['creation_date', 'title'], order='DESC')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/test/data/cubicweb_blog/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,22 @@
+from yams.buildobjs import EntityType, String, RichString, SubjectRelation
+from cubicweb.schema import WorkflowableEntityType, ERQLExpression
+
+
+class Blog(EntityType):
+    title = String(maxsize=50, required=True)
+    description = RichString()
+    rss_url = String(maxsize=128, description=(
+        'blog\'s rss url (useful for when using external site such as feedburner)'))
+
+
+class BlogEntry(WorkflowableEntityType):
+    __permissions__ = {
+        'read': ('managers', 'users', ERQLExpression('X in_state S, S name "published"'),),
+        'add': ('managers', 'users'),
+        'update': ('managers', 'owners'),
+        'delete': ('managers', 'owners')
+    }
+    title = String(required=True, fulltextindexed=True, maxsize=256)
+    content = RichString(required=True, fulltextindexed=True)
+    entry_of = SubjectRelation('Blog')
+    same_as = SubjectRelation('ExternalUri')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/test/data/cubicweb_file/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/test/data/cubicweb_file/entities.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,50 @@
+from logilab.mtconverter import guess_mimetype_and_encoding
+from cubicweb.entities import AnyEntity, fetch_config
+
+
+class File(AnyEntity):
+    """customized class for File entities"""
+    __regid__ = 'File'
+    fetch_attrs, cw_fetch_order = fetch_config(['data_name', 'title'])
+
+    def set_format_and_encoding(self):
+        """try to set format and encoding according to known values (filename,
+        file content, format, encoding).
+
+        This method must be called in a before_[add|update]_entity hook else it
+        won't have any effect.
+        """
+        assert 'data' in self.cw_edited, "missing mandatory attribute data"
+        if self.cw_edited.get('data'):
+            if (hasattr(self.data, 'filename')
+                    and not self.cw_edited.get('data_name')):
+                self.cw_edited['data_name'] = self.data.filename
+        else:
+            self.cw_edited['data_format'] = None
+            self.cw_edited['data_encoding'] = None
+            self.cw_edited['data_name'] = None
+            return
+        if 'data_format' in self.cw_edited:
+            format = self.cw_edited.get('data_format')
+        else:
+            format = None
+        if 'data_encoding' in self.cw_edited:
+            encoding = self.cw_edited.get('data_encoding')
+        else:
+            encoding = None
+        if not (format and encoding):
+            format, encoding = guess_mimetype_and_encoding(
+                data=self.cw_edited.get('data'),
+                # use get and not get_value since data has changed, we only
+                # want to consider explicitly specified values, not old ones
+                filename=self.cw_edited.get('data_name'),
+                format=format, encoding=encoding,
+                fallbackencoding=self._cw.encoding)
+            if format:
+                self.cw_edited['data_format'] = str(format)
+            if encoding:
+                self.cw_edited['data_encoding'] = str(encoding)
+
+
+class UnResizeable(Exception):
+    pass
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/test/data/cubicweb_file/hooks.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,69 @@
+import os
+
+from cubicweb.server import hook
+from cubicweb.predicates import is_instance
+from cubicweb.entities import adapters
+
+from cubicweb_file.entities import UnResizeable
+
+
+class UpdateFileHook(hook.Hook):
+    """a file has been updated, check data_format/data_encoding consistency
+    """
+    __regid__ = 'updatefilehook'
+    __select__ = hook.Hook.__select__ & is_instance('File')
+    events = ('before_add_entity', 'before_update_entity',)
+    order = -1  # should be run before other hooks
+    category = 'hash'
+
+    def __call__(self):
+        edited = self.entity.cw_edited
+        if 'data' in edited:
+            self.entity.set_format_and_encoding()
+            maxsize = None
+            if maxsize and self.entity.data_format.startswith('image/'):
+                iimage = self.entity.cw_adapt_to('IImage')
+                try:
+                    edited['data'] = iimage.resize(maxsize)
+                except UnResizeable:
+                    # if the resize fails for some reason, do nothing
+                    # (original image will be stored)
+                    pass
+
+            # thumbnail cache invalidation
+            if 'update' in self.event and 'data' in edited:
+                thumb = self.entity.cw_adapt_to('IThumbnail')
+                if not thumb:
+                    return
+                thumbpath = thumb.thumbnail_path()
+                if thumbpath:
+                    try:
+                        os.unlink(thumbpath)
+                    except Exception as exc:
+                        self.warning(
+                            'could not invalidate thumbnail file `%s` '
+                            '(cause: %s)',
+                            thumbpath, exc)
+
+
+class FileIDownloadableAdapter(adapters.IDownloadableAdapter):
+    __select__ = is_instance('File')
+
+    # IDownloadable
+    def download_url(self, **kwargs):
+        # include filename in download url for nicer url
+        name = self._cw.url_quote(self.download_file_name())
+        path = '%s/raw/%s' % (self.entity.rest_path(), name)
+        return self._cw.build_url(path, **kwargs)
+
+    def download_content_type(self):
+        return self.entity.data_format
+
+    def download_encoding(self):
+        return self.entity.data_encoding
+
+    def download_file_name(self):
+        return self.entity.data_name
+
+    def download_data(self):
+        return self.entity.data.getvalue()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/test/data/cubicweb_file/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,27 @@
+from yams.buildobjs import EntityType, String, Bytes, RichString
+
+
+class File(EntityType):
+    """a downloadable file which may contains binary data"""
+    title = String(fulltextindexed=True, maxsize=256)
+    data = Bytes(required=True, description='file to upload')
+    data_format = String(
+        required=True, maxsize=128,
+        description=('MIME type of the file. Should be dynamically set '
+                     'at upload time.'))
+    data_encoding = String(
+        maxsize=32,
+        description=('encoding of the file when it applies (e.g. text). '
+                     'Should be dynamically set at upload time.'))
+    data_name = String(
+        required=True, fulltextindexed=True,
+        description=('name of the file. Should be dynamically set '
+                     'at upload time.'))
+    data_hash = String(
+        maxsize=256,  # max len of currently available hash alg + prefix is 140
+        description=('hash of the file. May be set at upload time.'),
+        __permissions__={'read': ('managers', 'users', 'guests'),
+                         'add': (),
+                         'update': ()})
+    description = RichString(fulltextindexed=True, internationalizable=True,
+                             default_format='text/rest')
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/test/data/cubicweb_file/uiprops.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,1 @@
+FILE_ICON = data('file.png')  # noqa: F821
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/test/data/cubicweb_file/views.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,9 @@
+from cubicweb.web import formwidgets as wdgs
+from cubicweb.web.views import uicfg
+
+# fields required in the schema but automatically set by hooks. Tell about that
+# to the ui
+_pvdc = uicfg.autoform_field_kwargs
+_pvdc.tag_attribute(('File', 'data_name'), {
+    'required': False, 'widget': wdgs.TextInput({'size': 45})})
+_pvdc.tag_attribute(('File', 'data_format'), {'required': False})
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/test/data/cubicweb_file/wdoc/toc.xml	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,6 @@
+<toc>
+  <section resource="add_file" appendto="add_content">
+    <title xml:lang="en">Add an attachement</title>
+    <title xml:lang="fr">Ajouter un fichier</title>
+  </section>
+</toc>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/test/data/cubicweb_tag/__pkginfo__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+numversion = (1, 2, 3)
+version = "1.2.3"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/test/data/cubicweb_tag/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,17 @@
+from yams.buildobjs import EntityType, String, SubjectRelation, RelationType
+
+
+class Tag(EntityType):
+    """tags are used by users to mark entities.
+    When you include the Tag entity, all application specific entities
+    may then be tagged using the "tags" relation.
+    """
+    name = String(required=True, fulltextindexed=True, unique=True,
+                  maxsize=128)
+    # when using this component, add the Tag tag X relation for each type that
+    # should be taggeable
+    tags = SubjectRelation('Tag', description="tagged objects")
+
+
+class tags(RelationType):
+    """indicates that an entity is classified by a given tag"""
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cubicweb/web/test/data/cubicweb_tag/views.py	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,28 @@
+from cubicweb.web import component
+from cubicweb.web.views import ajaxcontroller
+
+
+@ajaxcontroller.ajaxfunc
+def tag_entity(self, eid, taglist):
+    execute = self._cw.execute
+    # get list of tag for this entity
+    tagged_by = set(tagname for (tagname,) in
+                    execute('Any N WHERE T name N, T tags X, X eid %(x)s',
+                            {'x': eid}))
+    for tagname in taglist:
+        tagname = tagname.strip()
+        if not tagname or tagname in tagged_by:
+            continue
+        tagrset = execute('Tag T WHERE T name %(name)s', {'name': tagname})
+        if tagrset:
+            rql = 'SET T tags X WHERE T eid %(t)s, X eid %(x)s'
+            execute(rql, {'t': tagrset[0][0], 'x': eid})
+        else:
+            rql = 'INSERT Tag T: T name %(name)s, T tags X WHERE X eid %(x)s'
+            execute(rql, {'name': tagname, 'x': eid})
+
+
+class TagsBox(component.AjaxEditRelationCtxComponent):
+    __regid__ = 'tags_box'
+    rtype = 'tags'
+    role = 'object'
--- a/cubicweb/web/test/unittest_application.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/test/unittest_application.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,10 +18,8 @@
 """unit tests for cubicweb.web.application"""
 
 import base64
-
-from six import text_type
-from six.moves import http_client
-from six.moves.http_cookies import SimpleCookie
+import http.client
+from http.cookies import SimpleCookie
 
 from logilab.common.testlib import TestCase, unittest_main
 from logilab.common.decorators import clear_cache
@@ -167,7 +165,7 @@
     def test_publish_validation_error(self):
         with self.admin_access.web_request() as req:
             user = req.user
-            eid = text_type(user.eid)
+            eid = str(user.eid)
             req.form = {
                 'eid': eid,
                 '__type:' + eid: 'CWUser',
@@ -248,8 +246,8 @@
         self.config.global_set_option('language-mode', 'http-negotiation')
         orig_translations = self.config.translations.copy()
         self.config.translations = {
-            'fr': (text_type, lambda x, y: text_type(y)),
-            'en': (text_type, lambda x, y: text_type(y))}
+            'fr': (str, lambda x, y: str(y)),
+            'en': (str, lambda x, y: str(y))}
         try:
             headers = {'Accept-Language': 'fr'}
             with self.admin_access.web_request(headers=headers) as req:
@@ -336,8 +334,8 @@
         parent_eid = parent_eid or '__cubicweb_internal_field__'
         with self.admin_access.web_request() as req:
             req.form = {
-                'eid': text_type(dir_eid),
-                '__maineid': text_type(dir_eid),
+                'eid': str(dir_eid),
+                '__maineid': str(dir_eid),
                 '__type:%s' % dir_eid: etype,
                 'parent-%s:%s' % (role, dir_eid): parent_eid,
             }
@@ -353,8 +351,8 @@
         version_eid = version_eid or '__cubicweb_internal_field__'
         with self.admin_access.web_request() as req:
             req.form = {
-                'eid': text_type(ticket_eid),
-                '__maineid': text_type(ticket_eid),
+                'eid': str(ticket_eid),
+                '__maineid': str(ticket_eid),
                 '__type:%s' % ticket_eid: 'Ticket',
                 'in_version-subject:%s' % ticket_eid: version_eid,
             }
@@ -395,8 +393,8 @@
 
         with self.admin_access.web_request() as req:
             req.form = {
-                'eid': (text_type(topd.eid), u'B'),
-                '__maineid': text_type(topd.eid),
+                'eid': (str(topd.eid), u'B'),
+                '__maineid': str(topd.eid),
                 '__type:%s' % topd.eid: 'Directory',
                 '__type:B': 'Directory',
                 'parent-object:%s' % topd.eid: u'B',
@@ -569,8 +567,8 @@
             cnx.commit()
 
         with self.admin_access.web_request() as req:
-            dir_eid = text_type(mydir.eid)
-            perm_eid = text_type(perm.eid)
+            dir_eid = str(mydir.eid)
+            perm_eid = str(perm.eid)
             req.form = {
                 'eid': [dir_eid, perm_eid],
                 '__maineid': dir_eid,
@@ -596,7 +594,7 @@
                 with self.admin_access.web_request(vid='test.ajax.error', url='') as req:
                     req.ajax_request = True
                     app.handle_request(req)
-        self.assertEqual(http_client.INTERNAL_SERVER_ERROR,
+        self.assertEqual(http.client.INTERNAL_SERVER_ERROR,
                          req.status_out)
 
     def _test_cleaned(self, kwargs, injected, cleaned):
@@ -760,18 +758,6 @@
             req.form['rql'] = 'rql:Any OV1, X WHERE X custom_workflow OV1?'
             self.app_handle_request(req)
 
-    def test_handle_deprecation(self):
-        """Test deprecation warning for *_handle methods."""
-        with self.admin_access.web_request(url='foo') as req:
-            with self.assertWarns(DeprecationWarning) as cm:
-                self.app.core_handle(req, 'foo')
-            self.assertIn('path argument got removed from "core_handle"',
-                          str(cm.warning))
-            with self.assertWarns(DeprecationWarning) as cm:
-                self.app.main_handle_request('foo', req)
-            self.assertIn('entry point arguments are now (req, path)',
-                          str(cm.warning))
-
 
 if __name__ == '__main__':
     unittest_main()
--- a/cubicweb/web/test/unittest_form.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/test/unittest_form.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,8 +19,6 @@
 import time
 from datetime import datetime
 
-from six import text_type
-
 import pytz
 
 from lxml import html
@@ -80,19 +78,19 @@
             t = req.create_entity('Tag', name=u'x')
             form1 = self.vreg['forms'].select('edition', req, entity=t)
             choices = [reid for rview, reid in form1.field_by_name('tags', 'subject', t.e_schema).choices(form1)]
-            self.assertIn(text_type(b.eid), choices)
+            self.assertIn(str(b.eid), choices)
             form2 = self.vreg['forms'].select('edition', req, entity=b)
             choices = [reid for rview, reid in form2.field_by_name('tags', 'object', t.e_schema).choices(form2)]
-            self.assertIn(text_type(t.eid), choices)
+            self.assertIn(str(t.eid), choices)
 
             b.cw_clear_all_caches()
             t.cw_clear_all_caches()
             req.cnx.execute('SET X tags Y WHERE X is Tag, Y is BlogEntry')
 
             choices = [reid for rview, reid in form1.field_by_name('tags', 'subject', t.e_schema).choices(form1)]
-            self.assertIn(text_type(b.eid), choices)
+            self.assertIn(str(b.eid), choices)
             choices = [reid for rview, reid in form2.field_by_name('tags', 'object', t.e_schema).choices(form2)]
-            self.assertIn(text_type(t.eid), choices)
+            self.assertIn(str(t.eid), choices)
 
     def test_form_field_choices_new_entity(self):
         with self.admin_access.web_request() as req:
--- a/cubicweb/web/test/unittest_magicsearch.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/test/unittest_magicsearch.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,8 +21,6 @@
 import sys
 from contextlib import contextmanager
 
-from six.moves import range
-
 from logilab.common.testlib import TestCase, unittest_main
 
 from rql import BadRQLQuery, RQLSyntaxError
--- a/cubicweb/web/test/unittest_request.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/test/unittest_request.py	Fri Oct 18 23:39:03 2019 +0200
@@ -4,8 +4,6 @@
 import unittest
 from functools import partial
 
-from six import text_type
-
 from cubicweb.devtools.fake import FakeConfig, FakeCWRegistryStore
 
 from cubicweb.web.request import (CubicWebRequestBase, _parse_accept_header,
@@ -84,7 +82,7 @@
         vreg = FakeCWRegistryStore(FakeConfig(), initlog=False)
         vreg.config['base-url'] = 'http://testing.fr/cubicweb/'
         vreg.config['language-mode'] = 'url-prefix'
-        vreg.config.translations['fr'] = text_type, text_type
+        vreg.config.translations['fr'] = str, str
         req = CubicWebRequestBase(vreg)
         # Override from_controller to avoid getting into relative_path method,
         # which is not implemented in CubicWebRequestBase.
--- a/cubicweb/web/test/unittest_urlrewrite.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/test/unittest_urlrewrite.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,8 +16,6 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 
-from six import text_type
-
 from logilab.common import tempattr
 
 from cubicweb.devtools.testlib import CubicWebTC
@@ -139,8 +137,8 @@
                  rgx_action(r'Any X WHERE X surname %(sn)s, '
                             'X firstname %(fn)s',
                             argsgroups=('sn', 'fn'),
-                            transforms={'sn' : text_type.capitalize,
-                                        'fn' : text_type.lower,})),
+                            transforms={'sn' : str.capitalize,
+                                        'fn' : str.lower,})),
                 ]
         with self.admin_access.web_request() as req:
             rewriter = TestSchemaBasedRewriter(req)
--- a/cubicweb/web/test/unittest_views_basecontrollers.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/test/unittest_views_basecontrollers.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,14 +18,11 @@
 """cubicweb.web.views.basecontrollers unit tests"""
 
 import time
-
-from six import text_type
-from six.moves.urllib.parse import urlsplit, urlunsplit, urljoin, parse_qs
+from urllib.parse import urlsplit, urlunsplit, urljoin, parse_qs
 
 import lxml
 
 from logilab.common.testlib import unittest_main
-from logilab.common.decorators import monkeypatch
 
 from cubicweb import Binary, NoSelectableObject, ValidationError, transaction as tx
 from cubicweb.schema import RRQLExpression
@@ -37,7 +34,6 @@
 from cubicweb.uilib import rql_for_eid
 from cubicweb.web import Redirect, RemoteCallFailed, http_headers, formfields as ff
 from cubicweb.web.views.autoform import get_pending_inserts, get_pending_deletes
-from cubicweb.web.views.basecontrollers import JSonController, xhtmlize, jsonize
 from cubicweb.web.views.ajaxcontroller import ajaxfunc, AjaxFunction
 from cubicweb.server.session import Connection
 from cubicweb.server.hook import Hook, Operation
@@ -94,7 +90,7 @@
                     }
             with self.assertRaises(ValidationError) as cm:
                 self.ctrl_publish(req)
-            cm.exception.translate(text_type)
+            cm.exception.translate(str)
             expected = {
                 '': u'some relations violate a unicity constraint',
                 'login': u'login is part of violated unicity constraint',
@@ -151,12 +147,12 @@
             user = req.user
             groupeids = [eid for eid, in req.execute('CWGroup G WHERE G name '
                                                      'in ("managers", "users")')]
-            groups = [text_type(eid) for eid in groupeids]
-            eid = text_type(user.eid)
+            groups = [str(eid) for eid in groupeids]
+            eid = str(user.eid)
             req.form = {
                 'eid': eid, '__type:'+eid: 'CWUser',
                 '_cw_entity_fields:'+eid: 'login-subject,firstname-subject,surname-subject,in_group-subject',
-                'login-subject:'+eid:     text_type(user.login),
+                'login-subject:'+eid:     str(user.login),
                 'surname-subject:'+eid: u'Th\xe9nault',
                 'firstname-subject:'+eid:   u'Sylvain',
                 'in_group-subject:'+eid:  groups,
@@ -174,7 +170,7 @@
             self.create_user(cnx, u'user')
             cnx.commit()
         with self.new_access(u'user').web_request() as req:
-            eid = text_type(req.user.eid)
+            eid = str(req.user.eid)
             req.form = {
                 'eid': eid, '__maineid' : eid,
                 '__type:'+eid: 'CWUser',
@@ -194,12 +190,12 @@
         with self.admin_access.web_request() as req:
             user = req.user
             groupeids = [g.eid for g in user.in_group]
-            eid = text_type(user.eid)
+            eid = str(user.eid)
             req.form = {
                 'eid':       eid,
                 '__type:'+eid:    'CWUser',
                 '_cw_entity_fields:'+eid: 'login-subject,firstname-subject,surname-subject',
-                'login-subject:'+eid:     text_type(user.login),
+                'login-subject:'+eid:     str(user.login),
                 'firstname-subject:'+eid: u'Th\xe9nault',
                 'surname-subject:'+eid:   u'Sylvain',
                 }
@@ -222,7 +218,7 @@
                         'login-subject:X': u'adim',
                         'upassword-subject:X': u'toto', 'upassword-subject-confirm:X': u'toto',
                         'surname-subject:X': u'Di Mascio',
-                        'in_group-subject:X': text_type(gueid),
+                        'in_group-subject:X': str(gueid),
 
                         '__type:Y': 'EmailAddress',
                         '_cw_entity_fields:Y': 'address-subject,use_email-object',
@@ -289,7 +285,7 @@
         # non regression test for #3120495. Without the fix, leads to
         # "unhashable type: 'list'" error
         with self.admin_access.web_request() as req:
-            cwrelation = text_type(req.execute('CWEType X WHERE X name "CWSource"')[0][0])
+            cwrelation = str(req.execute('CWEType X WHERE X name "CWSource"')[0][0])
             req.form = {'eid': [cwrelation], '__maineid' : cwrelation,
 
                         '__type:'+cwrelation: 'CWEType',
@@ -302,7 +298,7 @@
 
     def test_edit_multiple_linked(self):
         with self.admin_access.web_request() as req:
-            peid = text_type(self.create_user(req, u'adim').eid)
+            peid = str(self.create_user(req, u'adim').eid)
             req.form = {'eid': [peid, 'Y'], '__maineid': peid,
 
                         '__type:'+peid: u'CWUser',
@@ -322,7 +318,7 @@
             self.assertEqual(email.address, 'dima@logilab.fr')
 
         # with self.admin_access.web_request() as req:
-            emaileid = text_type(email.eid)
+            emaileid = str(email.eid)
             req.form = {'eid': [peid, emaileid],
 
                         '__type:'+peid: u'CWUser',
@@ -344,7 +340,7 @@
         with self.admin_access.web_request() as req:
             user = req.user
             req.form = {'eid': 'X',
-                        '__cloned_eid:X': text_type(user.eid), '__type:X': 'CWUser',
+                        '__cloned_eid:X': str(user.eid), '__type:X': 'CWUser',
                         '_cw_entity_fields:X': 'login-subject,upassword-subject',
                         'login-subject:X': u'toto',
                         'upassword-subject:X': u'toto',
@@ -353,7 +349,7 @@
                 self.ctrl_publish(req)
             self.assertEqual({'upassword-subject': u'password and confirmation don\'t match'},
                              cm.exception.errors)
-            req.form = {'__cloned_eid:X': text_type(user.eid),
+            req.form = {'__cloned_eid:X': str(user.eid),
                         'eid': 'X', '__type:X': 'CWUser',
                         '_cw_entity_fields:X': 'login-subject,upassword-subject',
                         'login-subject:X': u'toto',
@@ -377,11 +373,11 @@
                         '__type:X': 'Salesterm',
                         '_cw_entity_fields:X': 'amount-subject,described_by_test-subject',
                         'amount-subject:X': u'-10',
-                        'described_by_test-subject:X': text_type(feid),
+                        'described_by_test-subject:X': str(feid),
                     }
             with self.assertRaises(ValidationError) as cm:
                 self.ctrl_publish(req)
-            cm.exception.translate(text_type)
+            cm.exception.translate(str)
             self.assertEqual({'amount-subject': 'value -10 must be >= 0'},
                              cm.exception.errors)
 
@@ -390,11 +386,11 @@
                         '__type:X': 'Salesterm',
                         '_cw_entity_fields:X': 'amount-subject,described_by_test-subject',
                         'amount-subject:X': u'110',
-                        'described_by_test-subject:X': text_type(feid),
+                        'described_by_test-subject:X': str(feid),
                         }
             with self.assertRaises(ValidationError) as cm:
                 self.ctrl_publish(req)
-            cm.exception.translate(text_type)
+            cm.exception.translate(str)
             self.assertEqual(cm.exception.errors, {'amount-subject': 'value 110 must be <= 100'})
 
         with self.admin_access.web_request(rollbackfirst=True) as req:
@@ -402,7 +398,7 @@
                         '__type:X': 'Salesterm',
                         '_cw_entity_fields:X': 'amount-subject,described_by_test-subject',
                         'amount-subject:X': u'10',
-                        'described_by_test-subject:X': text_type(feid),
+                        'described_by_test-subject:X': str(feid),
                         }
             self.expect_redirect_handle_request(req, 'edit')
             # should be redirected on the created
@@ -421,7 +417,7 @@
 
         # ensure a value that violate a constraint is properly detected
         with self.admin_access.web_request(rollbackfirst=True) as req:
-            req.form = {'eid': [text_type(seid)],
+            req.form = {'eid': [str(seid)],
                         '__type:%s'%seid: 'Salesterm',
                         '_cw_entity_fields:%s'%seid: 'amount-subject',
                         'amount-subject:%s'%seid: u'-10',
@@ -432,7 +428,7 @@
 
         # ensure a value that comply a constraint is properly processed
         with self.admin_access.web_request(rollbackfirst=True) as req:
-            req.form = {'eid': [text_type(seid)],
+            req.form = {'eid': [str(seid)],
                         '__type:%s'%seid: 'Salesterm',
                         '_cw_entity_fields:%s'%seid: 'amount-subject',
                         'amount-subject:%s'%seid: u'20',
@@ -448,7 +444,7 @@
                         '__type:X': 'Salesterm',
                         '_cw_entity_fields:X': 'amount-subject,described_by_test-subject',
                         'amount-subject:X': u'0',
-                        'described_by_test-subject:X': text_type(feid),
+                        'described_by_test-subject:X': str(feid),
                     }
 
             # ensure a value that is modified in an operation on a modify
@@ -556,7 +552,7 @@
     def test_redirect_delete_button(self):
         with self.admin_access.web_request() as req:
             eid = req.create_entity('BlogEntry', title=u'hop', content=u'hop').eid
-            req.form = {'eid': text_type(eid), '__type:%s'%eid: 'BlogEntry',
+            req.form = {'eid': str(eid), '__type:%s'%eid: 'BlogEntry',
                         '__action_delete': ''}
             path, params = self.expect_redirect_handle_request(req, 'edit')
             self.assertEqual(path, 'blogentry')
@@ -565,14 +561,14 @@
             req.execute('SET X use_email E WHERE E eid %(e)s, X eid %(x)s',
                         {'x': req.user.eid, 'e': eid})
             req.cnx.commit()
-            req.form = {'eid': text_type(eid), '__type:%s'%eid: 'EmailAddress',
+            req.form = {'eid': str(eid), '__type:%s'%eid: 'EmailAddress',
                         '__action_delete': ''}
             path, params = self.expect_redirect_handle_request(req, 'edit')
             self.assertEqual(path, 'cwuser/admin')
             self.assertIn('_cwmsgid', params)
             eid1 = req.create_entity('BlogEntry', title=u'hop', content=u'hop').eid
             eid2 = req.create_entity('EmailAddress', address=u'hop@logilab.fr').eid
-            req.form = {'eid': [text_type(eid1), text_type(eid2)],
+            req.form = {'eid': [str(eid1), str(eid2)],
                         '__type:%s'%eid1: 'BlogEntry',
                         '__type:%s'%eid2: 'EmailAddress',
                         '__action_delete': ''}
@@ -674,13 +670,13 @@
             groupeids = sorted(eid
                                for eid, in req.execute('CWGroup G '
                                                        'WHERE G name in ("managers", "users")'))
-            groups = [text_type(eid) for eid in groupeids]
+            groups = [str(eid) for eid in groupeids]
             cwetypeeid = req.execute('CWEType X WHERE X name "CWEType"')[0][0]
-            basegroups = [text_type(eid)
+            basegroups = [str(eid)
                           for eid, in req.execute('CWGroup G '
                                                   'WHERE X read_permission G, X eid %(x)s',
                                                   {'x': cwetypeeid})]
-            cwetypeeid = text_type(cwetypeeid)
+            cwetypeeid = str(cwetypeeid)
             req.form = {
                 'eid':      cwetypeeid,
                 '__type:'+cwetypeeid:  'CWEType',
@@ -760,8 +756,8 @@
         with self.admin_access.web_request(url='edit') as req:
             p = self.create_user(req, u"doe")
             # do not try to skip 'primary_email' for this test
-            old_skips = p.__class__.skip_copy_for
-            p.__class__.skip_copy_for = ()
+            old_skips = p.__class__.cw_skip_copy_for
+            p.__class__.cw_skip_copy_for = ()
             try:
                 e = req.create_entity('EmailAddress', address=u'doe@doe.com')
                 req.execute('SET P use_email E, P primary_email E WHERE P eid %(p)s, E eid %(e)s',
@@ -786,7 +782,7 @@
                 rset = req.execute('CWUser P WHERE P surname "Boom"')
                 self.assertEqual(len(rset), 0)
             finally:
-                p.__class__.skip_copy_for = old_skips
+                p.__class__.cw_skip_copy_for = old_skips
 
     def test_regr_inlined_forms(self):
         with self.admin_access.web_request() as req:
@@ -1020,64 +1016,6 @@
         self.assertEqual(cm.exception.reason, 'no foo method')
 
 
-class JSonControllerTC(AjaxControllerTC):
-    # NOTE: this class performs the same tests as AjaxController but with
-    #       deprecated 'json' controller (i.e. check backward compatibility)
-    tested_controller = 'json'
-
-    def setUp(self):
-        super(JSonControllerTC, self).setUp()
-        self.exposed_remote_funcs = [fname for fname in dir(JSonController)
-                                     if fname.startswith('js_')]
-
-    def tearDown(self):
-        super(JSonControllerTC, self).tearDown()
-        for funcname in dir(JSonController):
-            # remove functions added dynamically during tests
-            if funcname.startswith('js_') and funcname not in self.exposed_remote_funcs:
-                delattr(JSonController, funcname)
-
-    def test_monkeypatch_jsoncontroller(self):
-        with self.assertRaises(RemoteCallFailed):
-            with self.remote_calling('foo'):
-                pass
-        @monkeypatch(JSonController)
-        def js_foo(self):
-            return u'hello'
-        with self.remote_calling('foo') as (res, _):
-            self.assertEqual(res, b'hello')
-
-    def test_monkeypatch_jsoncontroller_xhtmlize(self):
-        with self.assertRaises(RemoteCallFailed):
-            with self.remote_calling('foo'):
-                pass
-        @monkeypatch(JSonController)
-        @xhtmlize
-        def js_foo(self):
-            return u'hello'
-        with self.remote_calling('foo') as (res, _):
-            self.assertEqual(b'<div>hello</div>', res)
-
-    def test_monkeypatch_jsoncontroller_jsonize(self):
-        with self.assertRaises(RemoteCallFailed):
-            with self.remote_calling('foo'):
-                pass
-        @monkeypatch(JSonController)
-        @jsonize
-        def js_foo(self):
-            return 12
-        with self.remote_calling('foo') as (res, _):
-            self.assertEqual(res, b'12')
-
-    def test_monkeypatch_jsoncontroller_stdfunc(self):
-        @monkeypatch(JSonController)
-        @jsonize
-        def js_reledit_form(self):
-            return 12
-        with self.remote_calling('reledit_form') as (res, _):
-            self.assertEqual(res, b'12')
-
-
 class UndoControllerTC(CubicWebTC):
 
     def setUp(self):
--- a/cubicweb/web/test/unittest_views_errorform.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/test/unittest_views_errorform.py	Fri Oct 18 23:39:03 2019 +0200
@@ -51,7 +51,7 @@
                     req.data['ex'] = e
                     html = self.view('error', req=req)
                     self.assertTrue(re.search(b'^<input name="__signature" type="hidden" '
-                                              b'value="[0-9a-f]{32}" />$',
+                                              b'value="[0-9a-f]{128}" />$',
                                               html.source, re.M))
 
 
--- a/cubicweb/web/test/unittest_views_json.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/test/unittest_views_json.py	Fri Oct 18 23:39:03 2019 +0200
@@ -16,7 +16,6 @@
 #
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-from six import binary_type
 
 from cubicweb.devtools.testlib import CubicWebTC
 
@@ -50,7 +49,7 @@
                              'rql': u'Any GN,COUNT(X) GROUPBY GN ORDERBY GN '
                              'WHERE X in_group G, G name GN'})
             data = self.ctrl_publish(req, ctrl='jsonp')
-            self.assertIsInstance(data, binary_type)
+            self.assertIsInstance(data, bytes)
             self.assertEqual(req.headers_out.getRawHeaders('content-type'),
                              ['application/javascript'])
             # because jsonp anonymizes data, only 'guests' group should be found
--- a/cubicweb/web/test/unittest_viewselector.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/test/unittest_viewselector.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,15 +17,14 @@
 # You should have received a copy of the GNU Lesser General Public License along
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """XXX rename, split, reorganize this"""
-from __future__ import print_function
 
+from logilab.common.registry import NoSelectableObject
 from logilab.common.testlib import unittest_main
 
 from cubicweb.devtools.testlib import CubicWebTC
 from cubicweb import Binary, UnknownProperty
 from cubicweb.predicates import (is_instance,
                                  specified_etype_implements, rql_condition)
-from cubicweb.web import NoSelectableObject
 from cubicweb.web.action import Action
 
 from cubicweb.web.views import (primary, baseviews, tableview,
--- a/cubicweb/web/test/unittest_web.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/test/unittest_web.py	Fri Oct 18 23:39:03 2019 +0200
@@ -28,7 +28,7 @@
     requests = None
 
 from logilab.common.testlib import TestCase, unittest_main
-from cubicweb.devtools.httptest import CubicWebWsgiTC
+from cubicweb.devtools.httptest import CubicWebServerTC
 from cubicweb.devtools.fake import FakeRequest
 
 class AjaxReplaceUrlTC(TestCase):
@@ -56,7 +56,7 @@
             req.html_headers.post_inlined_scripts[0])
 
 
-class FileUploadTC(CubicWebWsgiTC):
+class FileUploadTC(CubicWebServerTC):
 
     def setUp(self):
         "Skip whole test class if a suitable requests module is not available"
@@ -101,7 +101,7 @@
         self.assertDictEqual(expect, loads(webreq.text))
 
 
-class LanguageTC(CubicWebWsgiTC):
+class LanguageTC(CubicWebServerTC):
 
     def test_language_neg(self):
         headers = {'Accept-Language': 'fr'}
@@ -132,7 +132,7 @@
         self.assertIn('HttpOnly', webreq.getheader('set-cookie'))
 
 
-class MiscOptionsTC(CubicWebWsgiTC):
+class MiscOptionsTC(CubicWebServerTC):
     @classmethod
     def setUpClass(cls):
         super(MiscOptionsTC, cls).setUpClass()
--- a/cubicweb/web/uicfg.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,28 +0,0 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-This module has been moved to web.views.uicfg.
-"""
-
-
-from warnings import warn
-from cubicweb.web.views.uicfg import *
-
-
-warn('[3.16] moved to cubicweb.web.views.uicfg',
-     DeprecationWarning, stacklevel=2)
--- a/cubicweb/web/uihelper.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/uihelper.py	Fri Oct 18 23:39:03 2019 +0200
@@ -45,8 +45,6 @@
 """
 
 
-from six import add_metaclass
-
 from logilab.common.deprecation import deprecated
 from cubicweb.web.views import uicfg
 
@@ -94,8 +92,7 @@
         super(meta_formconfig, cls).__init__(name, bases, classdict)
 
 
-@add_metaclass(meta_formconfig)
-class FormConfig:
+class FormConfig(metaclass=meta_formconfig):
     """helper base class to define uicfg rules on a given entity type.
 
     In all descriptions below, attributes list can either be a list of
--- a/cubicweb/web/views/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,15 +19,10 @@
 
 
 
-import os
 import sys
-import tempfile
-
-from six import add_metaclass
 
 from rql import nodes
 from logilab.mtconverter import xml_escape
-from logilab.common.deprecation import class_deprecated
 
 
 def need_table_view(rset, schema):
@@ -126,23 +121,3 @@
         return u'<a href="%s" class="%s">%s</a>' % (
             xml_escape(url), csscls, req.__('New %s' % etype))
     return u''
-
-
-
-@add_metaclass(class_deprecated)
-class TmpFileViewMixin(object):
-    __deprecation_warning__ = '[3.18] %(cls)s is deprecated'
-    binary = True
-    content_type = 'application/octet-stream'
-    cache_max_age = 60*60*2 # stay in http cache for 2 hours by default
-
-    def call(self):
-        self.cell_call()
-
-    def cell_call(self, row=0, col=0):
-        self.cw_row, self.cw_col = row, col # in case one needs it
-        fd, tmpfile = tempfile.mkstemp('.png')
-        os.close(fd)
-        self._generate(tmpfile)
-        self.w(open(tmpfile, 'rb').read())
-        os.unlink(tmpfile)
--- a/cubicweb/web/views/ajaxcontroller.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/ajaxcontroller.py	Fri Oct 18 23:39:03 2019 +0200
@@ -61,17 +61,11 @@
 
 """
 
-
-
-from warnings import warn
+import http.client as http_client
 from functools import partial
 
-from six import PY2, text_type
-from six.moves import http_client
-
 from logilab.common.date import strptime
 from logilab.common.registry import yes
-from logilab.common.deprecation import deprecated
 
 from cubicweb import ObjectNotFound, NoSelectableObject, ValidationError
 from cubicweb.appobject import AppObject
@@ -119,23 +113,11 @@
         except KeyError:
             raise RemoteCallFailed('no method specified',
                                    status=http_client.BAD_REQUEST)
-        # 1/ check first for old-style (JSonController) ajax func for bw compat
         try:
-            func = getattr(basecontrollers.JSonController, 'js_%s' % fname)
-            if PY2:
-                func = func.__func__
-            func = partial(func, self)
-        except AttributeError:
-            # 2/ check for new-style (AjaxController) ajax func
-            try:
-                func = self._cw.vreg['ajax-func'].select(fname, self._cw)
-            except ObjectNotFound:
-                raise RemoteCallFailed('no %s method' % fname,
-                                       status=http_client.BAD_REQUEST)
-        else:
-            warn('[3.15] remote function %s found on JSonController, '
-                 'use AjaxFunction / @ajaxfunc instead' % fname,
-                 DeprecationWarning, stacklevel=2)
+            func = self._cw.vreg['ajax-func'].select(fname, self._cw)
+        except ObjectNotFound:
+            raise RemoteCallFailed('no %s method' % fname,
+                                   status=http_client.BAD_REQUEST)
         debug_mode = self._cw.vreg.config.debugmode
         # no <arg> attribute means the callback takes no argument
         args = self._cw.form.get('arg', ())
@@ -165,7 +147,7 @@
         if result is None:
             return b''
         # get unicode on @htmlize methods, encoded string on @jsonize methods
-        elif isinstance(result, text_type):
+        elif isinstance(result, str):
             return result.encode(self._cw.encoding)
         return result
 
@@ -444,16 +426,6 @@
     """remove user's session data associated to current pageid"""
     self._cw.session.data.pop(self._cw.pageid, None)
 
-@ajaxfunc(output_type='json')
-@deprecated("[3.13] use jQuery.cookie(cookiename, cookievalue, {path: '/'}) in js land instead")
-def set_cookie(self, cookiename, cookievalue):
-    """generates the Set-Cookie HTTP reponse header corresponding
-    to `cookiename` / `cookievalue`.
-    """
-    cookiename, cookievalue = str(cookiename), str(cookievalue)
-    self._cw.set_cookie(cookiename, cookievalue)
-
-
 
 @ajaxfunc
 def delete_relation(self, rtype, subjeid, objeid):
--- a/cubicweb/web/views/authentication.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/authentication.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,8 +17,6 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """user authentication component"""
 
-from six import text_type
-
 from logilab.common.deprecation import class_renamed
 from logilab.common.textutils import unormalize
 
@@ -114,8 +112,8 @@
         self.sessionid = make_uid(unormalize(user.login))
         self.data = {}
 
-    def __unicode__(self):
-        return '<session %s (0x%x)>' % (text_type(self.user.login), id(self))
+    def __str__(self):
+        return '<session %s (0x%x)>' % (self.user.login, id(self))
 
     @property
     def anonymous_session(self):
--- a/cubicweb/web/views/autoform.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/autoform.py	Fri Oct 18 23:39:03 2019 +0200
@@ -118,9 +118,6 @@
 .. Controlling the generic relation fields
 """
 
-import six
-from six.moves import range
-
 from logilab.mtconverter import xml_escape
 from logilab.common.decorators import iclassmethod, cached
 from logilab.common.registry import NoSelectableObject
@@ -311,7 +308,7 @@
         if form.form_previous_values:
             cdvalues = self._cw.list_form_param(eid_param(self.rtype, self.peid),
                                                 form.form_previous_values)
-            if six.text_type(entity.eid) not in cdvalues:
+            if str(entity.eid) not in cdvalues:
                 return False
         return True
 
--- a/cubicweb/web/views/basecomponents.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/basecomponents.py	Fri Oct 18 23:39:03 2019 +0200
@@ -26,12 +26,9 @@
 from logilab.mtconverter import xml_escape
 from logilab.common.registry import yes
 from logilab.common.deprecation import class_renamed
-from rql import parse
 
-from cubicweb.predicates import (match_form_params, match_context,
-                                 multi_etypes_rset, configuration_values,
+from cubicweb.predicates import (match_context, configuration_values,
                                  anonymous_user, authenticated_user)
-from cubicweb.schema import display_name
 from cubicweb.utils import wrap_on_write
 from cubicweb.uilib import toggle_action
 from cubicweb.web import component
--- a/cubicweb/web/views/basecontrollers.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/basecontrollers.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,15 +19,7 @@
 object to handle publication.
 """
 
-
-from cubicweb import _
-
-from warnings import warn
-
-from six import text_type
-from six.moves import http_client
-
-from logilab.common.deprecation import deprecated
+import http.client
 
 from cubicweb import (NoSelectableObject, ObjectNotFound, ValidationError,
                       AuthenticationError, UndoTransactionException,
@@ -35,44 +27,9 @@
 from cubicweb.utils import json_dumps
 from cubicweb.predicates import (authenticated_user, anonymous_user,
                                 match_form_params)
-from cubicweb.web import Redirect, RemoteCallFailed
-from cubicweb.web.controller import Controller, append_url_params
+from cubicweb.web import Redirect
+from cubicweb.web.controller import Controller
 from cubicweb.web.views import vid_from_rset
-import cubicweb.transaction as tx
-
-@deprecated('[3.15] jsonize is deprecated, use AjaxFunction appobjects instead')
-def jsonize(func):
-    """decorator to sets correct content_type and calls `json_dumps` on
-    results
-    """
-    def wrapper(self, *args, **kwargs):
-        self._cw.set_content_type('application/json')
-        return json_dumps(func(self, *args, **kwargs))
-    wrapper.__name__ = func.__name__
-    return wrapper
-
-@deprecated('[3.15] xhtmlize is deprecated, use AjaxFunction appobjects instead')
-def xhtmlize(func):
-    """decorator to sets correct content_type and calls `xmlize` on results"""
-    def wrapper(self, *args, **kwargs):
-        self._cw.set_content_type(self._cw.html_content_type())
-        result = func(self, *args, **kwargs)
-        return ''.join((u'<div>', result.strip(),
-                        u'</div>'))
-    wrapper.__name__ = func.__name__
-    return wrapper
-
-@deprecated('[3.15] check_pageid is deprecated, use AjaxFunction appobjects instead')
-def check_pageid(func):
-    """decorator which checks the given pageid is found in the
-    user's session data
-    """
-    def wrapper(self, *args, **kwargs):
-        data = self._cw.session.data.get(self._cw.pageid)
-        if data is None:
-            raise RemoteCallFailed(self._cw._('pageid-not-found'))
-        return func(self, *args, **kwargs)
-    return wrapper
 
 
 class LoginController(Controller):
@@ -86,7 +43,7 @@
             raise AuthenticationError()
         else:
             # Cookie authentication
-            self._cw.status_out = http_client.FORBIDDEN
+            self._cw.status_out = http.client.FORBIDDEN
             return self.appli.need_login_content(self._cw)
 
 class LoginControllerForAuthed(Controller):
@@ -229,7 +186,7 @@
     except Exception as ex:
         req.cnx.rollback()
         req.exception('unexpected error while validating form')
-        return (False, text_type(ex), ctrl._edited_entity)
+        return (False, str(ex), ctrl._edited_entity)
     return (False, '???', None)
 
 
@@ -255,16 +212,6 @@
         return self.response(domid, status, args, entity).encode(self._cw.encoding)
 
 
-class JSonController(Controller):
-    __regid__ = 'json'
-
-    def publish(self, rset=None):
-        warn('[3.15] JSONController is deprecated, use AjaxController instead',
-             DeprecationWarning)
-        ajax_controller = self._cw.vreg['controllers'].select('ajax', self._cw, appli=self.appli)
-        return ajax_controller.publish(rset)
-
-
 class MailBugReportController(Controller):
     __regid__ = 'reportbug'
     __select__ = match_form_params('description')
--- a/cubicweb/web/views/baseviews.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/baseviews.py	Fri Oct 18 23:39:03 2019 +0200
@@ -77,10 +77,6 @@
 
 from cubicweb import _
 
-from warnings import warn
-
-from six.moves import range
-
 from logilab.mtconverter import TransformError, xml_escape
 from logilab.common.registry import yes
 
@@ -368,11 +364,8 @@
 
         :param listid: the DOM id to use for the root element
         """
-        if subvid is None and 'vid' in kwargs:
-            warn("should give a 'subvid' argument instead of 'vid'",
-                 DeprecationWarning, stacklevel=2)
-        else:
-            kwargs['vid'] = subvid
+        assert 'vid' not in kwargs
+        kwargs['vid'] = subvid
         return super(SimpleListView, self).call(**kwargs)
 
 
@@ -626,18 +619,3 @@
         url = self.index_url(basepath, key[1], vtitle=vtitle)
         title = self._cw._('archive for %(author)s') % {'author': key[0]}
         return tags.a(label, href=url, title=title)
-
-
-# bw compat ####################################################################
-
-from logilab.common.deprecation import class_moved, class_deprecated
-
-from cubicweb.web.views import boxes, xmlrss, primary, tableview
-PrimaryView = class_moved(primary.PrimaryView)
-SideBoxView = class_moved(boxes.SideBoxView)
-XmlView = class_moved(xmlrss.XMLView)
-XmlItemView = class_moved(xmlrss.XMLItemView)
-XmlRsetView = class_moved(xmlrss.XMLRsetView)
-RssView = class_moved(xmlrss.RSSView)
-RssItemView = class_moved(xmlrss.RSSItemView)
-TableView = class_moved(tableview.TableView)
--- a/cubicweb/web/views/boxes.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/boxes.py	Fri Oct 18 23:39:03 2019 +0200
@@ -28,12 +28,7 @@
 
 from cubicweb import _
 
-from warnings import warn
-
-from six import text_type, add_metaclass
-
 from logilab.mtconverter import xml_escape
-from logilab.common.deprecation import class_deprecated
 
 from cubicweb import Unauthorized
 from cubicweb.predicates import (match_user_groups, match_kwargs,
@@ -215,7 +210,7 @@
 
     @property
     def domid(self):
-        return super(RsetBox, self).domid + text_type(abs(id(self))) + text_type(abs(id(self.cw_rset)))
+        return super(RsetBox, self).domid + str(abs(id(self))) + str(abs(id(self.cw_rset)))
 
     def render_title(self, w):
         w(self.cw_extra_kwargs['title'])
@@ -230,28 +225,6 @@
 
  # helper classes ##############################################################
 
-@add_metaclass(class_deprecated)
-class SideBoxView(EntityView):
-    """helper view class to display some entities in a sidebox"""
-    __deprecation_warning__ = '[3.10] SideBoxView is deprecated, use RsetBox instead (%(cls)s)'
-
-    __regid__ = 'sidebox'
-
-    def call(self, title=u'', **kwargs):
-        """display a list of entities by calling their <item_vid> view"""
-        if 'dispctrl' in self.cw_extra_kwargs:
-            # XXX do not modify dispctrl!
-            self.cw_extra_kwargs['dispctrl'].setdefault('subvid', 'outofcontext')
-            self.cw_extra_kwargs['dispctrl'].setdefault('use_list_limit', 1)
-        if title:
-            self.cw_extra_kwargs['title'] = title
-        self.cw_extra_kwargs.setdefault('context', 'incontext')
-        box = self._cw.vreg['ctxcomponents'].select(
-            'rsetbox', self._cw, rset=self.cw_rset, vid='autolimited',
-            **self.cw_extra_kwargs)
-        box.render(self.w)
-
-
 class ContextualBoxLayout(component.Layout):
     __select__ = match_context('incontext', 'left', 'right') & contextual()
     # predefined class in cubicweb.css: contextualBox | contextFreeBox
--- a/cubicweb/web/views/csvexport.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/csvexport.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,9 +20,6 @@
 
 from cubicweb import _
 
-from six import PY2
-from six.moves import range
-
 from cubicweb.schema import display_name
 from cubicweb.predicates import any_rset, empty_rset
 from cubicweb.uilib import UnicodeCSVWriter
@@ -32,7 +29,7 @@
     """mixin class for CSV views"""
     templatable = False
     content_type = "text/comma-separated-values"
-    binary = PY2 # python csv module is unicode aware in py3k
+    binary = False
     csv_params = {'dialect': 'excel',
                   'quotechar': '"',
                   'delimiter': ';',
--- a/cubicweb/web/views/cwsources.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/cwsources.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,8 +21,6 @@
 
 import logging
 
-from six.moves import range
-
 from logilab.common.decorators import cachedproperty
 
 from cubicweb import _
--- a/cubicweb/web/views/cwuser.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/cwuser.py	Fri Oct 18 23:39:03 2019 +0200
@@ -22,9 +22,6 @@
 
 from hashlib import sha1  # pylint: disable=E0611
 
-from six import text_type
-from six.moves import range
-
 from logilab.mtconverter import xml_escape
 
 from cubicweb import tags
@@ -252,6 +249,6 @@
         'group': tableview.MainEntityColRenderer(),
         'nb_users': tableview.EntityTableColRenderer(
             header=_('num. users'),
-            renderfunc=lambda w, x: w(text_type(x.num_users())),
+            renderfunc=lambda w, x: w(str(x.num_users())),
             sortfunc=lambda x: x.num_users()),
     }
--- a/cubicweb/web/views/debug.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/debug.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,8 +21,6 @@
 
 from time import strftime, localtime
 
-from six import text_type
-
 from logilab.mtconverter import xml_escape
 
 from cubicweb.predicates import none_rset, match_user_groups
@@ -98,7 +96,7 @@
             if k.endswith('_cache_size'):
                 stats[k] = '%s / %s' % (stats[k]['size'], stats[k]['maxsize'])
         def format_stat(sname, sval):
-            return '%s %s' % (xml_escape(text_type(sval)),
+            return '%s %s' % (xml_escape(str(sval)),
                               sname.endswith('percent') and '%' or '')
         pyvalue = [(sname, format_stat(sname, sval))
                     for sname, sval in sorted(stats.items())]
--- a/cubicweb/web/views/editcontroller.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/editcontroller.py	Fri Oct 18 23:39:03 2019 +0200
@@ -22,8 +22,6 @@
 
 from datetime import datetime
 
-from six import text_type
-
 from logilab.common.graph import ordered_nodes
 
 from rql.utils import rqlvar_maker
@@ -201,7 +199,7 @@
             if '__linkto' in req.form and 'eid' in req.form:
                 self.execute_linkto()
             elif '__delete' not in req.form:
-                raise ValidationError(None, {None: text_type(ex)})
+                raise ValidationError(None, {None: str(ex)})
         # all pending inlined relations to newly created entities have been
         # treated now (pop to ensure there are no attempt to add new ones)
         pending_inlined = req.data.pop('pending_inlined')
@@ -234,7 +232,7 @@
                 autoform.delete_relations(self._cw, todelete)
         self._cw.remove_pending_operations()
         if self.errors:
-            errors = dict((f.name, text_type(ex)) for f, ex in self.errors)
+            errors = dict((f.name, str(ex)) for f, ex in self.errors)
             raise ValidationError(valerror_eid(form.get('__maineid')), errors)
 
     def _insert_entity(self, etype, eid, rqlquery):
@@ -285,7 +283,7 @@
             rqlquery.set_inlined(field.name, form_.edited_entity.eid)
         if not rqlquery.canceled:
             if self.errors:
-                errors = dict((f.role_name(), text_type(ex)) for f, ex in self.errors)
+                errors = dict((f.role_name(), str(ex)) for f, ex in self.errors)
                 raise ValidationError(valerror_eid(entity.eid), errors)
             if eid is None:  # creation or copy
                 entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery)
--- a/cubicweb/web/views/editforms.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/editforms.py	Fri Oct 18 23:39:03 2019 +0200
@@ -23,10 +23,7 @@
 
 from copy import copy
 
-from six.moves import range
-
 from logilab.common.registry import yes
-from logilab.common.deprecation import class_moved
 
 from cubicweb import _
 from cubicweb import tags
@@ -299,9 +296,3 @@
                                              copy_nav_params=True,
                                              formvid='edition')
         form.render(w=self.w)
-
-
-# click and edit handling ('reledit') ##########################################
-
-ClickAndEditFormView = class_moved(reledit.ClickAndEditFormView)
-AutoClickAndEditFormView = class_moved(reledit.AutoClickAndEditFormView)
--- a/cubicweb/web/views/embedding.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,38 +0,0 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Objects interacting together to provides the external page embeding
-functionality.
-"""
-
-from logilab.common.deprecation import class_moved, moved
-
-try:
-    from cubes.embed.views import *
-
-    IEmbedableAdapter = class_moved(IEmbedableAdapter, message='[3.17] IEmbedableAdapter moved to cubes.embed.views')
-    ExternalTemplate = class_moved(ExternalTemplate, message='[3.17] IEmbedableAdapter moved to cubes.embed.views')
-    EmbedController = class_moved(EmbedController, message='[3.17] IEmbedableAdapter moved to cubes.embed.views')
-    entity_has_embedable_url = moved('cubes.embed.views', 'entity_has_embedable_url')
-    EmbedAction = class_moved(EmbedAction, message='[3.17] EmbedAction moved to cubes.embed.views')
-    replace_href = class_moved(replace_href, message='[3.17] replace_href moved to cubes.embed.views')
-    embed_external_page = moved('cubes.embed.views', 'embed_external_page')
-    absolutize_links = class_moved(absolutize_links, message='[3.17] absolutize_links moved to cubes.embed.views')
-    prefix_links = moved('cubes.embed.views', 'prefix_links')
-except ImportError:
-    from cubicweb.web import LOGGER
-    LOGGER.warning('[3.17] embedding extracted to cube embed that was not found. try installing it.')
--- a/cubicweb/web/views/facets.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/facets.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 
 from cubicweb import _
 
-from warnings import warn
-
 from logilab.mtconverter import xml_escape
 from logilab.common.decorators import cachedproperty
 from logilab.common.registry import objectify_predicate, yes
@@ -129,7 +127,7 @@
     needs_js = ['cubicweb.ajax.js', 'cubicweb.facets.js']
     needs_css = ['cubicweb.facets.css']
 
-    def generate_form(self, w, rset, divid, vid, vidargs=None, mainvar=None,
+    def generate_form(self, w, rset, divid, vid, mainvar=None,
                       paginate=False, cssclass='', hiddens=None, **kwargs):
         """display a form to filter some view's content
 
@@ -163,12 +161,7 @@
         self._cw.add_css(self.needs_css)
         self._cw.html_headers.define_var('facetLoadingMsg',
                                          self._cw._('facet-loading-msg'))
-        if vidargs is not None:
-            warn("[3.14] vidargs is deprecated. Maybe you're using some TableView?",
-                 DeprecationWarning, stacklevel=2)
-        else:
-            vidargs = {}
-        vidargs = dict((k, v) for k, v in vidargs.items() if v)
+        vidargs = {}
         facetargs = xml_escape(json_dumps([divid, vid, paginate, vidargs]))
         w(u'<form id="%sForm" class="%s" method="post" action="" '
           'cubicweb:facetargs="%s" >' % (divid, cssclass, facetargs))
--- a/cubicweb/web/views/formrenderers.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/formrenderers.py	Fri Oct 18 23:39:03 2019 +0200
@@ -37,8 +37,6 @@
 
 from warnings import warn
 
-from six import text_type
-
 from logilab.mtconverter import xml_escape
 from logilab.common.registry import yes
 
@@ -121,7 +119,7 @@
             data.insert(0, errormsg)
         # NOTE: we call unicode because `tag` objects may be found within data
         #       e.g. from the cwtags library
-        w(''.join(text_type(x) for x in data))
+        w(''.join(str(x) for x in data))
 
     def render_content(self, w, form, values):
         if self.display_progress_div:
--- a/cubicweb/web/views/forms.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/forms.py	Fri Oct 18 23:39:03 2019 +0200
@@ -45,8 +45,6 @@
 import time
 import inspect
 
-from six import text_type
-
 from logilab.common import dictattr, tempattr
 from logilab.common.decorators import iclassmethod, cached
 from logilab.common.textutils import splitstrip
@@ -286,7 +284,7 @@
                 except ProcessFormError as exc:
                     errors.append((field, exc))
             if errors:
-                errors = dict((f.role_name(), text_type(ex)) for f, ex in errors)
+                errors = dict((f.role_name(), str(ex)) for f, ex in errors)
                 raise ValidationError(None, errors)
             return processed
 
--- a/cubicweb/web/views/ibreadcrumbs.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/ibreadcrumbs.py	Fri Oct 18 23:39:03 2019 +0200
@@ -22,8 +22,6 @@
 
 from warnings import warn
 
-from six import text_type
-
 from logilab.mtconverter import xml_escape
 
 from cubicweb import tags, uilib
@@ -146,7 +144,7 @@
                 xml_escape(url), xml_escape(uilib.cut(title, textsize))))
         else:
             textsize = self._cw.property_value('navigation.short-line-size')
-            w(xml_escape(uilib.cut(text_type(part), textsize)))
+            w(xml_escape(uilib.cut(str(part), textsize)))
 
 
 class BreadCrumbETypeVComponent(BreadCrumbEntityVComponent):
--- a/cubicweb/web/views/idownloadable.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/idownloadable.py	Fri Oct 18 23:39:03 2019 +0200
@@ -22,10 +22,7 @@
 
 from cubicweb import _
 
-from six.moves import range
-
 from logilab.mtconverter import BINARY_ENCODINGS, TransformError, xml_escape
-from logilab.common.deprecation import class_renamed, deprecated
 
 from cubicweb import tags
 from cubicweb.view import EntityView
--- a/cubicweb/web/views/igeocodable.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,37 +0,0 @@
-# copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for entities implementing IGeocodable"""
-
-try:
-    from cubes.geocoding.views import (IGeocodableAdapter,
-                                       GeocodingJsonView,
-                                       GoogleMapBubbleView,
-                                       GoogleMapsView,
-                                       GoogeMapsLegend)
-
-    from logilab.common.deprecation import class_moved
-
-    msg = '[3.17] cubicweb.web.views.igeocodable moved to cubes.geocoding.views'
-    IGeocodableAdapter = class_moved(IGeocodableAdapter, message=msg)
-    GeocodingJsonView = class_moved(GeocodingJsonView, message=msg)
-    GoogleMapBubbleView = class_moved(GoogleMapBubbleView, message=msg)
-    GoogleMapsView = class_moved(GoogleMapsView, message=msg)
-    GoogeMapsLegend = class_moved(GoogeMapsLegend, message=msg)
-except ImportError:
-    from cubicweb.web import LOGGER
-    LOGGER.warning('[3.17] igeocoding extracted to cube geocoding that was not found. try installing it.')
--- a/cubicweb/web/views/isioc.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Specific views for SIOC (Semantically-Interlinked Online Communities)
-
-http://sioc-project.org
-"""
-
-from logilab.common.deprecation import class_moved
-
-try:
-    from cubes.sioc.views import *
-
-    ISIOCItemAdapter = class_moved(ISIOCItemAdapter, message='[3.17] ISIOCItemAdapter moved to cubes.isioc.views')
-    ISIOCContainerAdapter = class_moved(ISIOCContainerAdapter, message='[3.17] ISIOCContainerAdapter moved to cubes.isioc.views')
-    SIOCView = class_moved(SIOCView, message='[3.17] SIOCView moved to cubes.is.view')
-    SIOCContainerView = class_moved(SIOCContainerView, message='[3.17] SIOCContainerView moved to cubes.is.view')
-    SIOCItemView = class_moved(SIOCItemView, message='[3.17] SIOCItemView moved to cubes.is.view')
-except ImportError:
-    from cubicweb.web import LOGGER
-    LOGGER.warning('[3.17] isioc extracted to cube sioc that was not found. try installing it.')
--- a/cubicweb/web/views/magicsearch.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/magicsearch.py	Fri Oct 18 23:39:03 2019 +0200
@@ -23,8 +23,6 @@
 import re
 from logging import getLogger
 
-from six import text_type
-
 from yams.interfaces import IVocabularyConstraint
 
 from rql import RQLSyntaxError, BadRQLQuery, parse
@@ -98,7 +96,7 @@
 
 def resolve_ambiguities(var_types, ambiguous_nodes, schema):
     """Tries to resolve remaining ambiguities for translation
-    /!\ An ambiguity is when two different string can be localized with
+    /!\\ An ambiguity is when two different string can be localized with
         the same string
     A simple example:
       - 'name' in a company context will be localized as 'nom' in French
@@ -388,7 +386,7 @@
         self.processors = sorted(processors, key=lambda x: x.priority)
 
     def process_query(self, uquery):
-        assert isinstance(uquery, text_type)
+        assert isinstance(uquery, str)
         try:
             procname, query = uquery.split(':', 1)
             proc = self.by_name[procname.strip().lower()]
--- a/cubicweb/web/views/massmailing.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""Mass mailing handling: send mail to entities adaptable to IEmailable"""
-
-try:
-    from cubes.massmailing.views import (SendEmailAction,
-                                         recipient_vocabulary,
-                                         MassMailingForm,
-                                         MassMailingFormRenderer,
-                                         MassMailingFormView,
-                                         SendMailController)
-
-
-    from logilab.common.deprecation import class_moved, moved
-
-    msg = '[3.17] cubicweb.web.views.massmailing moved to cubes.massmailing.views'
-    SendEmailAction = class_moved(SendEmailAction, message=msg)
-    recipient_vocabulary = moved('cubes.massmailing.views', 'recipient_vocabulary')
-    MassMailingForm = class_moved(MassMailingForm, message=msg)
-    MassMailingFormRenderer = class_moved(MassMailingFormRenderer, message=msg)
-    MassMailingFormView = class_moved(MassMailingFormView, message=msg)
-    SendMailController = class_moved(SendMailController, message=msg)
-except ImportError:
-    from cubicweb.web import LOGGER
-    LOGGER.warning('[3.17] massmailing extracted to cube massmailing that was not found. try installing it.')
--- a/cubicweb/web/views/navigation.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/navigation.py	Fri Oct 18 23:39:03 2019 +0200
@@ -50,12 +50,9 @@
 
 from datetime import datetime
 
-from six import text_type
-
 from rql.nodes import VariableRef, Constant
 
 from logilab.mtconverter import xml_escape
-from logilab.common.deprecation import deprecated
 
 from cubicweb.predicates import paginated_rset, sorted_rset, adaptable
 from cubicweb.uilib import cut
@@ -194,10 +191,10 @@
                 return entity.printable_value(attrname, format='text/plain')
         elif col is None: # smart links disabled.
             def index_display(row):
-                return text_type(row)
+                return str(row)
         elif self._cw.vreg.schema.eschema(rset.description[0][col]).final:
             def index_display(row):
-                return text_type(rset[row][col])
+                return str(rset[row][col])
         else:
             def index_display(row):
                 return rset.get_entity(row, col).view('text')
--- a/cubicweb/web/views/owl.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/owl.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,8 +21,6 @@
 
 from cubicweb import _
 
-from six.moves import range
-
 from logilab.mtconverter import TransformError, xml_escape
 
 from cubicweb.view import StartupView, EntityView
--- a/cubicweb/web/views/plots.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/plots.py	Fri Oct 18 23:39:03 2019 +0200
@@ -17,18 +17,10 @@
 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
 """basic plot views"""
 
-
-from cubicweb import _
-
-from six import add_metaclass
-from six.moves import range
-
-from logilab.common.date import datetime2ticks
-from logilab.common.deprecation import class_deprecated
 from logilab.common.registry import objectify_predicate
 from logilab.mtconverter import xml_escape
 
-from cubicweb.utils import UStringIO, json_dumps
+from cubicweb.utils import UStringIO
 from cubicweb.predicates import multi_columns_rset
 from cubicweb.web.views import baseviews
 
@@ -87,89 +79,6 @@
         raise NotImplementedError
 
 
-@add_metaclass(class_deprecated)
-class FlotPlotWidget(PlotWidget):
-    """PlotRenderer widget using Flot"""
-    __deprecation_warning__ = '[3.14] cubicweb.web.views.plots module is deprecated, use the jqplot cube instead'
-    onload = u"""
-var fig = jQuery('#%(figid)s');
-if (fig.attr('cubicweb:type') != 'prepared-plot') {
-    %(plotdefs)s
-    jQuery.plot(jQuery('#%(figid)s'), [%(plotdata)s],
-        {points: {show: true},
-         lines: {show: true},
-         grid: {hoverable: true},
-         /*yaxis : {tickFormatter : suffixFormatter},*/
-         xaxis: {mode: %(mode)s}});
-    jQuery('#%(figid)s').data({mode: %(mode)s, dateformat: %(dateformat)s});
-    jQuery('#%(figid)s').bind('plothover', onPlotHover);
-    fig.attr('cubicweb:type','prepared-plot');
-}
-"""
-
-    def __init__(self, labels, plots, timemode=False):
-        self.labels = labels
-        self.plots = plots # list of list of couples
-        self.timemode = timemode
-
-    def dump_plot(self, plot):
-        if self.timemode:
-            plot = [(datetime2ticks(x), y) for x, y in plot]
-        return json_dumps(plot)
-
-    def _render(self, req, width=500, height=400):
-        if req.ie_browser():
-            req.add_js('excanvas.js')
-        req.add_js(('jquery.flot.js', 'cubicweb.flot.js'))
-        figid = u'figure%s' % next(req.varmaker)
-        plotdefs = []
-        plotdata = []
-        self.w(u'<div id="%s" style="width: %spx; height: %spx;"></div>' %
-               (figid, width, height))
-        for idx, (label, plot) in enumerate(zip(self.labels, self.plots)):
-            plotid = '%s_%s' % (figid, idx)
-            plotdefs.append('var %s = %s;' % (plotid, self.dump_plot(plot)))
-            # XXX ugly but required in order to not crash my demo
-            plotdata.append("{label: '%s', data: %s}" % (label.replace(u'&', u''), plotid))
-        fmt = req.property_value('ui.date-format') # XXX datetime-format
-        # XXX TODO make plot options customizable
-        req.html_headers.add_onload(self.onload %
-                                    {'plotdefs': '\n'.join(plotdefs),
-                                     'figid': figid,
-                                     'plotdata': ','.join(plotdata),
-                                     'mode': self.timemode and "'time'" or 'null',
-                                     'dateformat': '"%s"' % fmt})
-
-
-@add_metaclass(class_deprecated)
-class PlotView(baseviews.AnyRsetView):
-    __deprecation_warning__ = '[3.14] cubicweb.web.views.plots module is deprecated, use the jqplot cube instead'
-    __regid__ = 'plot'
-    title = _('generic plot')
-    __select__ = multi_columns_rset() & all_columns_are_numbers()
-    timemode = False
-    paginable = False
-
-    def call(self, width=500, height=400):
-        # prepare data
-        rqlst = self.cw_rset.syntax_tree()
-        # XXX try to make it work with unions
-        varnames = [var.name for var in rqlst.children[0].get_selected_variables()][1:]
-        abscissa = [row[0] for row in self.cw_rset]
-        plots = []
-        nbcols = len(self.cw_rset.rows[0])
-        for col in range(1, nbcols):
-            data = [row[col] for row in self.cw_rset]
-            plots.append(filterout_nulls(abscissa, data))
-        plotwidget = FlotPlotWidget(varnames, plots, timemode=self.timemode)
-        plotwidget.render(self._cw, width, height, w=self.w)
-
-
-class TimeSeriePlotView(PlotView):
-    __select__ = multi_columns_rset() & columns_are_date_then_numbers()
-    timemode = True
-
-
 try:
     from GChartWrapper import Pie, Pie3D
 except ImportError:
--- a/cubicweb/web/views/primary.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/primary.py	Fri Oct 18 23:39:03 2019 +0200
@@ -40,9 +40,6 @@
 
 from cubicweb import _
 
-from warnings import warn
-
-from logilab.common.deprecation import deprecated
 from logilab.mtconverter import xml_escape
 
 from cubicweb import Unauthorized, NoSelectableObject
--- a/cubicweb/web/views/pyviews.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/pyviews.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,10 +18,6 @@
 """Basic views for python values (eg without any result set)
 """
 
-
-from six import text_type
-from six.moves import range
-
 from cubicweb.view import View
 from cubicweb.predicates import match_kwargs
 from cubicweb.web.views import tableview
@@ -41,7 +37,7 @@
             w(self.empty_cell_content)
 
     def render_cell(self, w, rownum):
-        w(text_type(self.data[rownum][self.colid]))
+        w(str(self.data[rownum][self.colid]))
 
 
 class PyValTableView(tableview.TableMixIn, View):
--- a/cubicweb/web/views/rdf.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/rdf.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 
 from cubicweb import _
 
-from six.moves import range
-
 from yams import xy
 
 from cubicweb.schema import VIRTUAL_RTYPES
--- a/cubicweb/web/views/reledit.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/reledit.py	Fri Oct 18 23:39:03 2019 +0200
@@ -23,10 +23,8 @@
 from cubicweb import _
 
 import copy
-from warnings import warn
 
 from logilab.mtconverter import xml_escape
-from logilab.common.deprecation import deprecated, class_renamed
 from logilab.common.decorators import cached
 
 from cubicweb import neg_role
@@ -390,9 +388,6 @@
         self._close_form_wrapper()
 
 
-ClickAndEditFormView = class_renamed('ClickAndEditFormView', AutoClickAndEditFormView)
-
-
 @ajaxfunc(output_type='xhtml')
 def reledit_form(self):
     req = self._cw
--- a/cubicweb/web/views/schema.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/schema.py	Fri Oct 18 23:39:03 2019 +0200
@@ -26,8 +26,6 @@
 import os, os.path as osp
 import codecs
 
-from six import text_type
-
 from logilab.common.graph import GraphGenerator, DotBackend
 from logilab.common.ureports import Section, Table
 from logilab.common.registry import yes
@@ -281,7 +279,7 @@
     def cell_call(self, row, col):
         defaultval = self.cw_rset.rows[row][col]
         if defaultval is not None:
-            self.w(text_type(self.cw_rset.rows[row][col].unzpickle()))
+            self.w(str(self.cw_rset.rows[row][col].unzpickle()))
 
 class CWETypeRelationCardinalityCell(baseviews.FinalView):
     __regid__ = 'etype-rel-cardinality-cell'
@@ -489,7 +487,7 @@
         entity = self.cw_rset.get_entity(row, col)
         rschema = self._cw.vreg.schema.rschema(entity.rtype.name)
         rdef = rschema.rdefs[(entity.stype.name, entity.otype.name)]
-        constraints = [xml_escape(text_type(c)) for c in getattr(rdef, 'constraints')]
+        constraints = [xml_escape(str(c)) for c in getattr(rdef, 'constraints')]
         self.w(u'<br/>'.join(constraints))
 
 class CWAttributeOptionsCell(EntityView):
--- a/cubicweb/web/views/sparql.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/sparql.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 
 from cubicweb import _
 
-from six.moves import range
-
 from yams import xy
 from rql import TypeResolverException
 
--- a/cubicweb/web/views/startup.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/startup.py	Fri Oct 18 23:39:03 2019 +0200
@@ -25,7 +25,6 @@
 from cubicweb import _
 
 from logilab.common.textutils import unormalize
-from logilab.common.deprecation import deprecated
 from logilab.mtconverter import xml_escape
 
 from cubicweb.view import StartupView
@@ -167,7 +166,3 @@
     """
     __regid__ = 'index'
     title = _('view_index')
-
-    @deprecated('[3.11] display_folders method is deprecated, backport it if needed')
-    def display_folders(self):
-        return 'Folder' in self._cw.vreg.schema and self._cw.execute('Any COUNT(X) WHERE X is Folder')[0][0]
--- a/cubicweb/web/views/tableview.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/tableview.py	Fri Oct 18 23:39:03 2019 +0200
@@ -63,13 +63,9 @@
 
 from cubicweb import _
 
-from warnings import warn
 from copy import copy
 from types import MethodType
 
-from six import string_types, add_metaclass, create_bound_method
-from six.moves import range
-
 from logilab.mtconverter import xml_escape
 from logilab.common.decorators import cachedproperty
 from logilab.common.deprecation import class_deprecated
@@ -287,7 +283,7 @@
         attrs = renderer.attributes.copy()
         if renderer.sortable:
             sortvalue = renderer.sortvalue(rownum)
-            if isinstance(sortvalue, string_types):
+            if isinstance(sortvalue, str):
                 sortvalue = sortvalue[:self.sortvalue_limit]
             if sortvalue is not None:
                 attrs[u'cubicweb:sortvalue'] = js_dumps(sortvalue)
@@ -605,7 +601,7 @@
         return self.__regid__ == 'table'
 
     def call(self, headers=None, displaycols=None, cellvids=None,
-             paginate=None, **kwargs):
+             paginate=None):
         if self.headers:
             self.headers = [h and self._cw._(h) for h in self.headers]
         if (headers or displaycols or cellvids or paginate):
@@ -617,15 +613,7 @@
                 self.cellvids = cellvids
             if paginate is not None:
                 self.paginable = paginate
-        if kwargs:
-            # old table view arguments that we can safely ignore thanks to
-            # selectors
-            if len(kwargs) > 1:
-                msg = '[3.14] %s arguments are deprecated' % ', '.join(kwargs)
-            else:
-                msg = '[3.14] %s argument is deprecated' % ', '.join(kwargs)
-            warn(msg, DeprecationWarning, stacklevel=2)
-        super(RsetTableView, self).call(**kwargs)
+        super(RsetTableView, self).call()
 
     def main_var_index(self):
         """returns the index of the first non-attribute variable among the RQL
@@ -726,7 +714,7 @@
             for aname, member in[('renderfunc', renderfunc),
                                  ('sortfunc', sortfunc)]:
                 if isinstance(member, MethodType):
-                    member = create_bound_method(member.__func__, acopy)
+                    member = MethodType(member.__func__, acopy)
                 setattr(acopy, aname, member)
             return acopy
         finally:
@@ -921,8 +909,7 @@
 ################################################################################
 
 
-@add_metaclass(class_deprecated)
-class TableView(AnyRsetView):
+class TableView(AnyRsetView, metaclass=class_deprecated):
     """The table view accepts any non-empty rset. It uses introspection on the
     result set to compute column names and the proper way to display the cells.
 
@@ -1187,8 +1174,7 @@
     title = _('editable-table')
 
 
-@add_metaclass(class_deprecated)
-class CellView(EntityView):
+class CellView(EntityView, metaclass=class_deprecated):
     __deprecation_warning__ = '[3.14] %(cls)s is deprecated'
     __regid__ = 'cell'
     __select__ = nonempty_rset()
@@ -1211,128 +1197,3 @@
         else:
             # XXX why do we need a fallback view here?
             self.wview(cellvid or 'final', self.cw_rset, 'null', row=row, col=col)
-
-
-class InitialTableView(TableView):
-    """same display as  table view but consider two rql queries :
-
-    * the default query (ie `rql` form parameter), which is only used to select
-      this view and to build the filter form. This query should have the same
-      structure as the actual without actual restriction (but link to
-      restriction variables) and usually with a limit for efficiency (limit set
-      to 2 is advised)
-
-    * the actual query (`actualrql` form parameter) whose results will be
-      displayed with default restrictions set
-    """
-    __regid__ = 'initialtable'
-    __select__ = nonempty_rset()
-    # should not be displayed in possible view since it expects some specific
-    # parameters
-    title = None
-
-    def call(self, title=None, subvid=None, headers=None, divid=None,
-             paginate=False, displaycols=None, displayactions=None,
-             mainindex=None):
-        """Dumps a table displaying a composite query"""
-        try:
-            actrql = self._cw.form['actualrql']
-        except KeyError:
-            actrql = self.cw_rset.printable_rql()
-        else:
-            self._cw.ensure_ro_rql(actrql)
-        displaycols = self.displaycols(displaycols, headers)
-        if displayactions is None and 'displayactions' in self._cw.form:
-            displayactions = True
-        if divid is None and 'divid' in self._cw.form:
-            divid = self._cw.form['divid']
-        self.w(u'<div class="section">')
-        if not title and 'title' in self._cw.form:
-            # pop title so it's not displayed by the table view as well
-            title = self._cw.form.pop('title')
-        if title:
-            self.w(u'<h2>%s</h2>\n' % title)
-        if mainindex is None:
-            mainindex = self.main_var_index()
-        if mainindex is not None:
-            actions = self.form_filter(divid, displaycols, displayactions,
-                                       displayfilter=True, paginate=paginate,
-                                       hidden=True)
-        else:
-            actions = ()
-        if not subvid and 'subvid' in self._cw.form:
-            subvid = self._cw.form.pop('subvid')
-        self._cw.view('table', self._cw.execute(actrql),
-                      'noresult', w=self.w, displayfilter=False, subvid=subvid,
-                      displayactions=displayactions, displaycols=displaycols,
-                      actions=actions, headers=headers, divid=divid)
-        self.w(u'</div>\n')
-
-
-class EditableInitialTableTableView(InitialTableView):
-    __regid__ = 'editable-initialtable'
-    finalview = 'editable-final'
-
-
-@add_metaclass(class_deprecated)
-class EntityAttributesTableView(EntityView):
-    """This table displays entity attributes in a table and allow to set a
-    specific method to help building cell content for each attribute as well as
-    column header.
-
-    Table will render entity cell by using the appropriate build_COLNAME_cell
-    methods if defined otherwise cell content will be entity.COLNAME.
-
-    Table will render column header using the method header_for_COLNAME if
-    defined otherwise COLNAME will be used.
-    """
-    __deprecation_warning__ = '[3.14] %(cls)s is deprecated'
-    __abstract__ = True
-    columns = ()
-    table_css = "listing"
-    css_files = ()
-
-    def call(self, columns=None):
-        if self.css_files:
-            self._cw.add_css(self.css_files)
-        _ = self._cw._
-        self.columns = columns or self.columns
-        sample = self.cw_rset.get_entity(0, 0)
-        self.w(u'<table class="%s">' % self.table_css)
-        self.table_header(sample)
-        self.w(u'<tbody>')
-        for row in range(self.cw_rset.rowcount):
-            self.cell_call(row=row, col=0)
-        self.w(u'</tbody>')
-        self.w(u'</table>')
-
-    def cell_call(self, row, col):
-        _ = self._cw._
-        entity = self.cw_rset.get_entity(row, col)
-        entity.complete()
-        infos = {}
-        for col in self.columns:
-            meth = getattr(self, 'build_%s_cell' % col, None)
-            # find the build method or try to find matching attribute
-            if meth:
-                content = meth(entity)
-            else:
-                content = entity.printable_value(col)
-            infos[col] = content
-        self.w(u"""<tr onmouseover="$(this).addClass('highlighted');"
-            onmouseout="$(this).removeClass('highlighted')">""")
-        line = u''.join(u'<td>%%(%s)s</td>' % col for col in self.columns)
-        self.w(line % infos)
-        self.w(u'</tr>\n')
-
-    def table_header(self, sample):
-        """builds the table's header"""
-        self.w(u'<thead><tr>')
-        for column in self.columns:
-            meth = getattr(self, 'header_for_%s' % column, None)
-            if meth:
-                colname = meth(sample)
-            else:
-                colname = self._cw._(column)
-            self.w(u'<th>%s</th>' % xml_escape(colname))
-        self.w(u'</tr></thead>\n')
--- a/cubicweb/web/views/tabs.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/tabs.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 
 from cubicweb import _
 
-from six import string_types
-
 from logilab.common.deprecation import class_renamed
 from logilab.mtconverter import xml_escape
 
@@ -116,7 +114,7 @@
         active_tab = uilib.domid(default_tab)
         viewsvreg = self._cw.vreg['views']
         for tab in tabs:
-            if isinstance(tab, string_types):
+            if isinstance(tab, str):
                 tabid, tabkwargs = tab, {}
             else:
                 tabid, tabkwargs = tab
--- a/cubicweb/web/views/timeline.py	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,34 +0,0 @@
-# copyright 2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-
-try:
-    from cubes.timeline.views import (
-            TimelineJsonView,
-            TimelineViewMixIn,
-            TimelineView,
-            StaticTimelineView)
-
-except ImportError:
-    pass
-else:
-    from logilab.common.deprecation import class_moved
-
-    TimelineJsonView = class_moved(TimelineJsonView, 'TimelineJsonView')
-    TimelineViewMixIn = class_moved(TimelineViewMixIn, 'TimelineViewMixIn')
-    TimelineView = class_moved(TimelineView, 'TimelineView')
-    StaticTimelineView = class_moved(StaticTimelineView, 'StaticTimelineView')
--- a/cubicweb/web/views/timetable.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/timetable.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 
 from cubicweb import _
 
-from six.moves import range
-
 from logilab.mtconverter import xml_escape
 from logilab.common.date import ONEDAY, date_range, todatetime
 
--- a/cubicweb/web/views/treeview.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/treeview.py	Fri Oct 18 23:39:03 2019 +0200
@@ -22,8 +22,6 @@
 
 from cubicweb import _
 
-from warnings import warn
-
 from logilab.mtconverter import xml_escape
 
 from cubicweb.utils import make_uid, json
@@ -150,11 +148,7 @@
 jQuery("#tree-%s").treeview({toggle: toggleTree, prerendered: true});""" % treeid)
 
     def call(self, subvid=None, treeid=None,
-             initial_load=True, initial_thru_ajax=None, **morekwargs):
-        if initial_thru_ajax is not None:
-            msg = '[3.24] initial_thru_ajax argument is deprecated'
-            warn(msg, DeprecationWarning, stacklevel=2)
-
+             initial_load=True, **morekwargs):
         subvid, treeid = self._init_params(subvid, treeid,
                                            initial_load, morekwargs)
         ulid = ' '
--- a/cubicweb/web/views/uicfg.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/uicfg.py	Fri Oct 18 23:39:03 2019 +0200
@@ -56,8 +56,6 @@
 
 from itertools import repeat
 
-from six import string_types
-
 from cubicweb import neg_role
 from cubicweb.rtags import (RelationTags, RelationTagsBool, RelationTagsSet,
                             RelationTagsDict, NoTargetRelationTagsDict,
@@ -692,7 +690,7 @@
                 self.tag_relation((sschema, rschema, oschema, role), True)
 
     def _tag_etype_attr(self, etype, attr, desttype='*', *args, **kwargs):
-        if isinstance(attr, string_types):
+        if isinstance(attr, str):
             attr, role = attr, 'subject'
         else:
             attr, role = attr
--- a/cubicweb/web/views/urlrewrite.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/urlrewrite.py	Fri Oct 18 23:39:03 2019 +0200
@@ -19,8 +19,6 @@
 
 import re
 
-from six import string_types, add_metaclass
-
 from cubicweb.uilib import domid
 from cubicweb.appobject import AppObject
 
@@ -53,8 +51,7 @@
         return super(metarewriter, mcs).__new__(mcs, name, bases, classdict)
 
 
-@add_metaclass(metarewriter)
-class URLRewriter(AppObject):
+class URLRewriter(AppObject, metaclass=metarewriter):
     """Base class for URL rewriters.
 
     Url rewriters should have a `rules` dict that maps an input URI
@@ -124,14 +121,14 @@
                 required_groups = None
             if required_groups and not req.user.matching_groups(required_groups):
                 continue
-            if isinstance(inputurl, string_types):
+            if isinstance(inputurl, str):
                 if inputurl == uri:
                     req.form.update(infos)
                     break
             elif inputurl.match(uri): # it's a regexp
                 # XXX what about i18n? (vtitle for instance)
                 for param, value in infos.items():
-                    if isinstance(value, string_types):
+                    if isinstance(value, str):
                         req.form[param] = inputurl.sub(value, uri)
                     else:
                         req.form[param] = value
@@ -224,7 +221,7 @@
                 required_groups = None
             if required_groups and not req.user.matching_groups(required_groups):
                 continue
-            if isinstance(inputurl, string_types):
+            if isinstance(inputurl, str):
                 if inputurl == uri:
                     return callback(inputurl, uri, req, self._cw.vreg.schema)
             elif inputurl.match(uri): # it's a regexp
--- a/cubicweb/web/views/wdoc.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/wdoc.py	Fri Oct 18 23:39:03 2019 +0200
@@ -26,8 +26,6 @@
 from os.path import join
 from xml.etree.ElementTree import parse
 
-from six import text_type
-
 from logilab.common.registry import yes
 
 from cubicweb.predicates import match_form_params
@@ -91,9 +89,9 @@
     for title in node.findall('title'):
         title_lang = title.attrib['{http://www.w3.org/XML/1998/namespace}lang']
         if title_lang == lang:
-            return text_type(title.text)
+            return title.text
         if title_lang == 'en':
-            fallback_title = text_type(title.text)
+            fallback_title = title.text
     return fallback_title
 
 
--- a/cubicweb/web/views/workflow.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/workflow.py	Fri Oct 18 23:39:03 2019 +0200
@@ -24,12 +24,7 @@
 
 from cubicweb import _
 
-import os
-
-from six import add_metaclass, text_type
-
 from logilab.mtconverter import xml_escape
-from logilab.common.deprecation import class_deprecated
 
 from cubicweb import Unauthorized
 from cubicweb.predicates import (one_line_rset,
@@ -38,7 +33,6 @@
 from cubicweb.view import EntityView
 from cubicweb.web import stdmsgs, action, component, form
 from cubicweb.web import formwidgets as fwdgs
-from cubicweb.web.views import TmpFileViewMixin
 from cubicweb.web.views import uicfg, forms, ibreadcrumbs
 from cubicweb.web.views.tabs import TabbedPrimaryView, PrimaryTab
 from cubicweb.web.views.dotgraphview import DotGraphView, DotPropsHandler
@@ -313,7 +307,7 @@
     wf = req.entity_from_eid(wfeid)
     rschema = req.vreg.schema[field.name]
     param = 'toeid' if field.role == 'subject' else 'fromeid'
-    return sorted((e.view('combobox'), text_type(e.eid))
+    return sorted((e.view('combobox'), str(e.eid))
                   for e in getattr(wf, 'reverse_%s' % wfrelation)
                   if rschema.has_perm(req, 'add', **{param: e.eid}))
 
@@ -444,24 +438,3 @@
 
     def build_dotpropshandler(self):
         return WorkflowDotPropsHandler(self._cw)
-
-
-@add_metaclass(class_deprecated)
-class TmpPngView(TmpFileViewMixin, EntityView):
-    __deprecation_warning__ = '[3.18] %(cls)s is deprecated'
-    __regid__ = 'tmppng'
-    __select__ = match_form_params('tmpfile')
-    content_type = 'image/png'
-    binary = True
-
-    def cell_call(self, row=0, col=0):
-        key = self._cw.form['tmpfile']
-        if key not in self._cw.session.data:
-            # the temp file is gone and there's nothing
-            # we can do about it
-            # we should probably write it to some well
-            # behaved place and serve it
-            return
-        tmpfile = self._cw.session.data.pop(key)
-        self.w(open(tmpfile, 'rb').read())
-        os.unlink(tmpfile)
--- a/cubicweb/web/views/xbel.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/xbel.py	Fri Oct 18 23:39:03 2019 +0200
@@ -20,8 +20,6 @@
 
 from cubicweb import _
 
-from six.moves import range
-
 from logilab.mtconverter import xml_escape
 
 from cubicweb.predicates import is_instance
--- a/cubicweb/web/views/xmlrss.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/views/xmlrss.py	Fri Oct 18 23:39:03 2019 +0200
@@ -22,8 +22,6 @@
 from base64 import b64encode
 from time import timezone
 
-from six.moves import range
-
 from logilab.mtconverter import xml_escape
 
 from cubicweb.predicates import (is_instance, non_final_entity, one_line_rset,
--- a/cubicweb/web/webconfig.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/webconfig.py	Fri Oct 18 23:39:03 2019 +0200
@@ -24,37 +24,32 @@
 import hmac
 from uuid import uuid4
 from os.path import dirname, join, exists, split, isdir
-from warnings import warn
-
-from six import text_type
 
 from logilab.common.decorators import cached, cachedproperty
-from logilab.common.deprecation import deprecated
-from logilab.common.configuration import merge_options
+from logilab.common.configuration import Method, merge_options
 
 from cubicweb import ConfigurationError
-from cubicweb.toolsutils import read_config
 from cubicweb.cwconfig import CubicWebConfiguration, register_persistent_options
 
 
 _DATA_DIR = join(dirname(__file__), 'data')
 
 
-register_persistent_options( (
+register_persistent_options((
     # site-wide only web ui configuration
     ('site-title',
-     {'type' : 'string', 'default': 'unset title',
+     {'type': 'string', 'default': 'unset title',
       'help': _('site title'),
       'sitewide': True, 'group': 'ui',
       }),
     ('main-template',
-     {'type' : 'string', 'default': 'main-template',
+     {'type': 'string', 'default': 'main-template',
       'help': _('id of main template used to render pages'),
       'sitewide': True, 'group': 'ui',
       }),
     # user web ui configuration
     ('fckeditor',
-     {'type' : 'yn', 'default': False,
+     {'type': 'yn', 'default': False,
       'help': _('should html fields being edited using fckeditor (a HTML '
                 'WYSIWYG editor).  You should also select text/html as default '
                 'text format to actually get fckeditor.'),
@@ -62,23 +57,23 @@
       }),
     # navigation configuration
     ('page-size',
-     {'type' : 'int', 'default': 40,
+     {'type': 'int', 'default': 40,
       'help': _('maximum number of objects displayed by page of results'),
       'group': 'navigation',
       }),
     ('related-limit',
-     {'type' : 'int', 'default': 8,
+     {'type': 'int', 'default': 8,
       'help': _('maximum number of related entities to display in the primary '
                 'view'),
       'group': 'navigation',
       }),
     ('combobox-limit',
-     {'type' : 'int', 'default': 20,
+     {'type': 'int', 'default': 20,
       'help': _('maximum number of entities to display in related combo box'),
       'group': 'navigation',
       }),
 
-    ))
+))
 
 
 class BaseWebConfiguration(CubicWebConfiguration):
@@ -88,7 +83,7 @@
 
     options = merge_options(CubicWebConfiguration.options + (
         ('repository-uri',
-         {'type' : 'string',
+         {'type': 'string',
           'default': 'inmemory://',
           'help': 'see `cubicweb.dbapi.connect` documentation for possible value',
           'group': 'web', 'level': 2,
@@ -99,26 +94,27 @@
           'group': 'ui', 'level': 2,
           }),
         ('anonymous-user',
-         {'type' : 'string',
+         {'type': 'string',
           'default': None,
-          'help': 'login of the CubicWeb user account to use for anonymous user (if you want to allow anonymous)',
+          'help': ('login of the CubicWeb user account to use for anonymous '
+                   'user (if you want to allow anonymous)'),
           'group': 'web', 'level': 1,
           }),
         ('anonymous-password',
-         {'type' : 'string',
+         {'type': 'string',
           'default': None,
           'help': 'password of the CubicWeb user account to use for anonymous user, '
           'if anonymous-user is set',
           'group': 'web', 'level': 1,
           }),
         ('query-log-file',
-         {'type' : 'string',
+         {'type': 'string',
           'default': None,
           'help': 'web instance query log file',
           'group': 'web', 'level': 3,
           }),
         ('cleanup-anonymous-session-time',
-         {'type' : 'time',
+         {'type': 'time',
           'default': '5min',
           'help': 'Same as cleanup-session-time but specific to anonymous '
           'sessions. You can have a much smaller timeout here since it will be '
@@ -134,10 +130,8 @@
         allowed or if an empty login is used in configuration
         """
         try:
-            user   = self['anonymous-user'] or None
+            user = self['anonymous-user'] or None
             passwd = self['anonymous-password']
-            if user:
-                user = text_type(user)
         except KeyError:
             user, passwd = None, None
         except UnicodeDecodeError:
@@ -145,7 +139,6 @@
         return user, passwd
 
 
-
 class WebConfiguration(BaseWebConfiguration):
     """the WebConfiguration is a singleton object handling instance's
     configuration and preferences
@@ -160,20 +153,20 @@
           'group': 'web',
           }),
         ('auth-mode',
-         {'type' : 'choice',
-          'choices' : ('cookie', 'http'),
+         {'type': 'choice',
+          'choices': ('cookie', 'http'),
           'default': 'cookie',
           'help': 'authentication mode (cookie / http)',
           'group': 'web', 'level': 3,
           }),
         ('realm',
-         {'type' : 'string',
+         {'type': 'string',
           'default': 'cubicweb',
           'help': 'realm to use on HTTP authentication mode',
           'group': 'web', 'level': 3,
           }),
         ('http-session-time',
-         {'type' : 'time',
+         {'type': 'time',
           'default': 0,
           'help': "duration of the cookie used to store session identifier. "
           "If 0, the cookie will expire when the user exist its browser. "
@@ -181,7 +174,7 @@
           'group': 'web', 'level': 2,
           }),
         ('submit-mail',
-         {'type' : 'string',
+         {'type': 'string',
           'default': None,
           'help': ('Mail used as recipient to report bug in this instance, '
                    'if you want this feature on'),
@@ -189,31 +182,32 @@
           }),
 
         ('language-mode',
-         {'type' : 'choice',
+         {'type': 'choice',
           'choices': ('http-negotiation', 'url-prefix', ''),
           'default': 'http-negotiation',
           'help': ('source for interface\'s language detection. '
                    'If set to "http-negotiation" the Accept-Language HTTP header will be used,'
-                   ' if set to "url-prefix", the URL will be inspected for a short language prefix.'),
+                   ' if set to "url-prefix", the URL will be inspected for a'
+                   ' short language prefix.'),
           'group': 'web', 'level': 2,
           }),
 
         ('print-traceback',
-         {'type' : 'yn',
+         {'type': 'yn',
           'default': CubicWebConfiguration.mode != 'system',
           'help': 'print the traceback on the error page when an error occurred',
           'group': 'web', 'level': 2,
           }),
 
         ('captcha-font-file',
-         {'type' : 'string',
+         {'type': 'string',
           'default': join(_DATA_DIR, 'porkys.ttf'),
           'help': 'True type font to use for captcha image generation (you \
 must have the python imaging library installed to use captcha)',
           'group': 'web', 'level': 3,
           }),
         ('captcha-font-size',
-         {'type' : 'int',
+         {'type': 'int',
           'default': 25,
           'help': 'Font size to use for captcha image generation (you must \
 have the python imaging library installed to use captcha)',
@@ -221,7 +215,7 @@
           }),
 
         ('concat-resources',
-         {'type' : 'yn',
+         {'type': 'yn',
           'default': False,
           'help': 'use modconcat-like URLS to concat and serve JS / CSS files',
           'group': 'web', 'level': 2,
@@ -245,36 +239,37 @@
           'group': 'web', 'level': 2,
           }),
         ('access-control-allow-origin',
-         {'type' : 'csv',
+         {'type': 'csv',
           'default': (),
-          'help':('comma-separated list of allowed origin domains or "*" for any domain'),
+          'help': ('comma-separated list of allowed origin domains or "*" for any domain'),
           'group': 'web', 'level': 2,
           }),
         ('access-control-allow-methods',
-         {'type' : 'csv',
+         {'type': 'csv',
           'default': (),
           'help': ('comma-separated list of allowed HTTP methods'),
           'group': 'web', 'level': 2,
           }),
         ('access-control-max-age',
-         {'type' : 'int',
+         {'type': 'int',
           'default': None,
           'help': ('maximum age of cross-origin resource sharing (in seconds)'),
           'group': 'web', 'level': 2,
           }),
         ('access-control-expose-headers',
-         {'type' : 'csv',
+         {'type': 'csv',
           'default': (),
-          'help':('comma-separated list of HTTP headers the application declare in response to a preflight request'),
+          'help': ('comma-separated list of HTTP headers the application '
+                   'declare in response to a preflight request'),
           'group': 'web', 'level': 2,
           }),
         ('access-control-allow-headers',
-         {'type' : 'csv',
+         {'type': 'csv',
           'default': (),
-          'help':('comma-separated list of HTTP headers the application may set in the response'),
+          'help': ('comma-separated list of HTTP headers the application may set in the response'),
           'group': 'web', 'level': 2,
           }),
-        ))
+    ))
 
     def __init__(self, *args, **kwargs):
         super(WebConfiguration, self).__init__(*args, **kwargs)
@@ -292,10 +287,6 @@
                 continue
             yield key, pdef
 
-    @deprecated('[3.22] call req.cnx.repo.get_versions() directly')
-    def vc_config(self):
-        return self.repository().get_versions()
-
     @cachedproperty
     def _instance_salt(self):
         """This random key/salt is used to sign content to be sent back by
@@ -306,12 +297,13 @@
     def sign_text(self, text):
         """sign some text for later checking"""
         # hmac.new expect bytes
-        if isinstance(text, text_type):
+        if isinstance(text, str):
             text = text.encode('utf-8')
         # replace \r\n so we do not depend on whether a browser "reencode"
         # original message using \r\n or not
         return hmac.new(self._instance_salt,
-                        text.strip().replace(b'\r\n', b'\n')).hexdigest()
+                        text.strip().replace(b'\r\n', b'\n'),
+                        digestmod="sha3_512").hexdigest()
 
     def check_text_sign(self, text, signature):
         """check the text signature is equal to the given signature"""
@@ -343,14 +335,9 @@
         if directory is None:
             return None, None
         if self['use-uicache'] and rdirectory == 'data' and rid.endswith('.css'):
-            if rid == 'cubicweb.old.css':
-                # @import('cubicweb.css') in css
-                warn('[3.20] cubicweb.old.css has been renamed back to cubicweb.css',
-                     DeprecationWarning)
-                rid = 'cubicweb.css'
             return self.ensure_uid_directory(
-                        self.uiprops.process_resource(
-                             join(directory, rdirectory), rid)), rid
+                self.uiprops.process_resource(
+                    join(directory, rdirectory), rid)), rid
         return join(directory, rdirectory), rid
 
     def locate_all_files(self, rid, rdirectory='wdoc'):
@@ -408,14 +395,6 @@
             self._load_ui_properties_file(uiprops, path)
         self._load_ui_properties_file(uiprops, self.apphome)
         datadir_url = uiprops.context['datadir_url']
-        if (datadir_url+'/cubicweb.old.css') in uiprops['STYLESHEETS']:
-            warn('[3.20] cubicweb.old.css has been renamed back to cubicweb.css',
-                 DeprecationWarning)
-            idx = uiprops['STYLESHEETS'].index(datadir_url+'/cubicweb.old.css')
-            uiprops['STYLESHEETS'][idx] = datadir_url+'/cubicweb.css'
-        if datadir_url+'/cubicweb.reset.css' in uiprops['STYLESHEETS']:
-            warn('[3.20] cubicweb.reset.css is obsolete', DeprecationWarning)
-            uiprops['STYLESHEETS'].remove(datadir_url+'/cubicweb.reset.css')
         cubicweb_js_url = datadir_url + '/cubicweb.js'
         if cubicweb_js_url not in uiprops['JAVASCRIPTS']:
             uiprops['JAVASCRIPTS'].insert(0, cubicweb_js_url)
@@ -453,3 +432,39 @@
     def static_file_del(self, rpath):
         if self.static_file_exists(rpath):
             os.remove(join(self.static_directory, rpath))
+
+
+class WebConfigurationBase(WebConfiguration):
+    """web instance (in a web server) client of a RQL server"""
+
+    options = merge_options((
+        # ctl configuration
+        ('port',
+         {'type': 'int',
+          'default': None,
+          'help': 'http server port number (default to 8080)',
+          'group': 'web', 'level': 0,
+          }),
+        ('interface',
+         {'type': 'string',
+          'default': '0.0.0.0',
+          'help': 'http server address on which to listen (default to everywhere)',
+          'group': 'web', 'level': 1,
+          }),
+        ('max-post-length',  # XXX specific to "wsgi" server
+         {'type': 'bytes',
+          'default': '100MB',
+          'help': 'maximum length of HTTP request. Default to 100 MB.',
+          'group': 'web', 'level': 1,
+          }),
+        ('pid-file',
+         {'type': 'string',
+          'default': Method('default_pid_file'),
+          'help': 'repository\'s pid file',
+          'group': 'main', 'level': 2,
+          }),
+    ) + WebConfiguration.options)
+
+    def default_base_url(self):
+        from socket import getfqdn
+        return 'http://%s:%s/' % (self['host'] or getfqdn().lower(), self['port'] or 8080)
--- a/cubicweb/web/webctl.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/web/webctl.py	Fri Oct 18 23:39:03 2019 +0200
@@ -18,7 +18,6 @@
 """cubicweb-ctl commands and command handlers common to twisted/modpython
 web configuration
 """
-from __future__ import print_function
 
 import os
 import os.path as osp
--- a/cubicweb/wfutils.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/wfutils.py	Fri Oct 18 23:39:03 2019 +0200
@@ -45,8 +45,6 @@
 
 import collections
 
-from six import text_type
-
 from cubicweb import NoResultError
 
 
@@ -91,7 +89,6 @@
                     by calling :func:`cleanupworkflow`.
     :return: The created/updated workflow entity
     """
-    name = text_type(name)
     try:
         wf = cnx.find('Workflow', name=name).one()
     except NoResultError:
--- a/cubicweb/wsgi/__init__.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/wsgi/__init__.py	Fri Oct 18 23:39:03 2019 +0200
@@ -27,9 +27,9 @@
 
 
 from email import message, message_from_string
+from http.cookies import SimpleCookie
 from pprint import pformat as _pformat
 
-from six.moves.http_cookies import SimpleCookie
 
 def pformat(obj):
     """pretty prints `obj` if possible"""
--- a/cubicweb/wsgi/handler.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/wsgi/handler.py	Fri Oct 18 23:39:03 2019 +0200
@@ -21,8 +21,6 @@
 
 from itertools import chain, repeat
 
-from six.moves import zip
-
 from cubicweb import AuthenticationError
 from cubicweb.web import DirectResponse
 from cubicweb.web.application import CubicWebPublisher
--- a/cubicweb/wsgi/request.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/cubicweb/wsgi/request.py	Fri Oct 18 23:39:03 2019 +0200
@@ -28,8 +28,7 @@
 import tempfile
 
 from io import BytesIO
-
-from six.moves.urllib.parse import parse_qs
+from urllib.parse import parse_qs
 
 from cubicweb.multipart import (
     copy_file, parse_form_data, parse_options_header)
@@ -88,7 +87,7 @@
         # robust against potentially malformed input.
         form = pformat(self.form)
         meta = pformat(self.environ)
-        return '<CubicWebWsgiRequest\FORM:%s,\nMETA:%s>' % \
+        return '<CubicWebWsgiRequest\nFORM:%s,\nMETA:%s>' % \
             (form, meta)
 
     ## cubicweb request interface ################################################
--- a/debian/changelog	Tue Aug 27 20:16:01 2019 +0200
+++ b/debian/changelog	Fri Oct 18 23:39:03 2019 +0200
@@ -1,3 +1,40 @@
+cubicweb (3.27.0~a2-1) unstable; urgency=medium
+
+  * New upstream pre-release.
+  * Cleanup d/source/options from now gone symlinks.
+
+ -- Denis Laxalde <denis.laxalde@logilab.fr>  Thu, 01 Aug 2019 09:13:26 +0200
+
+cubicweb (3.27.0~a1-1) unstable; urgency=medium
+
+  * New upstream pre-release.
+
+ -- Denis Laxalde <denis.laxalde@logilab.fr>  Wed, 24 Jul 2019 16:52:31 +0200
+
+cubicweb (3.27.0~a0-1) unstable; urgency=medium
+
+  [ Jérémy Bobbio ]
+  * Switch all Debian packages to Python 3.
+  * Switch to Debian source format 3.0 (quilt).
+  * Run test suite as part of autopkgtest.
+  * Remove build dependency on obsolete dh-systemd.
+  * Specify priority “optional” instead of the obsolete “extra”.
+  * Tidy substvars usage in control file.
+  * Stop using an empty (and obsolete) /etc/bash_completion.d with
+    cubicweb-ctl.
+  * Move lintian-overrides file to debian/source directory.
+  * Use “dependency package” instead of “virtual package” to describe
+    python3-cubicweb-postgresql-support.
+  * Fix doc-base section.
+  * Fix spelling mistake in doc-base abstract.
+  * Remove transitional packages.
+
+  [ Denis Laxalde ]
+  * Let cubicweb-ctl replace cubicweb-ctl3
+  * New upstream pre-release.
+
+ -- Denis Laxalde <denis.laxalde@logilab.fr>  Wed, 24 Jul 2019 16:08:43 +0200
+
 cubicweb (3.26.14-1) unstable; urgency=medium
 
   * New upstream release.
--- a/debian/control	Tue Aug 27 20:16:01 2019 +0200
+++ b/debian/control	Fri Oct 18 23:39:03 2019 +0200
@@ -7,73 +7,62 @@
 Build-Depends:
  debhelper (>= 9.20160709),
  dh-python,
- python-all,
- python-setuptools,
- python-sphinx,
  python3-all,
  python3-setuptools,
+ python3-docutils,
  python3-sphinx,
-Standards-Version: 3.9.6
+ python3-logilab-common (>= 1.4.0),
+ python3-logilab-mtconverter,
+ python3-markdown,
+ python3-tz,
+ python3-rql (>= 0.34.0),
+ python3-yams (>= 0.45.0),
+ python3-lxml,
+ python3-setuptools,
+ python3-pyramid,
+ python3-pyramid-multiauth,
+ python3-waitress,
+ python3-passlib,
+ python3-repoze.lru,
+ python3-wsgicors,
+ python3-filelock,
+ python3-pycryptodome,
+ sphinx-common,
+Standards-Version: 4.3.0
 Homepage: https://www.cubicweb.org
-X-Python-Version: >= 2.7
 X-Python3-Version: >= 3.4
 
 
-Package: python-cubicweb
+Package: python3-cubicweb
 Architecture: all
 Section: python
 Depends:
  ${misc:Depends},
- ${python:Depends},
- python-six (>= 1.4.0),
- python-logilab-mtconverter (>= 0.8.0),
- python-logilab-common (>= 1.4.0),
- python-logilab-database (>= 1.15.0),
- python-yams (>= 0.45.0),
- python-rql (>= 0.34.0),
- python-unittest2 (>= 0.7.0),
- python-lxml,
- python-markdown,
- python-passlib,
- python-tz,
+ ${python3:Depends},
  graphviz,
  gettext,
 Recommends:
+ ${python3:Recommends},
  cubicweb-ctl (= ${source:Version}),
- python-cubicweb-postgresql-support (= ${source:Version})
+ python3-cubicweb-postgresql-support (= ${source:Version})
  | sqlite3,
- python-cubicweb-pyramid (= ${source:Version}),
-# common recommends
- python-simpletal (>= 4.0),
- python-crypto,
-# web recommends (mostly)
- python-docutils (>= 0.6),
- python-vobject,
  fckeditor,
- python-fyzz,
- python-imaging,
- python-rdflib,
- python-werkzeug,
-# dev recommends
- python-pysqlite2,
 Suggests:
- python-zmq,
- python-cwclientlib (>= 0.4.0),
- python-cubicweb-twisted (= ${source:Version}),
- python-cubicweb-documentation (= ${source:Version}),
+ ${python3:Suggests},
+ python3-cwclientlib (>= 0.4.0),
+ python3-cubicweb-documentation (= ${source:Version}),
  w3c-dtd-xhtml,
  xvfb,
+ python3-pyramid-debugtoolbar,
 Replaces:
  cubicweb (<< 3.24.0-1~),
  cubicweb-server (<< 3.24.0-1~),
- cubicweb-twisted (<< 3.24.0-1~),
  cubicweb-web (<< 3.24.0-1~),
  cubicweb-core,
  cubicweb-common (<< 3.24.0-1~),
 Breaks:
  cubicweb (<< 3.24.0-1~),
  cubicweb-server (<< 3.24.0-1~),
- cubicweb-twisted (<< 3.24.0-1~),
  cubicweb-inlinedit (<< 1.1.1),
  cubicweb-bootstrap (<< 0.6.6),
  cubicweb-folder (<< 1.10.0),
@@ -97,7 +86,7 @@
 Conflicts:
  cubicweb-multisources,
  cubicweb-core,
-Description: CubicWeb framework (Python 2)
+Description: CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
  This metapackage will install all the components you need to run cubicweb on a
@@ -106,58 +95,7 @@
  packages on the different hosts.
 
 
-Package: python3-cubicweb
-Architecture: all
-Section: python
-Depends:
- ${misc:Depends},
- ${python3:Depends},
- python3-six (>= 1.4.0),
- python3-logilab-mtconverter (>= 0.8.0),
- python3-logilab-common (>= 1.4.0),
- python3-logilab-database (>= 1.15.0),
- python3-yams (>= 0.45.0),
- python3-rql (>= 0.34.0),
- python3-unittest2 (>= 0.7.0),
- python3-lxml,
- python3-markdown,
- python3-passlib,
- python3-tz,
- graphviz,
- gettext,
-Recommends:
- cubicweb-ctl3 (= ${source:Version}),
- python3-cubicweb-postgresql-support (= ${source:Version})
- | sqlite3,
- python3-cubicweb-pyramid (= ${source:Version}),
-# common recommends
- python3-simpletal (>= 4.0),
- python3-crypto,
-# web recommends (mostly)
- python3-docutils (>= 0.6),
- python3-vobject,
- fckeditor,
- python3-fyzz,
- python3-imaging,
- python3-rdflib,
- python3-werkzeug,
-# dev recommends
- python3-pysqlite2,
-Suggests:
- python3-zmq,
- python3-cwclientlib (>= 0.4.0),
- w3c-dtd-xhtml,
- xvfb,
-Description: CubicWeb framework (Python 3)
- CubicWeb is a semantic web application framework.
- .
- This package will install all the components you need to run cubicweb on a
- single machine. You can also deploy cubicweb by running the different process
- on different computers, in which case you need to install the corresponding
- packages on the different hosts.
-
-
-Package: python-cubicweb-postgresql-support
+Package: python3-cubicweb-postgresql-support
 Architecture: all
 Section: python
 # postgresql-client packages for backup/restore of non local database
@@ -166,92 +104,25 @@
 Provides: cubicweb-postgresql-support
 Depends:
  ${misc:Depends},
- python-cubicweb (= ${source:Version}),
- python-psycopg2,
- postgresql-client
-Description: postgres support for the CubicWeb framework (Python 2)
- CubicWeb is a semantic web application framework.
- .
- This is a dependency package to add support for using PostgreSQL for the
- cubicweb repository.
-
-
-Package: python3-cubicweb-postgresql-support
-Architecture: all
-Section: python
-# postgresql-client packages for backup/restore of non local database
-Depends:
- ${misc:Depends},
  python3-cubicweb (= ${source:Version}),
  python3-psycopg2,
  postgresql-client
-Description: postgres support for the CubicWeb framework (Python 3)
- CubicWeb is a semantic web application framework.
- .
- This is a dependency package to add support for using PostgreSQL for the
- cubicweb repository.
-
-
-Package: python-cubicweb-twisted
-Architecture: all
-Section: python
-Depends:
- ${misc:Depends},
- python-cubicweb (= ${source:Version}),
- python-twisted-web (<< 16.0.0),
-Description: meta package to use Twisted as HTTP server for CubicWeb
+Description: postgres support for the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
- This package includes dependencies to run a Twisted based HTTP server to serve
- the adaptative web interface.
-
-
-Package: python-cubicweb-pyramid
-Architecture: all
-Section: python
-Depends:
- ${misc:Depends},
- python-cubicweb (= ${source:Version}),
- python-pyramid (>= 1.5.0),
- python-pyramid-multiauth,
- python-waitress (>= 0.8.9),
- python-wsgicors,
- python-repoze.lru,
-Recommends:
- python-pyramid-debugtoolbar
-Conflicts:
- pyramid-cubicweb
-Replaces:
- pyramid-cubicweb
-Description: meta package to use Pyramid as HTTP server for CubicWeb
- Provides pyramid extensions to load a CubicWeb instance and serve it through
- the pyramid stack.
-
-Package: python3-cubicweb-pyramid
-Architecture: all
-Section: python
-Depends:
- ${misc:Depends},
- python3-cubicweb (= ${source:Version}),
- python3-pyramid (>= 1.5.0),
- python3-pyramid-multiauth,
- python3-waitress (>= 0.8.9),
- python3-wsgicors,
- python3-repoze.lru,
-Recommends:
- python3-pyramid-debugtoolbar
-Description: meta package to use Pyramid as HTTP server for CubicWeb (Python 3)
- Provides pyramid extensions to load a CubicWeb instance and serve it through
- the pyramid stack.
+ This dependency package provides dependencies to use PostgreSQL for the
+ cubicweb repository.
 
 
 Package: cubicweb-ctl
 Architecture: all
 Depends:
  ${misc:Depends},
- python-cubicweb (= ${source:Version})
+ python3-cubicweb (= ${source:Version})
 Conflicts:
  cubicweb-ctl3,
+Replaces:
+ cubicweb-ctl3 (<< 3.27.0),
 Description: tool to manage the CubicWeb framework
  CubicWeb is a semantic web application framework.
  .
@@ -259,26 +130,13 @@
  CubicWeb applications.
 
 
-Package: cubicweb-ctl3
-Architecture: all
-Depends:
- ${misc:Depends},
- python3-cubicweb (= ${source:Version})
-Conflicts:
- cubicweb-ctl,
-Description: tool to manage the CubicWeb framework
- CubicWeb is a semantic web application framework.
- .
- This package provides a control script to manage (e.g. create, upgrade)
- CubicWeb applications.
-
-
-Package: python-cubicweb-documentation
+Package: python3-cubicweb-documentation
 Architecture: all
 Section: doc
 Replaces: cubicweb-documentation (<< 3.24.0-1~)
 Breaks: cubicweb-documentation (<< 3.24.0-1~)
 Provides: cubicweb-documentation
+Conflicts: python-cubicweb-documentation
 Depends:
  ${misc:Depends},
  ${sphinxdoc:Depends},
@@ -289,85 +147,3 @@
  CubicWeb is a semantic web application framework.
  .
  This package provides the system's documentation.
-
-
-# Transitional packages after renaming of (most) binary packages
-
-Package: cubicweb
-Architecture: all
-Priority: optional
-Section: oldlibs
-Depends:
- python-cubicweb, ${misc:Depends}
-Description: transitional package
-  This is a transitional package. It can safely be removed.
-
-
-Package: cubicweb-server
-Architecture: all
-Priority: optional
-Section: oldlibs
-Depends:
- python-cubicweb, ${misc:Depends}
-Description: transitional package
-  This is a transitional package. It can safely be removed.
-
-
-Package: cubicweb-postgresql-support
-Architecture: all
-Priority: optional
-Section: oldlibs
-Depends:
- python-cubicweb-postgresql-support, ${misc:Depends}
-Description: transitional package
-  This is a transitional package. It can safely be removed.
-
-
-Package: cubicweb-twisted
-Architecture: all
-Priority: extra
-Section: oldlibs
-Depends:
- python-cubicweb-twisted, ${misc:Depends}
-Description: transitional package
-  This is a transitional package. It can safely be removed.
-
-
-Package: cubicweb-web
-Architecture: all
-Priority: optional
-Section: oldlibs
-Depends:
- python-cubicweb, ${misc:Depends}
-Description: transitional package
-  This is a transitional package. It can safely be removed.
-
-
-Package: cubicweb-common
-Architecture: all
-Priority: optional
-Section: oldlibs
-Depends:
- python-cubicweb, ${misc:Depends}
-Description: transitional package
-  This is a transitional package. It can safely be removed.
-
-
-Package: cubicweb-dev
-Architecture: all
-Priority: optional
-Section: oldlibs
-Depends:
- python-cubicweb, ${misc:Depends}
-Description: transitional package
-  This is a transitional package. It can safely be removed.
-
-
-Package: cubicweb-documentation
-Architecture: all
-Priority: optional
-Section: oldlibs
-Depends:
- python-cubicweb-documentation, ${misc:Depends}
-Description: transitional package
-  This is a transitional package. It can safely be removed.
--- a/debian/cubicweb-ctl3.dirs	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-etc/init.d
-etc/cubicweb.d
-usr/bin
-var/log/cubicweb
-var/lib/cubicweb/backup
-var/lib/cubicweb/instances
--- a/debian/cubicweb-ctl3.manpages	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-man/cubicweb-ctl.1
--- a/debian/cubicweb-ctl3.postrm	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-#!/bin/sh -e
-
-if [ "$1" = "purge" ] ; then
-        rm -rf /etc/cubicweb.d/
-        rm -rf /var/log/cubicweb/
-        rm -rf /var/lib/cubicweb/
-fi
-
-#DEBHELPER#
- 
-exit 0
--- a/debian/python-cubicweb-documentation.doc-base	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-Document: cubicweb-doc
-Title: CubicWeb Documentation
-Author: Logilab
-Abstract: Some base documentation for CubicWeb users and developers
-Section: Programming
-
-Format: HTML
-Index: /usr/share/doc/python-cubicweb-documentation/html/index.html
-Files: /usr/share/doc/python-cubicweb-documentation/html/*
--- a/debian/python-cubicweb-documentation.docs	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-debian/cubicweb-doc/html 
-doc/book
--- a/debian/python-cubicweb.lintian-overrides	Tue Aug 27 20:16:01 2019 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,2 +0,0 @@
-missing-dep-for-interpreter make => make | build-essential | dpkg-dev (usr/*/cubicweb/skeleton/debian/rules)
-embedded-javascript-library usr/share/cubicweb/cubes/shared/data/jquery.js
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/python3-cubicweb-documentation.doc-base	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,9 @@
+Document: cubicweb-doc
+Title: CubicWeb Documentation
+Author: Logilab
+Abstract: Some base documentation for CubicWeb users and developers
+Section: Programming
+
+Format: HTML
+Index: /usr/share/doc/python3-cubicweb-documentation/html/index.html
+Files: /usr/share/doc/python3-cubicweb-documentation/html/*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/python3-cubicweb-documentation.docs	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,2 @@
+debian/cubicweb-doc/html 
+doc/book
--- a/debian/rules	Tue Aug 27 20:16:01 2019 +0200
+++ b/debian/rules	Fri Oct 18 23:39:03 2019 +0200
@@ -6,11 +6,10 @@
 # export DH_VERBOSE=1
 
 export PYBUILD_NAME=cubicweb
-export PYBUILD_DISABLE_python2=test
 export PYBUILD_DISABLE_python3=test
 
 %:
-	dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild
+	dh $@ --with python3,sphinxdoc --buildsystem=pybuild
 
 override_dh_auto_build: export http_proxy=127.0.0.1:9
 override_dh_auto_build: export https_proxy=127.0.0.1:9
@@ -20,12 +19,36 @@
 	PYTHONPATH=. sphinx-build -N -bhtml doc/ debian/cubicweb-doc/html
 endif
 
+override_dh_auto_install:
+	dh_auto_install
+	mv debian/python3-${PYBUILD_NAME}/usr/bin/cubicweb-ctl \
+		debian/cubicweb-ctl/usr/bin/cubicweb-ctl
+
 override_dh_installchangelogs:
 	dh_installchangelogs -Xdoc/changes
 
-override_dh_auto_install:
-	dh_auto_install
-	mkdir -p debian/cubicweb-ctl/usr/bin
-	mv debian/python-cubicweb/usr/bin/cubicweb-ctl debian/cubicweb-ctl/usr/bin
-	mkdir -p debian/cubicweb-ctl3/usr/bin
-	mv debian/python3-cubicweb/usr/bin/cubicweb-ctl debian/cubicweb-ctl3/usr/bin
+# Should extra sections in requires.txt go to Recommends, Suggests or be
+# ignored?
+#
+# All sections must be listed so we don't forget any in cases of future
+# changes.
+
+RECOMMENDS_SECTIONS = ext crypto ical pyramid rdf
+SUGGESTS_SECTIONS = captcha zmq
+# sparql currently requires fyzz which is not compatible with Python 3
+IGNORED_SECTIONS = sparql
+
+override_dh_python3:
+	@set -e && trap 'rm -f requires-sections debian-sections' EXIT && \
+		sed -n -e 's/\[\(.*\)\]/\1/p' cubicweb.egg-info/requires.txt | sort > requires-sections && \
+		printf "%s\n" $(RECOMMENDS_SECTIONS) $(SUGGESTS_SECTIONS) $(IGNORED_SECTIONS) | sort > debian-sections && \
+		FORGOTTEN_SECTIONS=$$(comm -23 requires-sections debian-sections) && \
+		if [ "$$FORGOTTEN_SECTIONS" ]; then \
+			echo "The following sections are not listed in debian/rules:" && \
+			echo "$$FORGOTTEN_SECTIONS" && \
+			echo "Please add them in either RECOMMENDS_SECTIONS, SUGGESTS_SECTIONS or IGNORED_SECTIONS" && \
+			exit 1; \
+		fi
+	dh_python3 \
+		$(foreach section,$(RECOMMENDS_SECTIONS),--recommends-section=$(section)) \
+		$(foreach section,$(SUGGESTS_SECTIONS),--suggests-section=$(section))
--- a/debian/source/options	Tue Aug 27 20:16:01 2019 +0200
+++ b/debian/source/options	Fri Oct 18 23:39:03 2019 +0200
@@ -1,1 +1,1 @@
-extend-diff-ignore = "^(__pkginfo__\.py$|doc/_themes/cubicweb/static/(logo-cubicweb-small\.svg|cubicweb\.ico|logo-cubicweb\.svg)$|cubicweb/(sobjects/test/data/(cubicweb_comment|cubicweb_card)|server/test/data-migractions/(migratedapp/)?(cubicweb_comment|cubicweb_localperms|cubicweb_basket|cubicweb_tag|cubicweb_card|cubicweb_file)|test/data-rewrite/(cubicweb_localperms|cubicweb_card))$|cubicweb\.spec$|setup\.cfg$|cubicweb/misc/cwfs)"
+extend-diff-ignore = "^(__pkginfo__\.py$|doc/_themes/cubicweb/static/(logo-cubicweb-small\.svg|cubicweb\.ico|logo-cubicweb\.svg)$|cubicweb\.spec$|setup\.cfg$|cubicweb/misc/cwfs)"
--- a/debian/tests/control	Tue Aug 27 20:16:01 2019 +0200
+++ b/debian/tests/control	Fri Oct 18 23:39:03 2019 +0200
@@ -1,6 +1,16 @@
+Tests: unittest
+Depends:
+ python3-cubicweb, cubicweb-ctl,
+ @builddeps@,
+ python3-pytest,
+ python3-flake8,
+ python3-psycopg2, postgresql, postgresql-plpython,
+ python3-ldap3, slapd, ldap-utils
+Restrictions: allow-stderr, isolation-container
+
 Tests: skeleton-packaging
 Depends:
- python-cubicweb, cubicweb-ctl,
- python-pyramid, python-wsgicors,
+ python3-cubicweb, cubicweb-ctl,
+ python3-pyramid, python3-wsgicors,
  devscripts, equivs, lintian, autopkgtest
 Restrictions: allow-stderr, needs-root
--- a/debian/tests/skeleton-packaging	Tue Aug 27 20:16:01 2019 +0200
+++ b/debian/tests/skeleton-packaging	Fri Oct 18 23:39:03 2019 +0200
@@ -24,11 +24,11 @@
 cubicweb-ctl newcube -s 'Just a test cube' ${PACKAGE#cubicweb-}
 cd "$PACKAGE"
 
-UPSTREAM_VERSION=$(python setup.py --version)
+UPSTREAM_VERSION=$(python3 setup.py --version)
 DEBIAN_VERSION=$(dpkg-parsechangelog -S Version)
 
 # Create source tarball
-python setup.py sdist
+python3 setup.py sdist
 mv "dist/${PACKAGE}-${UPSTREAM_VERSION}.tar.gz" "../${PACKAGE}_${UPSTREAM_VERSION}.orig.tar.gz"
 
 # Install build-dependencies
@@ -50,9 +50,6 @@
 lintian -i ../*.dsc ../*.changes
 
 # Test if Python module is usable
-python -c 'import cubicweb_mytest; print(dir(cubicweb_mytest))'
-
-# Test if Python3 module is usable
 python3 -c 'import cubicweb_mytest; print(dir(cubicweb_mytest))'
 
 # Run autopkgtest (uh… inception, anyone?)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/debian/tests/unittest	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+set -e
+set -x
+
+### Setup tests
+
+find cubicweb -type d -name 'test' -a '!' -wholename 'cubicweb/skeleton/*' | while read dir; do
+	mkdir -p "$AUTOPKGTEST_TMP"/$(dirname "$dir")
+	cp -r "$dir" "$AUTOPKGTEST_TMP/$dir"
+	cp tox.ini "$AUTOPKGTEST_TMP"
+done
+chown -R nobody:nogroup "$AUTOPKGTEST_TMP"
+
+### Find PostgreSQL binaries
+
+POSTGRESQL_BINDIR=$(find /usr/lib/postgresql -type f -name 'initdb' -printf "%h\n" | head -n 1)
+test "$POSTGRESQL_BINDIR" || { echo "Unable to find 'initdb'" >&2; exit 1; }
+
+### Run tests
+
+for py in $(py3versions -r 2>/dev/null); do
+	cd "$AUTOPKGTEST_TMP"
+	echo "Testing with $py:"
+	su nobody --shell /bin/sh \
+		-c "env PATH='$PATH:$POSTGRESQL_BINDIR' $py -m pytest -v"
+done
--- a/doc/book/admin/create-instance.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/admin/create-instance.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -41,7 +41,7 @@
 located in :file:`~/etc/cubicweb.d/myinstance/*`. To launch it, you
 just type ::
 
-  cubicweb-ctl start -D myinstance
+  cubicweb-ctl pyramid -D myinstance
 
 The option `-D` specifies the *debug mode* : the instance is not
 running in server mode and does not disconnect from the terminal,
@@ -60,9 +60,9 @@
 
 .. note::
 
-  The output of `cubicweb-ctl start -D myinstance` can be
+  The output of `cubicweb-ctl pyramid -D myinstance` can be
   overwhelming. It is possible to reduce the log level with the
-  `--loglevel` parameter as in `cubicweb-ctl start -D myinstance -l
+  `--loglevel` parameter as in `cubicweb-ctl pyramid -D myinstance -l
   info` to filter out all logs under `info` gravity.
 
 upgrade
--- a/doc/book/admin/cubicweb-ctl.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/admin/cubicweb-ctl.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -65,21 +65,19 @@
 * ``db-init``, initializes the system database of an instance
   (schema, groups, users, workflows...)
 
-Commands to control instances
------------------------------
+Run an instance
+---------------
 
-* ``start``, starts one or more or all instances
+To start an instance during development, use ::
 
-of special interest::
+   cubicweb-ctl pyramid [-D] [-l <log-level>] <instance-id>
 
-  start -D
+without ``-D``, the instance will be start in the background, as a daemon.
 
-will start in debug mode (under windows, starting without -D will not
-work; you need instead to setup your instance as a service).
+See :ref:`cubicweb-ctl_pyramid` for more details.
 
-* ``stop``, stops one or more or all instances
-* ``restart``, restarts one or more or all instances
-* ``status``, returns the status of the instance(s)
+In production, it is recommended to run CubicWeb through a WSGI server like
+uWSGI or Gunicorn. See :mod:`cubicweb.pyramid` more details.
 
 Commands to maintain instances
 ------------------------------
--- a/doc/book/admin/ldap.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/admin/ldap.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -116,10 +116,9 @@
 Other notes
 -----------
 
-* Cubicweb is able to start if ldap cannot be reached, even on
-  cubicweb-ctl start ... If some source ldap server cannot be used
-  while an instance is running, the corresponding users won't be
-  authenticated but their status will not change (e.g. they will not
+* Cubicweb is able to start if ldap cannot be reached... If some source ldap
+  server cannot be used while an instance is running, the corresponding users
+  won't be authenticated but their status will not change (e.g. they will not
   be deactivated)
 
 * The user-base-dn is a key that helps cubicweb map CWUsers to LDAP
--- a/doc/book/admin/setup-windows.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/admin/setup-windows.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -32,10 +32,6 @@
   applications (including Eclipse + pydev, which is an arguably good
   IDE for Python under Windows).
 
-* `Twisted <http://twistedmatrix.com/trac/>`_ is an event-driven
-  networking engine
-  (`Download Twisted <http://twistedmatrix.com/trac/>`_)
-
 * `lxml <http://codespeak.net/lxml/>`_ library
   (version >=2.2.1) allows working with XML and HTML
   (`Download lxml <http://pypi.python.org/pypi/lxml/2.2.1>`_)
--- a/doc/book/admin/setup.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/admin/setup.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -62,17 +62,11 @@
 Update your list of packages and perform the installation::
 
   apt-get update
-  apt-get install cubicweb cubicweb-dev
-
-  # if you want pyramid, the recommended application server
-  apt-get install pyramid-cubicweb
+  apt-get install python3-cubicweb --install-recommends
 
-  # or if you want twisted (considered deprecated)
-  apt-get install cubicweb-twisted
-
-``cubicweb`` installs the framework itself, allowing you to create new
-instances. ``cubicweb-dev`` installs the development environment
-allowing you to develop new cubes.
+``python3-cubicweb`` installs the framework itself, allowing you to create new
+instances. Installing recommended packages will install the development
+environment allowing you to develop new cubes.
 
 There is also a wide variety of :ref:`cubes <AvailableCubes>`. You can access a
 list of available cubes using ``apt-cache search cubicweb`` or at the
@@ -80,16 +74,13 @@
 
 .. note::
 
-  `cubicweb-dev` will install basic sqlite support. You can easily setup
+  `python3-cubicweb` will install basic sqlite support. You can easily setup
   :ref:`cubicweb with other database <DatabaseInstallation>` using the following
   virtual packages :
 
-  * `cubicweb-postgresql-support` contains the necessary dependencies for
+  * `python3-cubicweb-postgresql-support` contains the necessary dependencies for
     using :ref:`cubicweb with postgresql datatabase <PostgresqlConfiguration>`
 
-  * `cubicweb-mysql-support` contains the necessary dependencies for using
-    :ref:`cubicweb with mysql database <MySqlConfiguration>`.
-
 .. _`list of sources`: http://wiki.debian.org/SourcesList
 .. _`Logilab's gnupg key`: https://www.logilab.fr/logilab-debian-keyring.gpg
 .. _`CubicWeb.org Forge`: http://www.cubicweb.org/project/
@@ -137,8 +128,7 @@
 
 A working compilation chain is needed to build the modules that include C
 extensions. If you really do not want to compile anything, installing `lxml <http://lxml.de/>`_,
-`Twisted Web <http://twistedmatrix.com/trac/wiki/Downloads/>`_ and `libgecode
-<http://www.gecode.org/>`_ will help.
+and `libgecode <http://www.gecode.org/>`_ will help.
 
 For Debian, these minimal dependencies can be obtained by doing::
 
@@ -155,18 +145,13 @@
 - setuptools http://www.lfd.uci.edu/~gohlke/pythonlibs/#setuptools
 - libxml-python http://www.lfd.uci.edu/~gohlke/pythonlibs/#libxml-python>
 - lxml http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml and
-- twisted http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted
 
 Make sure to choose the correct architecture and version of Python.
 
 Finally, install |cubicweb| and its dependencies, by running::
 
-  # for pyramid, the recommended application server
   pip install cubicweb[pyramid]
 
-  # or for twisted, considered deprecated (used by "cubicweb-ctl")
-  pip install cubicweb[etwist]
-
 Many other :ref:`cubes <AvailableCubes>` are available. A list is available at
 `PyPI <http://pypi.python.org/pypi?%3Aaction=search&term=cubicweb&submit=search>`_
 or at the `CubicWeb.org forge`_.
--- a/doc/book/annexes/depends.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/annexes/depends.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -19,8 +19,6 @@
 
 * lxml - http://codespeak.net/lxml - http://pypi.python.org/pypi/lxml
 
-* twisted - http://twistedmatrix.com/ - http://pypi.python.org/pypi/Twisted
-
 * logilab-common - http://www.logilab.org/project/logilab-common -
   http://pypi.python.org/pypi/logilab-common/
 
--- a/doc/book/annexes/rql/debugging.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/annexes/rql/debugging.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -14,7 +14,6 @@
 .. autodata:: cubicweb.server.DBG_RQL
 .. autodata:: cubicweb.server.DBG_SQL
 .. autodata:: cubicweb.server.DBG_REPO
-.. autodata:: cubicweb.server.DBG_MS
 .. autodata:: cubicweb.server.DBG_HOOKS
 .. autodata:: cubicweb.server.DBG_OPS
 .. autodata:: cubicweb.server.DBG_MORE
--- a/doc/book/annexes/rql/language.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/annexes/rql/language.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -557,7 +557,7 @@
 ~~~~~~~~~~~~~~~~~~~
 
 Below is the list of aggregate and transformation functions that are supported
-nativly by the framework. Notice that cubes may define additional functions.
+natively by the framework. Notice that cubes may define additional functions.
 
 .. _RQLAggregateFunctions:
 
--- a/doc/book/devrepo/datamodel/baseschema.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/devrepo/datamodel/baseschema.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -29,7 +29,6 @@
 
 Other entity types
 ~~~~~~~~~~~~~~~~~~
-* _`CWCache`, cache entities used to improve performances
 * _`CWProperty`, used to configure the instance
 
 * _`EmailAddress`, email address, used by the system to send notifications
--- a/doc/book/devrepo/datamodel/define-workflows.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/devrepo/datamodel/define-workflows.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -111,9 +111,6 @@
     (e.g. a list of user groups who can apply the transition; the user
     has to belong to at least one of the listed group to perform the action).
 
-.. sourcecode:: python
-
-  checkpoint()
 
 .. note::
   Do not forget to add the `_()` in front of all states and
@@ -129,6 +126,13 @@
 * `X`, the entity on which we may pass the transition
 * `U`, the user executing that may pass the transition
 
+It's also possible to get a given transition (usefull in migration) from a
+workflow use `transition_by_name(trname)`.
+To update the permission associated to the transition use
+`set_permissions(requiredgroups=(), conditions=(), reset=True)`.
+If `reset` is False, then the new permission are added instead of replacing the
+old one.
+
 
 .. image:: ../../../images/03-transitions-view_en.png
 
--- a/doc/book/devrepo/entityclasses/data-as-objects.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/devrepo/entityclasses/data-as-objects.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -113,7 +113,7 @@
 
 .. sourcecode:: python
 
-    from cubes.OTHER_CUBE import entities
+    from cubicweb_OTHER_CUBE import entities
 
     class EntityExample(entities.EntityExample):
 
--- a/doc/book/devrepo/migration.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/devrepo/migration.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -164,6 +164,9 @@
 * `set_size_constraint(etype, rtype, size, commit=True)`, changes the size constraints
   for the relation <rtype> of entity type <etype>.
 
+* `update_bfss_path(old_path, new_path, commit=True)`, change the path from `old_path` to
+  `new_path` in Bytes File-System Storage (bfss).
+
 Data migration
 --------------
 The following functions for data migration are available in `repository` scripts:
@@ -182,7 +185,13 @@
 scripts:
 
 * `add_workflow(label, workflowof, initial=False, commit=False, **kwargs)`, adds a new workflow
-  for a given type(s)
+  for a given type(s),
+* `get_workflow_for(etype)`, return the workflow for the given entity type,
+* `transition_by_name(self, trname)`, method of cubicweb.entities.wfobjs.Workflow instance
+  that returns the transition named `trname`,
+* `set_permissions(self, requiredgroups=(), conditions=(), reset=True)` method of
+  cubicweb.entities.wfobjs.Transition instance that sets or adds (if `reset` is False)
+  groups and conditions for this transition.
 
 You can find more details about workflows in the chapter :ref:`Workflow` .
 
--- a/doc/book/devrepo/repo/hooks.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/devrepo/repo/hooks.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -19,7 +19,7 @@
        age = Int(required=True)
 
 We would like to add a range constraint over a person's age. Let's write an hook
-(supposing yams can not handle this nativly, which is wrong). It shall be placed
+(supposing yams can not handle this natively, which is wrong). It shall be placed
 into `mycube/hooks.py`. If this file were to grow too much, we can easily have a
 `mycube/hooks/... package` containing hooks in various modules.
 
@@ -44,7 +44,7 @@
 In our example the base `__select__` is augmented with an `is_instance` selector
 matching the desired entity type.
 
-The `events` tuple is used specify that our hook should be called before the
+The `events` tuple is used to specify that our hook should be called before the
 entity is added or updated.
 
 Then in the hook's `__call__` method, we:
@@ -53,7 +53,7 @@
 * if so, check the value is in the range
 * if not, raise a validation error properly
 
-Now Let's augment our schema with new `Company` entity type with some relation to
+Now let's augment our schema with a new `Company` entity type with some relation to
 `Person` (in 'mycube/schema.py').
 
 .. sourcecode:: python
@@ -99,7 +99,7 @@
 
     from cubicweb.server.hook import Hook, DataOperationMixIn, Operation, match_rtype
 
-    def check_cycle(self, session, eid, rtype, role='subject'):
+    def check_cycle(session, eid, rtype, role='subject'):
         parents = set([eid])
         parent = session.entity_from_eid(eid)
         while parent.related(rtype, role):
@@ -135,8 +135,6 @@
 
 .. sourcecode:: python
 
-   from cubicweb.server.hook import set_operation
-
    class CheckSubsidiaryCycleHook(Hook):
        __regid__ = 'check_no_subsidiary_cycle'
        events = ('after_add_relation',)
@@ -152,11 +150,11 @@
                check_cycle(self.session, eid, self.rtype)
 
 
-Here, we call :func:`set_operation` so that we will simply accumulate eids of
+Here, we call :func:`add_data` so that we will simply accumulate eids of
 entities to check at the end in a single `CheckSubsidiaryCycleOp`
-operation. Value are stored in a set associated to the
-'subsidiary_cycle_detection' transaction data key. The set initialization and
-operation creation are handled nicely by :func:`set_operation`.
+operation. Values are stored in a set associated to the
+'check_no_subsidiary_cycle' transaction data key. The set initialization and
+operation creation are handled nicely by :func:`add_data`.
 
 A more realistic example can be found in the advanced tutorial chapter
 :ref:`adv_tuto_security_propagation`.
--- a/doc/book/devweb/edition/form.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/devweb/edition/form.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -35,7 +35,7 @@
  {'base': [<class 'cubicweb.web.views.forms.FieldsForm'>,
            <class 'cubicweb.web.views.forms.EntityFieldsForm'>],
   'changestate': [<class 'cubicweb.web.views.workflow.ChangeStateForm'>,
-                  <class 'cubes.tracker.views.forms.VersionChangeStateForm'>],
+                  <class 'cubicweb_tracker.views.forms.VersionChangeStateForm'>],
   'composite': [<class 'cubicweb.web.views.forms.CompositeForm'>,
                 <class 'cubicweb.web.views.forms.CompositeEntityForm'>],
   'deleteconf': [<class 'cubicweb.web.views.editforms.DeleteConfForm'>],
--- a/doc/book/devweb/internationalization.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/devweb/internationalization.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -58,23 +58,23 @@
 retrieve the proper translation of translation strings in the
 requested language.
 
-Finally you can also use the `__` attribute of request object to get a
+Finally you can also use the `__` (two underscores) attribute of request object to get a
 translation for a string *which should not itself added to the catalog*,
 usually in case where the actual msgid is created by string interpolation ::
 
   self._cw.__('This %s' % etype)
 
-In this example ._cw.__` is used instead of ._cw._` so we don't have 'This %s' in
+In this example `._cw.__` is used instead of `._cw._` so we don't have 'This %s' in
 messages catalogs.
 
 Translations in cubicweb-tal template can also be done with TAL tags
 `i18n:content` and `i18n:replace`.
 
-If you need to add messages on top of those that can be found in the source,
-you can create a file named `i18n/static-messages.pot`.
+If you need to mark other messages as translatable,
+you can create a file named `i18n/static-messages.pot`, see for example :ref:`translate-application-cube`.
 
 You could put there messages not found in the python sources or
-overrides for some messages of used cubes.
+overrides some messages that are in cubes used in the dependencies.
 
 Generated string
 ````````````````
@@ -149,7 +149,7 @@
 To update the translation catalogs you need to do:
 
 1. `cubicweb-ctl i18ncube <cube>`
-2. Edit the <cube>/i18n/xxx.po  files and add missing translations (empty `msgstr`)
+2. Edit the `<cube>/i18n/xxx.po` files and add missing translations (those with an empty `msgstr`)
 3. `hg ci -m "updated i18n catalogs"`
 4. `cubicweb-ctl i18ninstance <myinstance>`
 
@@ -280,10 +280,12 @@
 entity.dc_type(), eschema.display_name(req) or display_name(etype,
 req, form, context) methods/function calls will use them.
 
-It is also possible to explicitly use the with _cw.pgettext(context,
-msgid).
+It is also possible to explicitly use a context with `_cw.pgettext(context,
+msgid)`.
 
 
+.. _translate-application-cube:
+
 Specialize translation for an application cube
 ``````````````````````````````````````````````
 
--- a/doc/book/devweb/publisher.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/devweb/publisher.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -7,11 +7,7 @@
 
 The story begins with the ``CubicWebPublisher.main_publish``
 method. We do not get upper in the bootstrap process because it is
-dependant on the used HTTP library. With `twisted`_ however,
-``cubicweb.etwist.server.CubicWebRootResource.render_request`` is the
-real entry point.
-
-.. _`twisted`: http://twistedmatrix.com/trac/
+dependant on the used HTTP library.
 
 What main_publish does:
 
--- a/doc/book/devweb/request.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/devweb/request.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -107,10 +107,9 @@
     *while a request is executed*
 
 Please note that this class is abstract and that a concrete implementation
-will be provided by the *frontend* web used (in particular *twisted* as of
-today). For the views or others that are executed on the server side,
-most of the interface of `Request` is defined in the session associated
-to the client.
+will be provided by the *frontend* web used. For the views or others that are
+executed on the server side, most of the interface of `Request` is defined in
+the session associated to the client.
 
 API
 ```
--- a/doc/book/pyramid/ctl.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/book/pyramid/ctl.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -8,10 +8,6 @@
 The 'pyramid' command is a replacement for the 'start' command of :ref:`cubicweb-ctl`.
 It provides the same options and a few other ones.
 
-.. note::
-
-    The 'pyramid' command is provided by the ``pyramid`` cube.
-
 Options
 -------
 
--- a/doc/changes/3.25.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/changes/3.25.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -77,6 +77,8 @@
   (4516c3956d46).
 
 * The `next_tabindex` method of request class has been removed (011730a4af73).
+  This include the removal of `settabindex` from the `FieldWidget` class init
+  method.
 
 * The `cubicweb.hook.logstats.start` hook was dropped because it's looping
   task would not be run in a web instance (see first point about repository
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/changes/3.27.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -0,0 +1,67 @@
+3.27 (not yet released)
+=======================
+
+New features
+------------
+
+* Tests can now be run concurrently across multiple processes. You can use
+  `pytest-xdist`_ for that. For tests using `PostgresApptestConfiguration` you
+  should be aware that `startpgcluster()` can't run concurrently. Workaround is
+  to call pytest with ``--dist=loadfile`` to use a single test process per test
+  module or use an existing database cluster and set ``db-host`` and
+  ``db-port`` of ``devtools.DEFAULT_PSQL_SOURCES['system']`` accordingly.
+
+.. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist
+
+* on `cubicweb-ctl create` and `cubicweb-ctl pyramid`, if it doesn't already
+  exist in the instance directory, the `pyramid.ini` file will be generated
+  with the needed secrets.
+
+* add a --pdb flag to all cubicweb-ctl command to launch (i)pdb if an exception
+  occurs during a command execution.
+
+* the --loglevel and --dbglevel flags are available for all cubicweb-ctl
+  instance commands (and not only the ``pyramid`` one)
+
+* following "only in foreground" behavior all commands logs to stdout by
+  default from now on. To still log to a file pass ``log_to_file=True`` to
+  ``CubicWebConfiguration.config_for``
+
+* add a new migration function `update_bfss_path(old_path, new_path)` to update
+  the path in Bytes File-System Storage (bfss).
+
+Backwards incompatible changes
+------------------------------
+
+* Standardization on the way to launch a cubicweb instance, from now on the
+  only way to do that will be the used the ``pyramid`` command. Therefore:
+
+   * ``cubicweb-ctl`` commands "start", "stop", "restart", "reload" and "status"
+     have been removed because they relied on the Twisted web server backend that
+     is no longer maintained nor working with Python 3.
+
+   * Twisted web server support has been removed.
+
+   * ``cubicweb-ctl wsgi`` has also been removed.
+
+* Support for legacy cubes (in the 'cubes' python namespace) has been dropped.
+  Use of environment variables CW_CUBES_PATH and CUBES_DIR is removed.
+
+* Python 2 support has been dropped.
+
+* Exceptions in notification hooks aren't catched-all anymore during tests so
+  one can expect tests that seem to pass (but were actually silently failing)
+  to fail now.
+
+* All "cubicweb-ctl" command only accept one instance argument from now one
+  (instead of 0 to n)
+
+* 'pyramid' command will always run in the foreground now, by consequence the
+  option ``--no-daemon`` has been removed.
+
+* DBG_MS flag has been removed since it is not used anymore
+
+Deprecated code drops
+---------------------
+
+Most code deprecated until version 3.25 has been dropped.
--- a/doc/changes/changelog.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/changes/changelog.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -2,6 +2,7 @@
  Changelog history
 ===================
 
+.. include:: 3.27.rst
 .. include:: 3.26.rst
 .. include:: 3.25.rst
 .. include:: 3.24.rst
--- a/doc/changes/index.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/changes/index.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -4,6 +4,8 @@
 .. toctree::
     :maxdepth: 1
 
+    3.27
+    3.26
     3.25
     3.24
     3.23
--- a/doc/dev/features_list.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/dev/features_list.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -171,7 +171,6 @@
    |           +--------------------------------------------------------+----+----+
    |           | mime type based conversion                             | 2  | 0  |
    |           +--------------------------------------------------------+----+----+
-   |           | CWCache                                                | 1  | 0  |
    +-----------+--------------------------------------------------------+----+----+
 
 
--- a/doc/tools/mode_plan.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/tools/mode_plan.py	Fri Oct 18 23:39:03 2019 +0200
@@ -23,8 +23,6 @@
 rename A010-joe.en.txt to A030-joe.en.txt
 accept [y/N]?
 """
-from __future__ import print_function
-
 
 def ren(a,b):
     names = glob.glob('%s*'%a)
--- a/doc/tutorials/advanced/part01_create-cube.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/tutorials/advanced/part01_create-cube.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -10,9 +10,9 @@
 
 Fisrt I need a python virtual environment with cubicweb::
 
-  virtualenv python-2.7.5_cubicweb
-  . /python-2.7.5_cubicweb/bin/activate
-  pip install cubicweb[etwist]
+  python3 -m venv venv
+  . venv/bin/activate
+  pip install cubicweb[pyramid]
 
 
 Step 2: creating a new cube for my web site
@@ -20,9 +20,9 @@
 
 One note about my development environment: I wanted to use the packaged
 version of CubicWeb and cubes while keeping my cube in the current
-directory, let's say `~src/cubes`::
+directory, let's say `~/src/cubes`::
 
-  cd ~src/cubes
+  cd ~/src/cubes
   CW_MODE=user
 
 I can now create the cube which will hold custom code for this web
@@ -173,7 +173,7 @@
 
 Once the instance and database are fully initialized, run ::
 
-  cubicweb-ctl start -D sytweb_instance
+  cubicweb-ctl pyramid -D sytweb_instance
 
 to start the instance, check you can connect on it, etc... then go on
 http://localhost:8080 (or with another port if you've modified it)
--- a/doc/tutorials/advanced/part02_security.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/tutorials/advanced/part02_security.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -137,12 +137,12 @@
 	    'delete': ('managers', 'owners'),
 	    }
 
-    from cubes.folder.schema import Folder
-    from cubes.file.schema import File
-    from cubes.comment.schema import Comment
-    from cubes.person.schema import Person
-    from cubes.zone.schema import Zone
-    from cubes.tag.schema import Tag
+    from cubicweb_folder.schema import Folder
+    from cubicweb_file.schema import File
+    from cubicweb_comment.schema import Comment
+    from cubicweb_person.schema import Person
+    from cubicweb_zone.schema import Zone
+    from cubicweb_tag.schema import Tag
 
     Folder.__permissions__ = VISIBILITY_PERMISSIONS
     File.__permissions__ = VISIBILITY_PERMISSIONS
--- a/doc/tutorials/advanced/part04_ui-base.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/tutorials/advanced/part04_ui-base.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -174,7 +174,7 @@
 
 .. sourcecode:: python
 
-    from cubes.folder import entities as folder
+    from cubicweb_folder import entities as folder
 
 
     class FolderITreeAdapter(folder.FolderITreeAdapter):
@@ -327,7 +327,7 @@
 To see if everything is ok on my test instance, I do: ::
 
   $ cubicweb-ctl i18ninstance sytweb_instance
-  $ cubicweb-ctl start -D sytweb_instance
+  $ cubicweb-ctl pyramid -D sytweb_instance
 
 The first command compile i18n catalogs (e.g. generates '.mo' files) for my test
 instance. The second command start it in debug mode, so I can open my browser and
--- a/doc/tutorials/advanced/part05_ui-advanced.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/tutorials/advanced/part05_ui-advanced.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -196,8 +196,8 @@
 
   from cubicweb.predicates import none_rset
   from cubicweb.web.views import bookmark
-  from cubes.zone import views as zone
-  from cubes.tag import views as tag
+  from cubicweb_zone import views as zone
+  from cubicweb_tag import views as tag
 
 
   # change bookmarks box selector so it's only displayed on startup views
--- a/doc/tutorials/base/blog-in-five-minutes.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/tutorials/base/blog-in-five-minutes.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -8,7 +8,7 @@
 For Debian or Ubuntu users, first install the following packages
 (:ref:`DebianInstallation`)::
 
-    cubicweb, cubicweb-dev, cubicweb-twisted, cubicweb-blog
+    python3-cubicweb, cubicweb-ctl, cubicweb-blog
 
 Windows or Mac OS X users must install |cubicweb| from source (see
 :ref:`SourceInstallation` and :ref:`WindowsInstallation`).
@@ -17,7 +17,7 @@
 
    virtualenv venv
    source venv/bin/activate
-   pip install cubicweb[etwist] cubicweb-dev cubicweb-blog
+   pip install cubicweb[pyramid] cubicweb-dev cubicweb-blog
 
 Then create and initialize your instance::
 
@@ -38,19 +38,16 @@
 choose `sqlite` when asked for which database driver to use, since it has a much
 simple setup (no database server needed).
 
+Then, you need to setup the CubicWeb Pyramid interface, as document at
+:ref:`pyramid_settings`.
+
 One the process is completed (including database initialisation), you can start
 your instance by using: ::
 
-    cubicweb-ctl start -D myblog
+    cubicweb-ctl pyramid -D myblog
 
 The `-D` option activates the debugging mode. Removing it will launch the instance
-as a daemon in the background, and ``cubicweb-ctl stop myblog`` will stop
-it in that case.
-
-.. Note::
-
-   If you get a traceback when going on the web interface make sure your
-   version of twisted is **inferior** to 17.
+as a daemon in the background.
 
 .. _AboutFileSystemPermissions:
 
--- a/doc/tutorials/base/customizing-the-application.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/tutorials/base/customizing-the-application.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -16,30 +16,16 @@
 Create your own cube
 ~~~~~~~~~~~~~~~~~~~~
 
-First, notice that if you've installed |cubicweb| using Debian packages, you will
-need the additional ``cubicweb-dev`` package to get the commands necessary to
-|cubicweb| development. All `cubicweb-ctl` commands are described in details in
-:ref:`cubicweb-ctl`.
-
 Once your |cubicweb| development environment is set up, you can create a new
 cube::
 
   cubicweb-ctl newcube myblog
 
-This will create in the cubes directory (:file:`/path/to/grshell/cubes` for source
-installation, :file:`/usr/share/cubicweb/cubes` for Debian packages installation)
-a directory named :file:`blog` reflecting the structure described in
-:ref:`cubelayout`.
+This will create a a directory named :file:`cubicweb-myblog` reflecting the
+structure described in :ref:`cubelayout`.
 
-For packages installation, you can still create new cubes in your home directory
-using the following configuration. Let's say you want to develop your new cubes
-in `~src/cubes`, then set the following environment variables: ::
-
-  CW_CUBES_PATH=~/src/cubes
-
-and then create your new cube using: ::
-
-  cubicweb-ctl newcube --directory=~/src/cubes myblog
+All `cubicweb-ctl` commands are described in details in
+:ref:`cubicweb-ctl`.
 
 .. Note::
 
@@ -58,7 +44,7 @@
 
 .. sourcecode:: python
 
-   __depends__ =  {'cubicweb': '>= 3.10.7',
+   __depends__ =  {'cubicweb': '>= 3.24.0',
                    'cubicweb-blog': None}
 
 where the ``None`` means we do not depends on a particular version of the cube.
@@ -136,7 +122,7 @@
   cubicweb-ctl stop myblog # or Ctrl-C in the terminal running the server in debug mode
   cubicweb-ctl delete myblog
   cubicweb-ctl create myblog myblog
-  cubicweb-ctl start -D myblog
+  cubicweb-ctl pyramid -D myblog
 
 Another way is to add our cube to the instance using the cubicweb-ctl shell
 facility. It's a python shell connected to the instance with some special
@@ -151,7 +137,7 @@
   type "exit" or Ctrl-D to quit the shell and resume operation
   >>> add_cube('myblog')
   >>>
-  $ cubicweb-ctl start -D myblog
+  $ cubicweb-ctl pyramid -D myblog
 
 The `add_cube` command is enough since it automatically updates our
 application to the cube's schema. There are plenty of other migration
@@ -225,7 +211,7 @@
   cubicweb-ctl i18ncube myblog # build/update cube's message catalogs
   # then add translation into .po file into the cube's i18n directory
   cubicweb-ctl i18ninstance myblog # recompile instance's message catalogs
-  cubicweb-ctl restart -D myblog # instance has to be restarted to consider new catalogs
+  # instance has to be restarted to consider new catalogs
 
 You'll then be able to redefine each of them according to your needs and
 preferences. So let's see how to do such thing.
--- a/doc/tutorials/dataimport/diseasome_import.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/tutorials/dataimport/diseasome_import.py	Fri Oct 18 23:39:03 2019 +0200
@@ -27,7 +27,7 @@
 
 # CubicWeb imports
 import cubicweb.dataimport as cwdi
-from cubes.dataio import dataimport as mcwdi
+from cubicweb_dataio import dataimport as mcwdi
 
 # Diseasome parser import
 import diseasome_parser as parser
--- a/doc/tutorials/dataimport/index.rst	Tue Aug 27 20:16:01 2019 +0200
+++ b/doc/tutorials/dataimport/index.rst	Fri Oct 18 23:39:03 2019 +0200
@@ -354,7 +354,7 @@
 ``dataimport`` module, then initialize the store by giving it the
 ``session`` parameter::
 
-    from cubes.dataio import dataimport as mcwdi
+    from cubicweb_dataio import dataimport as mcwdi
     ...
 
     store = mcwdi.MassiveObjectStore(session)
@@ -441,7 +441,7 @@
         store = cwdi.SQLGenObjectStore(session)
    
    where ``cwdi`` is the imported ``cubicweb.dataimport`` or 
-   ``cubes.dataio.dataimport``.
+   ``cubicweb_dataio.dataimport``.
 
 #. calls the diseasome parser, that is, the ``entities_from_rdf`` function in the 
    ``diseasome_parser`` module and iterates on its result, in a line such as::
--- a/flake8-ok-files.txt	Tue Aug 27 20:16:01 2019 +0200
+++ b/flake8-ok-files.txt	Fri Oct 18 23:39:03 2019 +0200
@@ -1,5 +1,8 @@
 cubicweb/__init__.py
 cubicweb/__main__.py
+cubicweb/_exceptions.py
+cubicweb/cwctl.py
+cubicweb/cwvreg.py
 cubicweb/crypto.py
 cubicweb/dataimport/csv.py
 cubicweb/dataimport/importer.py
@@ -10,9 +13,9 @@
 cubicweb/dataimport/test/test_csv.py
 cubicweb/dataimport/test/test_pgstore.py
 cubicweb/dataimport/test/test_massive_store.py
-cubicweb/dataimport/test/test_sqlgenstore.py
 cubicweb/dataimport/test/test_stores.py
 cubicweb/dataimport/test/unittest_importer.py
+cubicweb/devtools/httptest.py
 cubicweb/devtools/test/data/cubes/i18ntestcube/__init__.py
 cubicweb/devtools/test/data/cubes/i18ntestcube/views.py
 cubicweb/devtools/test/data/cubes/__init__.py
@@ -25,9 +28,6 @@
 cubicweb/entities/adapters.py
 cubicweb/entities/authobjs.py
 cubicweb/entities/test/unittest_base.py
-cubicweb/etwist/__init__.py
-cubicweb/etwist/request.py
-cubicweb/etwist/service.py
 cubicweb/ext/__init__.py
 cubicweb/ext/test/unittest_rest.py
 cubicweb/hooks/synccomputed.py
@@ -109,11 +109,14 @@
 cubicweb/test/unittest_rset.py
 cubicweb/test/unittest_rtags.py
 cubicweb/test/unittest_schema.py
+cubicweb/test/unittest_statsd.py
 cubicweb/test/unittest_toolsutils.py
 cubicweb/test/unittest_wfutils.py
 cubicweb/toolsutils.py
 cubicweb/web/application.py
+cubicweb/web/box.py
 cubicweb/web/formwidgets.py
+cubicweb/web/schemaviewer.py
 cubicweb/web/test/data/entities.py
 cubicweb/web/test/unittest_application.py
 cubicweb/web/test/unittest_http_headers.py
@@ -132,6 +135,7 @@
 cubicweb/web/views/wdoc.py
 cubicweb/web/views/workflow.py
 cubicweb/web/views/uicfg.py
+cubicweb/web/webconfig.py
 cubicweb/web/webctl.py
 cubicweb/xy.py
 cubicweb/pyramid/auth.py
@@ -151,6 +155,7 @@
 cubicweb/pyramid/test/test_bw_request.py
 cubicweb/pyramid/test/test_config.py
 cubicweb/pyramid/test/test_core.py
+cubicweb/pyramid/test/test_hooks.py
 cubicweb/pyramid/test/test_login.py
 cubicweb/pyramid/test/test_rest_api.py
 cubicweb/pyramid/test/test_tools.py
--- a/requirements/dev.txt	Tue Aug 27 20:16:01 2019 +0200
+++ b/requirements/dev.txt	Fri Oct 18 23:39:03 2019 +0200
@@ -1,1 +1,2 @@
 pytest
+pytest-subtests
--- a/requirements/test-misc.txt	Tue Aug 27 20:16:01 2019 +0200
+++ b/requirements/test-misc.txt	Fri Oct 18 23:39:03 2019 +0200
@@ -1,19 +1,13 @@
 ### Requirements for tests in various cubicweb/**/test directories. ###
 
 ## shared by several test folders
-cubicweb-card == 0.5.8
 docutils
-Twisted < 16.0.0
 webtest
 
 ## cubicweb/test
 Pygments
-pycrypto
-mock
+pycryptodomex
 #fyzz XXX pip install fails
-cubicweb-file == 1.18.0
-cubicweb-localperms == 0.3.2
-cubicweb-tag == 1.8.3
 
 ## cubicweb/devtools/test
 flake8
@@ -29,4 +23,3 @@
 repoze.lru
 
 ## cubicweb/sobject/test
-cubicweb-comment == 1.12.2
--- a/requirements/test-server.txt	Tue Aug 27 20:16:01 2019 +0200
+++ b/requirements/test-server.txt	Fri Oct 18 23:39:03 2019 +0200
@@ -1,9 +1,2 @@
-mock
 psycopg2-binary
 ldap3 < 2
-cubicweb-basket
-cubicweb-card
-cubicweb-comment
-cubicweb-file >= 2.2.2
-cubicweb-localperms
-cubicweb-tag
--- a/requirements/test-web.txt	Tue Aug 27 20:16:01 2019 +0200
+++ b/requirements/test-web.txt	Fri Oct 18 23:39:03 2019 +0200
@@ -1,7 +1,3 @@
 docutils
-Twisted < 16.0.0
 requests
 webtest
-cubicweb-blog
-cubicweb-file >= 2.0.0
-cubicweb-tag
--- a/setup.py	Tue Aug 27 20:16:01 2019 +0200
+++ b/setup.py	Fri Oct 18 23:39:03 2019 +0200
@@ -63,17 +63,16 @@
     package_data=package_data,
     include_package_data=True,
     install_requires=[
-        'six >= 1.4.0',
         'logilab-common >= 1.4.0',
         'logilab-mtconverter >= 0.8.0',
         'rql >= 0.34.0',
         'yams >= 0.45.0',
         'lxml',
         'logilab-database >= 1.15.0',
-        'passlib',
+        'passlib >= 1.7',
         'pytz',
         'Markdown',
-        'unittest2 >= 0.7.0',
+        'filelock',
     ],
     entry_points={
         'console_scripts': [
@@ -88,10 +87,7 @@
             'Pillow',
         ],
         'crypto': [
-            'pycrypto',
-        ],
-        'etwist': [
-            'Twisted < 16.0.0',
+            'pycryptodomex',
         ],
         'ext': [
             'docutils >= 0.6',
--- a/tox.ini	Tue Aug 27 20:16:01 2019 +0200
+++ b/tox.ini	Fri Oct 18 23:39:03 2019 +0200
@@ -1,24 +1,22 @@
 [tox]
 envlist =
   check-manifest,flake8,
-  py{27,3}-{server,web,misc}
+  py3-{server,web,misc}
 
 [testenv]
+basepython=python3
 deps =
   -r{toxinidir}/requirements/dev.txt
-  py27: backports.tempfile
   misc: -r{toxinidir}/requirements/test-misc.txt
   server: -r{toxinidir}/requirements/test-server.txt
   web: -r{toxinidir}/requirements/test-web.txt
 commands =
-  misc: {envpython} -m pip install --upgrade --no-deps --quiet git+git://github.com/logilab/yapps@master#egg=yapps
+  misc: {envpython} -m pip install --upgrade --no-deps --quiet https://github.com/logilab/yapps/tarball/master#egg=yapps
   misc: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/test {toxinidir}/cubicweb/dataimport/test {toxinidir}/cubicweb/devtools/test {toxinidir}/cubicweb/entities/test {toxinidir}/cubicweb/ext/test {toxinidir}/cubicweb/hooks/test {toxinidir}/cubicweb/sobjects/test {toxinidir}/cubicweb/wsgi/test {toxinidir}/cubicweb/pyramid/test
-  py27-misc: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/etwist/test
   server: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/server/test
   web: {envpython} -m pytest {posargs} {toxinidir}/cubicweb/web/test
 
 [testenv:flake8]
-basepython=python3
 skip_install = true
 deps =
   flake8 >= 3.6
@@ -40,10 +38,7 @@
 deps =
   check-manifest
 commands =
-  {envpython} -m check_manifest {toxinidir} \
-# ignore symlinks that are not recognized by check-manifest, see
-# https://github.com/mgedmin/check-manifest/issues/69
-    --ignore cubicweb/devtools/test/data/cubes/i18ntestcube*,cubicweb/test/data/legacy_cubes*
+  {envpython} -m check_manifest {toxinidir}
 
 [pytest]
 python_files = *test_*.py