changeset 414:ccb2a22ea0fc

Python 3 port: /!\ Python 3.6+ is now needed to use SàT Pubsub /!\ instability may occur and features may not be working anymore, this will improve with time The same procedure as in backend has been applied (check backend commit ab2696e34d29 logs for details). Python minimal version has been updated in setup.py
author Goffi <goffi@goffi.org>
date Fri, 16 Aug 2019 12:53:33 +0200
parents a5edf5e1dd74
children 4179ed660a85
files sat_pubsub/__init__.py sat_pubsub/backend.py sat_pubsub/const.py sat_pubsub/container.py sat_pubsub/delegation.py sat_pubsub/error.py sat_pubsub/exceptions.py sat_pubsub/iidavoll.py sat_pubsub/mam.py sat_pubsub/pgsql_storage.py sat_pubsub/privilege.py sat_pubsub/pubsub_admin.py sat_pubsub/schema.py sat_pubsub/test/test_backend.py sat_pubsub/test/test_storage.py setup.py twisted/plugins/pubsub.py
diffstat 17 files changed, 169 insertions(+), 170 deletions(-) [+]
line wrap: on
line diff
--- a/sat_pubsub/__init__.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/__init__.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 
 # Copyright (c) 2012-2019 Jérôme Poisson
--- a/sat_pubsub/backend.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/backend.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 #
 # Copyright (c) 2012-2019 Jérôme Poisson
@@ -64,7 +64,7 @@
 import copy
 import uuid
 
-from zope.interface import implements
+from zope.interface import implementer
 
 from twisted.application import service
 from twisted.python import components, log
@@ -123,6 +123,7 @@
                               + [copy.deepcopy(d) for d in item_data[1:]])
 
 
+@implementer(iidavoll.IBackendService)
 class BackendService(service.Service, utility.EventDispatcher):
     """
     Generic publish-subscribe backend service.
@@ -134,7 +135,6 @@
     @cvar defaultConfig: The default node configuration.
     """
 
-    implements(iidavoll.IBackendService)
 
     nodeOptions = {
             const.OPT_PERSIST_ITEMS:
@@ -201,7 +201,7 @@
         self.storage = storage
         self._callbackList = []
         self.config = config
-        self.admins = config[u'admins_jids_list']
+        self.admins = config['admins_jids_list']
 
     def isAdmin(self, entity_jid):
         """Return True if an entity is an administrator"""
@@ -255,7 +255,7 @@
 
     def _makeMetaData(self, metaData):
         options = []
-        for key, value in metaData.iteritems():
+        for key, value in metaData.items():
             if key in self.nodeOptions:
                 option = {"var": key}
                 option.update(self.nodeOptions[key])
@@ -324,7 +324,7 @@
         """
         categories = []
         try:
-            entry_elt = item_elt.elements(const.NS_ATOM, "entry").next()
+            entry_elt = next(item_elt.elements(const.NS_ATOM, "entry"))
         except StopIteration:
             return categories
 
@@ -373,7 +373,7 @@
 
         # we now remove every field which is not in data schema
         to_remove = set()
-        for item_var, item_field in item_form.fields.iteritems():
+        for item_var, item_field in item_form.fields.items():
             if item_var not in schema_form.fields:
                 to_remove.add(item_field)
 
@@ -387,7 +387,7 @@
         current publisher must correspond to each item publisher
         """
         def doCheck(item_pub_map):
-            for item_publisher in item_pub_map.itervalues():
+            for item_publisher in item_pub_map.values():
                 if item_publisher.userhost() != publisher.userhost():
                     raise error.ItemForbidden()
 
@@ -431,13 +431,13 @@
                     item["id"] = yield node.getNextId()
                     new_item = True
                     if ret_payload is None:
-                        ret_pubsub_elt = domish.Element((pubsub.NS_PUBSUB, u'pubsub'))
-                        ret_publish_elt = ret_pubsub_elt.addElement(u'publish')
-                        ret_publish_elt[u'node'] = node.nodeIdentifier
+                        ret_pubsub_elt = domish.Element((pubsub.NS_PUBSUB, 'pubsub'))
+                        ret_publish_elt = ret_pubsub_elt.addElement('publish')
+                        ret_publish_elt['node'] = node.nodeIdentifier
                         ret_payload = ret_pubsub_elt
                     ret_publish_elt = ret_payload.publish
-                    ret_item_elt = ret_publish_elt.addElement(u'item')
-                    ret_item_elt["id"] = item[u"id"]
+                    ret_item_elt = ret_publish_elt.addElement('item')
+                    ret_item_elt["id"] = item["id"]
                 else:
                     check_overwrite = True
                     new_item = False
@@ -462,9 +462,9 @@
                             # TODO: handle multiple items publishing (from several
                             #       publishers)
                             raise error.NoPublishing(
-                                u"consistent_publisher is currently only possible when "
-                                u"publishing items from a single publisher. Try to "
-                                u"publish one item at a time")
+                                "consistent_publisher is currently only possible when "
+                                "publishing items from a single publisher. Try to "
+                                "publish one item at a time")
                         # we replace requestor and new payload's publisher by original
                         # item publisher to keep publisher consistent
                         requestor = publishers.pop()
@@ -506,7 +506,7 @@
 
             notifications = [(subscriber, subscriptions_, items_data)
                              for subscriber, subscriptions_
-                             in subsBySubscriber.iteritems()]
+                             in subsBySubscriber.items()]
 
             return notifications
 
@@ -762,7 +762,7 @@
             #        if at least one other entity is owner for this node
             raise error.Forbidden("You can't change your own affiliation")
 
-        to_delete = [jid_ for jid_, affiliation in affiliations.iteritems() if affiliation == 'none']
+        to_delete = [jid_ for jid_, affiliation in affiliations.items() if affiliation == 'none']
         for jid_ in to_delete:
             del affiliations[jid_]
 
@@ -960,7 +960,7 @@
         elif access_model == const.VAL_AMODEL_WHITELIST:
             yield self.checkNodeAffiliations(node, requestor)
         else:
-            raise Exception(u"Unknown access_model")
+            raise Exception("Unknown access_model")
 
         defer.returnValue((affiliation, owner, roster, access_model))
 
@@ -1154,7 +1154,7 @@
             """
             # TODO: the behaviour should be configurable (per node ?)
             if (any((requestor.userhostJID() != publisher.userhostJID()
-                    for publisher in publishers_map.itervalues()))
+                    for publisher in publishers_map.values()))
                 and not self.isAdmin(requestor)
                ):
                 raise error.Forbidden()
@@ -1278,7 +1278,7 @@
 
     discoIdentity = disco.DiscoIdentity('pubsub',
                                         'service',
-                                        u'Salut à Toi pubsub service')
+                                        'Salut à Toi pubsub service')
 
     pubsubService = None
 
@@ -1523,7 +1523,7 @@
         return d
 
     def _mapErrors(self, failure):
-        e = failure.trap(*self._errorMap.keys())
+        e = failure.trap(*list(self._errorMap.keys()))
 
         condition, pubsubCondition, feature = self._errorMap[e]
         msg = failure.value.msg
@@ -1601,7 +1601,7 @@
 
     def _publish_errb(self, failure, request):
         if failure.type == error.NodeNotFound and self.backend.supportsAutoCreate():
-            print "Auto-creating node %s" % (request.nodeIdentifier,)
+            print("Auto-creating node %s" % (request.nodeIdentifier,))
             d = self.backend.createNode(request.nodeIdentifier,
                                         request.sender,
                                         pep=self._isPep(request),
@@ -1780,8 +1780,8 @@
 
 
 
+@implementer(iwokkel.IDisco)
 class ExtraDiscoHandler(XMPPHandler):
-    implements(iwokkel.IDisco)
     # see comment in twisted/plugins/pubsub.py
     # FIXME: upstream must be fixed so we can use custom (non pubsub#) disco features
 
--- a/sat_pubsub/const.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/const.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 
 # Copyright (c) 2012-2019 Jérôme Poisson
--- a/sat_pubsub/container.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/container.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 
 # Copyright (C) 2016 Jérôme Poisson (goffi@goffi.org)
--- a/sat_pubsub/delegation.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/delegation.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 #
 # Copyright (c) 2015 Jérôme Poisson
@@ -31,7 +31,7 @@
 from twisted.words.protocols.jabber import jid, error
 from twisted.words.protocols.jabber.xmlstream import toResponse
 from twisted.words.xish import domish
-from zope.interface import implements
+from zope.interface import implementer
 
 DELEGATION_NS = 'urn:xmpp:delegation:1'
 FORWARDED_NS = 'urn:xmpp:forward:0'
@@ -49,8 +49,8 @@
     pass
 
 
+@implementer(iwokkel.IDisco)
 class DelegationsHandler(XMPPHandler):
-    implements(iwokkel.IDisco)
     _service_hacked = False
 
     def __init__(self):
@@ -144,32 +144,32 @@
 
     def onAdvertise(self, message):
         """Manage the <message/> advertising delegations"""
-        delegation_elt = message.elements(DELEGATION_NS, 'delegation').next()
+        delegation_elt = next(message.elements(DELEGATION_NS, 'delegation'))
         delegated = {}
         for delegated_elt in delegation_elt.elements(DELEGATION_NS):
             try:
                 if delegated_elt.name != 'delegated':
-                    raise InvalidStanza(u'unexpected element {}'.format(delegated_elt.name))
+                    raise InvalidStanza('unexpected element {}'.format(delegated_elt.name))
                 try:
                     namespace = delegated_elt['namespace']
                 except KeyError:
-                    raise InvalidStanza(u'was expecting a "namespace" attribute in delegated element')
+                    raise InvalidStanza('was expecting a "namespace" attribute in delegated element')
                 delegated[namespace] = []
                 for attribute_elt in delegated_elt.elements(DELEGATION_NS, 'attribute'):
                     try:
                         delegated[namespace].append(attribute_elt["name"])
                     except KeyError:
-                        raise InvalidStanza(u'was expecting a "name" attribute in attribute element')
+                        raise InvalidStanza('was expecting a "name" attribute in attribute element')
             except InvalidStanza as e:
                 log.msg("Invalid stanza received ({})".format(e))
 
-        log.msg(u'delegations updated:\n{}'.format(
-            u'\n'.join([u"    - namespace {}{}".format(ns,
-            u"" if not attributes else u" with filtering on {} attribute(s)".format(
-            u", ".join(attributes))) for ns, attributes in delegated.items()])))
+        log.msg('delegations updated:\n{}'.format(
+            '\n'.join(["    - namespace {}{}".format(ns,
+            "" if not attributes else " with filtering on {} attribute(s)".format(
+            ", ".join(attributes))) for ns, attributes in list(delegated.items())])))
 
         if not pubsub.NS_PUBSUB in delegated:
-            log.msg(u"Didn't got pubsub delegation from server, can't act as a PEP service")
+            log.msg("Didn't got pubsub delegation from server, can't act as a PEP service")
 
     def onForward(self, iq):
         """Manage forwarded iq
@@ -183,14 +183,14 @@
         # TODO: do proper origin security check
         _, allowed = iq['to'].split('.', 1)
         if jid.JID(iq['from']) != jid.JID(allowed):
-            log.msg((u"SECURITY WARNING: forwarded stanza doesn't come from our server: {}"
+            log.msg(("SECURITY WARNING: forwarded stanza doesn't come from our server: {}"
                      .format(iq.toXml())).encode('utf-8'))
             raise error.StanzaError('not-allowed')
 
         try:
-            fwd_iq = (iq.elements(DELEGATION_NS, 'delegation').next()
-                      .elements(FORWARDED_NS, 'forwarded').next()
-                      .elements('jabber:client', 'iq').next())
+            delegation_elt = next(iq.elements(DELEGATION_NS, 'delegation'))
+            forwarded_elt = next(delegation_elt.elements(FORWARDED_NS, 'forwarded'))
+            fwd_iq = next(forwarded_elt.elements('jabber:client', 'iq'))
         except StopIteration:
             raise error.StanzaError('not-acceptable')
 
--- a/sat_pubsub/error.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/error.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 
 # Copyright (c) 2012-2019 Jérôme Poisson
--- a/sat_pubsub/exceptions.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/exceptions.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 
 # Copyright (c) 2012-2019 Jérôme Poisson
--- a/sat_pubsub/iidavoll.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/iidavoll.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 
 # Copyright (c) 2003-2011 Ralph Meijer
--- a/sat_pubsub/mam.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/mam.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 
 # Copyright (c) 2016 Jérôme Poisson
@@ -25,7 +25,7 @@
 """
 
 
-from zope.interface import implements
+from zope.interface import implementer
 
 from twisted.words.xish import domish
 from twisted.python import log
@@ -40,8 +40,8 @@
 from wokkel import delay
 
 
+@implementer(mam.IMAMResource)
 class MAMResource(object):
-    implements(mam.IMAMResource)
     _errorMap = backend.PubSubResourceFromBackend._errorMap
 
     def __init__(self, backend_):
@@ -49,7 +49,7 @@
 
     def _mapErrors(self, failure):
         # XXX: come from backend.PubsubResourceFromBackend
-        e = failure.trap(*self._errorMap.keys())
+        e = failure.trap(*list(self._errorMap.keys()))
 
         condition, pubsubCondition, feature = self._errorMap[e]
         msg = failure.value.msg
@@ -77,7 +77,7 @@
             pep = False
         ext_data = {'pep': pep}
         if mam_request.form:
-            ext_data['filters'] = mam_request.form.fields.values()
+            ext_data['filters'] = list(mam_request.form.fields.values())
         if mam_request.rsm is None:
             if const.VAL_RSM_MAX_DEFAULT != None:
                 log.msg("MAM request without RSM limited to {}".format(const.VAL_RSM_MAX_DEFAULT))
@@ -114,7 +114,7 @@
                         #      "complete"
                         #      attribute set to "true".
                         page_max = (int(rsm_elt.first['index']) + 1) * mam_request.rsm.max
-                        count = int(unicode(rsm_elt.count))
+                        count = int(str(rsm_elt.count))
                         if page_max >= count:
                             # the maximum items which can be displayed is equal to or
                             # above the total number of items, which means we are complete
--- a/sat_pubsub/pgsql_storage.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/pgsql_storage.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 
 # Copyright (c) 2012-2019 Jérôme Poisson
@@ -53,7 +53,7 @@
 
 import copy, logging
 
-from zope.interface import implements
+from zope.interface import implementer
 
 from twisted.internet import reactor
 from twisted.internet import defer
@@ -77,7 +77,7 @@
 
 # parseXml manage str, but we get unicode
 parseXml = lambda unicode_data: generic.parseXml(unicode_data.encode('utf-8'))
-ITEMS_SEQ_NAME = u'node_{node_id}_seq'
+ITEMS_SEQ_NAME = 'node_{node_id}_seq'
 PEP_COL_NAME = 'pep'
 CURRENT_VERSION = '5'
 # retrieve the maximum integer item id + 1
@@ -102,9 +102,9 @@
     return "{} {}".format(query, pep_check), values
 
 
+@implementer(iidavoll.IStorage)
 class Storage:
 
-    implements(iidavoll.IStorage)
 
     defaultConfig = {
             'leaf': {
@@ -435,7 +435,7 @@
         if node_accesses != ('open',) or item_accesses != ('open',):
             raise NotImplementedError('only "open" access model is handled for now')
         if not pep:
-            raise NotImplementedError(u"getLastItems is only implemented for PEP at the moment")
+            raise NotImplementedError("getLastItems is only implemented for PEP at the moment")
         d = self.dbpool.runQuery("""SELECT DISTINCT ON (node_id) pep, node, data::text, items.access_model
                                     FROM items
                                     NATURAL JOIN nodes
@@ -452,9 +452,9 @@
         return d
 
 
+@implementer(iidavoll.INode)
 class Node:
 
-    implements(iidavoll.INode)
 
     def __init__(self, nodeDbId, nodeIdentifier, config, schema):
         self.nodeDbId = nodeDbId
@@ -488,10 +488,10 @@
         if self._config[const.OPT_SERIAL_IDS]:
             d = self.dbpool.runQuery("SELECT nextval('{seq_name}')".format(
                 seq_name = ITEMS_SEQ_NAME.format(node_id=self.nodeDbId)))
-            d.addCallback(lambda rows: unicode(rows[0][0]))
+            d.addCallback(lambda rows: str(rows[0][0]))
             return d
         else:
-            return defer.succeed(unicode(uuid.uuid4()))
+            return defer.succeed(str(uuid.uuid4()))
 
     @staticmethod
     def _configurationTriggers(cursor, node_id, old_config, new_config):
@@ -645,7 +645,7 @@
 
         subscriptions = []
         for row in rows:
-            subscriber = jid.JID(u'%s/%s' % (row.jid, row.resource))
+            subscriber = jid.JID('%s/%s' % (row.jid, row.resource))
 
             options = {}
             if row.subscription_type:
@@ -730,7 +730,7 @@
         values = []
         for subscription in subscriptions:
             entity_id = entities_map[subscription.subscriber].entity_id
-            resource = subscription.subscriber.resource or u''
+            resource = subscription.subscriber.resource or ''
             values.append((self.nodeDbId, entity_id, resource, subscription.state, None, None))
         # we use upsert so new values are inserted and existing one updated. This feature is only available for PostgreSQL >= 9.5
         cursor.execute("INSERT INTO subscriptions(node_id, entity_id, resource, state, subscription_type, subscription_depth) VALUES " + placeholders + " ON CONFLICT (entity_id, resource, node_id) DO UPDATE SET state=EXCLUDED.state", [v for v in values])
@@ -808,7 +808,8 @@
         # then we construct values for affiliations update according to entity_id we just got
         placeholders = ','.join(len(affiliations) * ["(%s,%s,%s)"])
         values = []
-        map(values.extend, ((e.entity_id, affiliations[jid.JID(e.jid)], self.nodeDbId) for e in entities))
+        for e in entities:
+            values.extend((e.entity_id, affiliations[jid.JID(e.jid)], self.nodeDbId))
 
         # we use upsert so new values are inserted and existing one updated. This feature is only available for PostgreSQL >= 9.5
         cursor.execute("INSERT INTO affiliations(entity_id,affiliation,node_id) VALUES " + placeholders + " ON CONFLICT  (entity_id,node_id) DO UPDATE SET affiliation=EXCLUDED.affiliation", values)
@@ -836,9 +837,9 @@
         return [row[0] for row in rows]
 
 
+@implementer(iidavoll.ILeafNode)
 class LeafNode(Node):
 
-    implements(iidavoll.ILeafNode)
 
     nodeType = 'leaf'
 
@@ -851,7 +852,7 @@
         """
         keys = ext_data.get('order_by')
         if not keys:
-            return u'ORDER BY updated ' + direction
+            return 'ORDER BY updated ' + direction
         cols_statmnt = []
         for key in keys:
             if key == 'creation':
@@ -859,11 +860,11 @@
             elif key == 'modification':
                 column = 'updated'
             else:
-                log.msg(u"WARNING: Unknown order by key: {key}".format(key=key))
+                log.msg("WARNING: Unknown order by key: {key}".format(key=key))
                 column = 'updated'
-            cols_statmnt.append(column + u' ' + direction)
+            cols_statmnt.append(column + ' ' + direction)
 
-        return u"ORDER BY " + u",".join([col for col in cols_statmnt])
+        return "ORDER BY " + ",".join([col for col in cols_statmnt])
 
     @defer.inlineCallbacks
     def storeItems(self, items_data, publisher):
@@ -918,10 +919,10 @@
                     cursor.execute(NEXT_ITEM_ID_QUERY.format(node_id=self.nodeDbId))
                     next_id = cursor.fetchone()[0]
                     # we update the sequence, so we can skip conflicting ids
-                    cursor.execute(u"SELECT setval('{seq_name}', %s)".format(
+                    cursor.execute("SELECT setval('{seq_name}', %s)".format(
                         seq_name = ITEMS_SEQ_NAME.format(node_id=self.nodeDbId)), [next_id])
                     # and now we can retry the query with the new id
-                    item['id'] = insert_data[1] = unicode(next_id)
+                    item['id'] = insert_data[1] = str(next_id)
                     # item saved in DB must also be updated with the new id
                     insert_data[3] = item.toXml()
                     cursor.execute(insert_query, insert_data)
@@ -1059,7 +1060,7 @@
                         args.append(filter_.value)
                     else:
                         query_filters.append("AND publisher LIKE %s")
-                        args.append(u"{}%".format(filter_.value))
+                        args.append("{}%".format(filter_.value))
                 elif filter_.var == const.MAM_FILTER_CATEGORY:
                     query.append("LEFT JOIN item_categories USING (item_id)")
                     query_filters.append("AND category=%s")
--- a/sat_pubsub/privilege.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/privilege.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 #
 # Copyright (c) 2015 Jérôme Poisson
@@ -92,18 +92,18 @@
 
         self._permissions will be updated according to advertised privileged
         """
-        privilege_elt = message.elements(PRIV_ENT_NS, 'privilege').next()
+        privilege_elt = next(message.elements(PRIV_ENT_NS, 'privilege'))
         for perm_elt in privilege_elt.elements(PRIV_ENT_NS):
             try:
                 if perm_elt.name != 'perm':
-                    raise InvalidStanza(u'unexpected element {}'.format(perm_elt.name))
+                    raise InvalidStanza('unexpected element {}'.format(perm_elt.name))
                 perm_access = perm_elt['access']
                 perm_type = perm_elt['type']
                 try:
                     if perm_type not in TO_CHECK[perm_access]:
-                        raise InvalidStanza(u'bad type [{}] for permission {}'.format(perm_type, perm_access))
+                        raise InvalidStanza('bad type [{}] for permission {}'.format(perm_type, perm_access))
                 except KeyError:
-                    raise InvalidStanza(u'bad permission [{}]'.format(perm_access))
+                    raise InvalidStanza('bad permission [{}]'.format(perm_access))
             except InvalidStanza as e:
                 log.msg("Invalid stanza received ({}), setting permission to none".format(e))
                 for perm in self._permissions:
@@ -238,7 +238,7 @@
             self.roster_cache[from_jid_bare] = {'timestamp': timestamp,
                                                 'roster': roster,
                                                 }
-            for roster_jid, roster_item in roster.iteritems():
+            for roster_jid, roster_item in roster.items():
                 if roster_item.subscriptionFrom:
                     self.presence_map.setdefault(roster_jid, set()).add(from_jid_bare)
 
@@ -296,7 +296,7 @@
         """
         auto_subscribers = []
         roster = yield self.getRoster(recipient)
-        for roster_jid, roster_item in roster.iteritems():
+        for roster_jid, roster_item in roster.items():
             if roster_jid in explicit_subscribers:
                 continue
             if roster_item.subscriptionFrom:
@@ -304,7 +304,7 @@
                     online_resources = self.caps_map[roster_jid]
                 except KeyError:
                     continue
-                for res, disco_tuple in online_resources.iteritems():
+                for res, disco_tuple in online_resources.items():
                      notify = self.hash_map[disco_tuple]['notify']
                      if nodeIdentifier in notify:
                          full_jid = jid.JID(tuple=(roster_jid.user, roster_jid.host, res))
--- a/sat_pubsub/pubsub_admin.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/pubsub_admin.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 
 # Copyright (c) 2019 Jérôme Poisson
@@ -21,7 +21,7 @@
 
 """
 
-from zope.interface import implements
+from zope.interface import implementer
 from twisted.python import log
 from twisted.internet import defer
 from twisted.words.protocols.jabber import jid, error as jabber_error, xmlstream
@@ -29,12 +29,12 @@
 from wokkel.subprotocols import XMPPHandler
 from wokkel import disco, iwokkel, pubsub
 
-NS_PUBSUB_ADMIN = u"https://salut-a-toi.org/spec/pubsub_admin:0"
+NS_PUBSUB_ADMIN = "https://salut-a-toi.org/spec/pubsub_admin:0"
 ADMIN_REQUEST = '/iq[@type="set"]/admin[@xmlns="{}"]'.format(NS_PUBSUB_ADMIN)
 
 
+@implementer(iwokkel.IDisco)
 class PubsubAdminHandler(XMPPHandler):
-    implements(iwokkel.IDisco)
 
     def __init__(self, backend):
         super(PubsubAdminHandler, self).__init__()
@@ -43,7 +43,7 @@
     def connectionInitialized(self):
         self.xmlstream.addObserver(ADMIN_REQUEST, self.onAdminRequest)
 
-    def sendError(self, iq_elt, condition=u'bad-request'):
+    def sendError(self, iq_elt, condition='bad-request'):
         stanza_error = jabber_error.StanzaError(condition)
         iq_error = stanza_error.toResponse(iq_elt)
         self.parent.xmlstream.send(iq_error)
@@ -58,42 +58,42 @@
             pep = False
 
         # is the sender really an admin?
-        admins = self.backend.config[u'admins_jids_list']
-        from_jid = jid.JID(iq_elt[u'from'])
+        admins = self.backend.config['admins_jids_list']
+        from_jid = jid.JID(iq_elt['from'])
         if from_jid.userhostJID() not in admins:
             log.msg("WARNING: admin request done by non admin entity {from_jid}"
                 .format(from_jid=from_jid.full()))
-            self.sendError(iq_elt, u'forbidden')
+            self.sendError(iq_elt, 'forbidden')
             return
 
         # alright, we can proceed
-        recipient = jid.JID(iq_elt[u'to'])
+        recipient = jid.JID(iq_elt['to'])
         admin_elt = iq_elt.admin
         try:
-            pubsub_elt = next(admin_elt.elements(pubsub.NS_PUBSUB, u'pubsub'))
-            publish_elt = next(pubsub_elt.elements(pubsub.NS_PUBSUB, u'publish'))
+            pubsub_elt = next(admin_elt.elements(pubsub.NS_PUBSUB, 'pubsub'))
+            publish_elt = next(pubsub_elt.elements(pubsub.NS_PUBSUB, 'publish'))
         except StopIteration:
             self.sendError(iq_elt)
             return
         try:
-            node = publish_elt[u'node']
+            node = publish_elt['node']
         except KeyError:
             self.sendError(iq_elt)
             return
 
         # we prepare the result IQ request, we will fill it with item ids
-        iq_result_elt = xmlstream.toResponse(iq_elt, u'result')
-        result_admin_elt = iq_result_elt.addElement((NS_PUBSUB_ADMIN, u'admin'))
-        result_pubsub_elt = result_admin_elt.addElement((pubsub.NS_PUBSUB, u'pubsub'))
-        result_publish_elt = result_pubsub_elt.addElement(u'publish')
-        result_publish_elt[u'node'] = node
+        iq_result_elt = xmlstream.toResponse(iq_elt, 'result')
+        result_admin_elt = iq_result_elt.addElement((NS_PUBSUB_ADMIN, 'admin'))
+        result_pubsub_elt = result_admin_elt.addElement((pubsub.NS_PUBSUB, 'pubsub'))
+        result_publish_elt = result_pubsub_elt.addElement('publish')
+        result_publish_elt['node'] = node
 
         # now we can send the items
-        for item in publish_elt.elements(pubsub.NS_PUBSUB, u'item'):
+        for item in publish_elt.elements(pubsub.NS_PUBSUB, 'item'):
             try:
-                requestor = jid.JID(item.attributes.pop(u'publisher'))
+                requestor = jid.JID(item.attributes.pop('publisher'))
             except Exception as e:
-                log.msg(u"WARNING: invalid jid in publisher ({requestor}): {msg}"
+                log.msg("WARNING: invalid jid in publisher ({requestor}): {msg}"
                     .format(requestor=requestor, msg=e))
                 self.sendError(iq_elt)
                 return
@@ -111,20 +111,20 @@
                     recipient=recipient)
             except (error.Forbidden, error.ItemForbidden):
                 __import__('pudb').set_trace()
-                self.sendError(iq_elt, u"forbidden")
+                self.sendError(iq_elt, "forbidden")
                 return
             except Exception as e:
-                self.sendError(iq_elt, u"internal-server-error")
-                log.msg(u"INTERNAL ERROR: {msg}".format(msg=e))
+                self.sendError(iq_elt, "internal-server-error")
+                log.msg("INTERNAL ERROR: {msg}".format(msg=e))
                 return
 
-            result_item_elt = result_publish_elt.addElement(u'item')
+            result_item_elt = result_publish_elt.addElement('item')
             # either the id was given and it is available in item
             # either it's a new item, and we can retrieve it from return payload
             try:
-                result_item_elt[u'id'] = item[u'id']
+                result_item_elt['id'] = item['id']
             except KeyError:
-                result_item_elt = payload.publish.item[u'id']
+                result_item_elt = payload.publish.item['id']
 
         self.xmlstream.send(iq_result_elt)
 
--- a/sat_pubsub/schema.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/schema.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 #
 # Copyright (c) 2015 Jérôme Poisson
@@ -27,14 +27,14 @@
 from wokkel.iwokkel import IPubSubService
 from wokkel.subprotocols import XMPPHandler, IQHandlerMixin
 from wokkel import data_form, pubsub
-from zope.interface import implements
+from zope.interface import implementer
 from sat_pubsub import const
 
 QUERY_SCHEMA = "/pubsub[@xmlns='" + const.NS_SCHEMA + "']"
 
 
+@implementer(iwokkel.IDisco)
 class SchemaHandler(XMPPHandler, IQHandlerMixin):
-    implements(iwokkel.IDisco)
     iqHandlers = {"/iq[@type='get']" + QUERY_SCHEMA: 'onSchemaGet',
                   "/iq[@type='set']" + QUERY_SCHEMA: 'onSchemaSet'}
 
@@ -54,7 +54,7 @@
         schema_elt = domish.Element((const.NS_SCHEMA, 'schema'))
         schema_elt['node'] = nodeIdentifier
         if x_elt is not None:
-            assert x_elt.uri == u'jabber:x:data'
+            assert x_elt.uri == 'jabber:x:data'
             schema_elt.addChild(x_elt)
         return schema_elt
 
@@ -86,7 +86,7 @@
         pep = iq_elt.delegated
         recipient = jid.JID(iq_elt['to'])
         try:
-            x_elt = next(schema_elt.elements(data_form.NS_X_DATA, u'x'))
+            x_elt = next(schema_elt.elements(data_form.NS_X_DATA, 'x'))
         except StopIteration:
             # no schema form has been found
             x_elt = None
--- a/sat_pubsub/test/test_backend.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/test/test_backend.py	Fri Aug 16 12:53:33 2019 +0200
@@ -105,9 +105,9 @@
             return defer.succeed(None)
 
         def cb(result):
-            self.assertEquals(1, len(preDeleteCalled))
+            self.assertEqual(1, len(preDeleteCalled))
             data = preDeleteCalled[-1]
-            self.assertEquals('to-be-deleted', data['node'].nodeIdentifier)
+            self.assertEqual('to-be-deleted', data['node'].nodeIdentifier)
             self.assertTrue(self.storage.deleteCalled)
 
         self.storage = TestStorage()
@@ -150,10 +150,10 @@
             return defer.succeed(None)
 
         def cb(result):
-            self.assertEquals(1, len(preDeleteCalled))
+            self.assertEqual(1, len(preDeleteCalled))
             data = preDeleteCalled[-1]
-            self.assertEquals('to-be-deleted', data['node'].nodeIdentifier)
-            self.assertEquals(uri, data['redirectURI'])
+            self.assertEqual('to-be-deleted', data['node'].nodeIdentifier)
+            self.assertEqual(uri, data['redirectURI'])
             self.assertTrue(self.storage.deleteCalled)
 
         self.storage = TestStorage()
@@ -217,12 +217,12 @@
                 return [sub]
 
         def cb(result):
-            self.assertEquals(1, len(result))
+            self.assertEqual(1, len(result))
             subscriber, subscriptions, items = result[-1]
 
-            self.assertEquals(OWNER, subscriber)
-            self.assertEquals({sub}, subscriptions)
-            self.assertEquals([item], items)
+            self.assertEqual(OWNER, subscriber)
+            self.assertEqual({sub}, subscriptions)
+            self.assertEqual([item], items)
 
         self.storage = self.NodeStore({'test': TestNode()})
         self.backend = backend.BackendService(self.storage)
@@ -251,11 +251,11 @@
                 return [subRoot]
 
         def cb(result):
-            self.assertEquals(1, len(result))
+            self.assertEqual(1, len(result))
             subscriber, subscriptions, items = result[-1]
-            self.assertEquals(OWNER, subscriber)
-            self.assertEquals({subRoot}, subscriptions)
-            self.assertEquals([item], items)
+            self.assertEqual(OWNER, subscriber)
+            self.assertEqual({subRoot}, subscriptions)
+            self.assertEqual([item], items)
 
         self.storage = self.NodeStore({'test': TestNode(),
                                        '': TestRootNode()})
@@ -283,12 +283,12 @@
                 return [subRoot]
 
         def cb(result):
-            self.assertEquals(1, len(result))
+            self.assertEqual(1, len(result))
             subscriber, subscriptions, items = result[-1]
 
-            self.assertEquals(OWNER, subscriber)
-            self.assertEquals({sub, subRoot}, subscriptions)
-            self.assertEquals([item], items)
+            self.assertEqual(OWNER, subscriber)
+            self.assertEqual({sub, subRoot}, subscriptions)
+            self.assertEqual([item], items)
 
         self.storage = self.NodeStore({'test': TestNode(),
                                        '': TestRootNode()})
@@ -452,9 +452,9 @@
                 return defer.succeed(TestNode())
 
         def cb(data):
-            self.assertEquals('node', data['node'].nodeIdentifier)
-            self.assertEquals([ITEM], data['items'])
-            self.assertEquals(OWNER, data['subscription'].subscriber)
+            self.assertEqual('node', data['node'].nodeIdentifier)
+            self.assertEqual([ITEM], data['items'])
+            self.assertEqual(OWNER, data['subscription'].subscriber)
 
         self.storage = TestStorage()
         self.backend = backend.BackendService(self.storage)
@@ -616,7 +616,7 @@
                 return defer.fail(error.NotSubscribed())
 
         def cb(e):
-            self.assertEquals('unexpected-request', e.condition)
+            self.assertEqual('unexpected-request', e.condition)
 
         resource = backend.PubSubResourceFromBackend(TestBackend())
         request = pubsub.PubSubRequest()
@@ -644,9 +644,9 @@
 
         def cb(info):
             self.assertIn('type', info)
-            self.assertEquals('leaf', info['type'])
+            self.assertEqual('leaf', info['type'])
             self.assertIn('meta-data', info)
-            self.assertEquals({'pubsub#persist_items': True}, info['meta-data'])
+            self.assertEqual({'pubsub#persist_items': True}, info['meta-data'])
 
         resource = backend.PubSubResourceFromBackend(TestBackend())
         d = resource.getInfo(OWNER, SERVICE, 'test')
@@ -680,7 +680,7 @@
                 return defer.succeed(options)
 
         def cb(options):
-            self.assertEquals(True, options["pubsub#persist_items"])
+            self.assertEqual(True, options["pubsub#persist_items"])
 
         resource = backend.PubSubResourceFromBackend(TestBackend())
         request = pubsub.PubSubRequest()
--- a/sat_pubsub/test/test_storage.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/sat_pubsub/test/test_storage.py	Fri Aug 16 12:53:33 2019 +0200
@@ -70,16 +70,16 @@
 PUBLISHER = jid.JID('publisher@example.com')
 ITEM = domish.Element((None, 'item'))
 ITEM['id'] = 'current'
-ITEM.addElement(('testns', 'test'), content=u'Test \u2083 item')
+ITEM.addElement(('testns', 'test'), content='Test \u2083 item')
 ITEM_NEW = domish.Element((None, 'item'))
 ITEM_NEW['id'] = 'new'
-ITEM_NEW.addElement(('testns', 'test'), content=u'Test \u2083 item')
+ITEM_NEW.addElement(('testns', 'test'), content='Test \u2083 item')
 ITEM_UPDATED = domish.Element((None, 'item'))
 ITEM_UPDATED['id'] = 'current'
-ITEM_UPDATED.addElement(('testns', 'test'), content=u'Test \u2084 item')
+ITEM_UPDATED.addElement(('testns', 'test'), content='Test \u2084 item')
 ITEM_TO_BE_DELETED = domish.Element((None, 'item'))
 ITEM_TO_BE_DELETED['id'] = 'to-be-deleted'
-ITEM_TO_BE_DELETED.addElement(('testns', 'test'), content=u'Test \u2083 item')
+ITEM_TO_BE_DELETED.addElement(('testns', 'test'), content='Test \u2083 item')
 
 def decode(object):
     if isinstance(object, str):
@@ -230,8 +230,8 @@
 
     def test_getConfiguration(self):
         config = self.node.getConfiguration()
-        self.assertIn('pubsub#persist_items', config.iterkeys())
-        self.assertIn('pubsub#deliver_payloads', config.iterkeys())
+        self.assertIn('pubsub#persist_items', iter(config.keys()))
+        self.assertIn('pubsub#deliver_payloads', iter(config.keys()))
         self.assertEqual(config['pubsub#persist_items'], True)
         self.assertEqual(config['pubsub#deliver_payloads'], True)
 
@@ -263,10 +263,10 @@
 
     def test_getMetaData(self):
         metaData = self.node.getMetaData()
-        for key, value in self.node.getConfiguration().iteritems():
-            self.assertIn(key, metaData.iterkeys())
+        for key, value in self.node.getConfiguration().items():
+            self.assertIn(key, iter(metaData.keys()))
             self.assertEqual(value, metaData[key])
-        self.assertIn('pubsub#node_type', metaData.iterkeys())
+        self.assertIn('pubsub#node_type', iter(metaData.keys()))
         self.assertEqual(metaData['pubsub#node_type'], 'leaf')
 
 
@@ -309,9 +309,9 @@
 
     def test_getSubscription(self):
         def cb(subscriptions):
-            self.assertEquals(subscriptions[0].state, 'subscribed')
-            self.assertEquals(subscriptions[1].state, 'pending')
-            self.assertEquals(subscriptions[2], None)
+            self.assertEqual(subscriptions[0].state, 'subscribed')
+            self.assertEqual(subscriptions[1].state, 'pending')
+            self.assertEqual(subscriptions[2], None)
 
         d = defer.gatherResults([self.node.getSubscription(SUBSCRIBER),
                                  self.node.getSubscription(SUBSCRIBER_PENDING),
@@ -347,10 +347,10 @@
 
     def test_isSubscriber(self):
         def cb(subscribed):
-            self.assertEquals(subscribed[0][1], True)
-            self.assertEquals(subscribed[1][1], True)
-            self.assertEquals(subscribed[2][1], False)
-            self.assertEquals(subscribed[3][1], False)
+            self.assertEqual(subscribed[0][1], True)
+            self.assertEqual(subscribed[1][1], True)
+            self.assertEqual(subscribed[2][1], False)
+            self.assertEqual(subscribed[3][1], False)
 
         d = defer.DeferredList([self.node.isSubscribed(SUBSCRIBER),
                                 self.node.isSubscribed(SUBSCRIBER.userhostJID()),
@@ -471,7 +471,7 @@
 
         def cb2(affiliations):
             affiliations = dict(((a[0].full(), a[1]) for a in affiliations))
-            self.assertEquals(affiliations[OWNER.userhost()], 'owner')
+            self.assertEqual(affiliations[OWNER.userhost()], 'owner')
 
         d = self.s.getNode('pre-existing')
         d.addCallback(cb1)
--- a/setup.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/setup.py	Fri Aug 16 12:53:33 2019 +0200
@@ -52,8 +52,8 @@
 
 setup(name=NAME,
       version=VERSION,
-      description=u'XMPP Publish-Subscribe Service Component, build for the need of '
-                  u'the « Salut à Toi » project',
+      description='XMPP Publish-Subscribe Service Component, build for the need of '
+                  'the « Salut à Toi » project',
       author='Association « Salut à Toi »',
       author_email='goffi@goffi.org',
       url='https://salut-a-toi.org',
@@ -72,5 +72,5 @@
       use_scm_version=sat_dev_version if is_dev_version else False,
       install_requires=install_requires,
       package_data={'sat_pubsub': ['VERSION']},
-      python_requires='~=2.7',
+      python_requires='>=3.6',
       )
--- a/twisted/plugins/pubsub.py	Fri Aug 16 12:48:34 2019 +0200
+++ b/twisted/plugins/pubsub.py	Fri Aug 16 12:53:33 2019 +0200
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
 #-*- coding: utf-8 -*-
 
 # Copyright (c) 2012-2019 Jérôme Poisson
@@ -67,7 +67,7 @@
 from wokkel import pubsub
 from wokkel import rsm
 from wokkel import mam
-from zope.interface import implements
+from zope.interface import implementer
 
 from sat_pubsub import const
 from sat_pubsub import mam as pubsub_mam
@@ -77,19 +77,19 @@
 from sat_pubsub.privilege import PrivilegesHandler
 from sat_pubsub.delegation import DelegationsHandler
 from os.path import expanduser, realpath
-import ConfigParser
+import configparser
 
 
 def coerceListType(value):
-    return csv.reader(
+    return next(csv.reader(
         [value], delimiter=",", quotechar='"', skipinitialspace=True
-    ).next()
+    ))
 
 
 def coerceJidListType(value):
     values = [JID(v) for v in coerceListType(value)]
     if any((j.resource for j in values)):
-        raise ValueError(u"you must use bare jids")
+        raise ValueError("you must use bare jids")
     return values
 
 
@@ -112,7 +112,7 @@
      coerceJidListType]
     ]
 
-CONFIG_FILENAME = u'sat'
+CONFIG_FILENAME = 'sat'
 # List of the configuration filenames sorted by ascending priority
 CONFIG_FILES = [realpath(expanduser(path) + CONFIG_FILENAME + '.conf') for path in (
     '/etc/', '/etc/{}/'.format(CONFIG_FILENAME),
@@ -142,23 +142,21 @@
         # if the  options values are the hard-coded ones or if they have been passed on the command line.
 
         # FIXME: must be refactored + code can be factorised with backend
-        config_parser = ConfigParser.SafeConfigParser()
+        config_parser = configparser.ConfigParser()
         config_parser.read(CONFIG_FILES)
         for param in self.optParameters + OPT_PARAMETERS_CFG:
             name = param[0]
             try:
                 value = config_parser.get(CONFIG_SECTION, name)
-                if isinstance(value, unicode):
-                    value = value.encode('utf-8')
                 try:
                     param[2] = param[4](value)
                 except IndexError: # the coerce method is optional
                     param[2] = value
                 except Exception as e:
-                    log.err(u'Invalid value for setting "{name}": {msg}'.format(
+                    log.err('Invalid value for setting "{name}": {msg}'.format(
                         name=name, msg=e))
                     sys.exit(1)
-            except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
+            except (configparser.NoSectionError, configparser.NoOptionError):
                 pass
         usage.Options.__init__(self)
         for opt_data in OPT_PARAMETERS_CFG:
@@ -173,10 +171,10 @@
         self['jid'] = JID(self['jid']) if self['jid'] else None
 
 
+@implementer(IServiceMaker, IPlugin)
 class SatPubsubMaker(object):
-    implements(IServiceMaker, IPlugin)
     tapname = "sat-pubsub"
-    description = u"Salut à Toi Publish-Subscribe Service Component".encode('utf-8')
+    description = "Salut à Toi Publish-Subscribe Service Component"
     options = Options
 
     def makeService(self, config):
@@ -198,7 +196,7 @@
                 'db_port': 'port',
             }
             kwargs = {}
-            for config_k, k in keys_map.iteritems():
+            for config_k, k in keys_map.items():
                 v = config.get(config_k)
                 if v is None:
                     continue
@@ -230,7 +228,7 @@
             cs.logTraffic = True
 
         FallbackHandler().setHandlerParent(cs)
-        VersionHandler(u'SàT Pubsub', sat_pubsub.__version__).setHandlerParent(cs)
+        VersionHandler('SàT Pubsub', sat_pubsub.__version__).setHandlerParent(cs)
         DiscoHandler().setHandlerParent(cs)
 
         ph = PrivilegesHandler(config['jid'])