From 77c8caf20557b17296896544a3414164696523ef Mon Sep 17 00:00:00 2001
From: mathieui <mathieui@mathieui.net>
Date: Thu, 4 Feb 2021 20:27:07 +0100
Subject: XEP-0012: Add type hints and switch to default args

---
 slixmpp/plugins/xep_0012/last_activity.py | 38 +++++++++++++++++++------------
 1 file changed, 24 insertions(+), 14 deletions(-)

diff --git a/slixmpp/plugins/xep_0012/last_activity.py b/slixmpp/plugins/xep_0012/last_activity.py
index 807c0e05..bd21252b 100644
--- a/slixmpp/plugins/xep_0012/last_activity.py
+++ b/slixmpp/plugins/xep_0012/last_activity.py
@@ -7,10 +7,16 @@
 """
 
 import logging
+from asyncio import Future
 from datetime import datetime, timedelta
+from typing import (
+    Dict,
+    Optional
+)
 
 from slixmpp.plugins import BasePlugin, register_plugin
-from slixmpp import future_wrapper, Iq
+from slixmpp import future_wrapper, JID
+from slixmpp.stanza import Iq
 from slixmpp.exceptions import XMPPError
 from slixmpp.xmlstream import JID, register_stanza_plugin
 from slixmpp.xmlstream.handler import Callback
@@ -59,7 +65,11 @@ class XEP_0012(BasePlugin):
     def session_bind(self, jid):
         self.xmpp['xep_0030'].add_feature('jabber:iq:last')
 
-    def begin_idle(self, jid=None, status=None):
+    def begin_idle(self, jid: Optional[JID] = None, status: str = None):
+        """Reset the last activity for the given JID.
+
+        :param status: Optional status.
+        """
         self.set_last_activity(jid, 0, status)
 
     def end_idle(self, jid=None):
@@ -77,8 +87,12 @@ class XEP_0012(BasePlugin):
         self.api['del_last_activity'](jid)
 
     @future_wrapper
-    def get_last_activity(self, jid, local=False, ifrom=None, timeout=None,
-                          callback=None, timeout_callback=None):
+    def get_last_activity(self, jid: JID, local: bool = False,
+                          ifrom: Optional[JID] = None, **iqkwargs) -> Future:
+        """Get last activity for a specific JID.
+
+        :param local: Fetch the value from the local cache.
+        """
         if jid is not None and not isinstance(jid, JID):
             jid = JID(jid)
 
@@ -94,15 +108,11 @@ class XEP_0012(BasePlugin):
             log.debug("Looking up local last activity data for %s", jid)
             return self.api['get_last_activity'](jid, None, ifrom, None)
 
-        iq = self.xmpp.Iq()
-        iq['from'] = ifrom
-        iq['to'] = jid
-        iq['type'] = 'get'
+        iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom)
         iq.enable('last_activity')
-        return iq.send(timeout=timeout, callback=callback,
-                       timeout_callback=timeout_callback)
+        return iq.send(**iqkwargs)
 
-    def _handle_get_last_activity(self, iq):
+    def _handle_get_last_activity(self, iq: Iq):
         log.debug("Received last activity query from " + \
                   "<%s> to <%s>.", iq['from'], iq['to'])
         reply = self.api['get_last_activity'](iq['to'], None, iq['from'], iq)
@@ -112,7 +122,7 @@ class XEP_0012(BasePlugin):
     # Default in-memory implementations for storing last activity data.
     # =================================================================
 
-    def _default_set_last_activity(self, jid, node, ifrom, data):
+    def _default_set_last_activity(self, jid: JID, node: str, ifrom: JID, data: Dict):
         seconds = data.get('seconds', None)
         if seconds is None:
             seconds = 0
@@ -125,11 +135,11 @@ class XEP_0012(BasePlugin):
             'seconds': datetime.now() - timedelta(seconds=seconds),
             'status': status}
 
-    def _default_del_last_activity(self, jid, node, ifrom, data):
+    def _default_del_last_activity(self, jid: JID, node: str, ifrom: JID, data: Dict):
         if jid in self._last_activities:
             del self._last_activities[jid]
 
-    def _default_get_last_activity(self, jid, node, ifrom, iq):
+    def _default_get_last_activity(self, jid: JID, node: str, ifrom: JID, iq: Iq) -> Iq:
         if not isinstance(iq, Iq):
             reply = self.xmpp.Iq()
         else:
-- 
cgit v1.2.3


From fbb899324495ff76400815b91649436ca75474f2 Mon Sep 17 00:00:00 2001
From: mathieui <mathieui@mathieui.net>
Date: Thu, 4 Feb 2021 20:35:22 +0100
Subject: XEP-0049: Add type hints and switch to default args

---
 slixmpp/plugins/xep_0049/private_storage.py | 42 ++++++++++++++++-------------
 1 file changed, 24 insertions(+), 18 deletions(-)

diff --git a/slixmpp/plugins/xep_0049/private_storage.py b/slixmpp/plugins/xep_0049/private_storage.py
index cb92db51..22286f92 100644
--- a/slixmpp/plugins/xep_0049/private_storage.py
+++ b/slixmpp/plugins/xep_0049/private_storage.py
@@ -7,12 +7,19 @@
 """
 
 import logging
-
-from slixmpp import Iq
+from typing import (
+    List,
+    Optional,
+    Union,
+)
+from asyncio import Future
+
+from slixmpp import JID
+from slixmpp.stanza import Iq
 from slixmpp.plugins import BasePlugin
 from slixmpp.xmlstream.handler import Callback
 from slixmpp.xmlstream.matcher import StanzaPath
-from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream import register_stanza_plugin, ElementBase
 from slixmpp.plugins.xep_0049 import stanza, PrivateXML
 
 
@@ -32,26 +39,25 @@ class XEP_0049(BasePlugin):
     def register(self, stanza):
         register_stanza_plugin(PrivateXML, stanza, iterable=True)
 
-    def store(self, data, ifrom=None, timeout=None, callback=None,
-              timeout_callback=None):
-        iq = self.xmpp.Iq()
-        iq['type'] = 'set'
-        iq['from'] = ifrom
+    def store(self, data: Union[List[ElementBase], ElementBase], ifrom: Optional[JID] = None, **iqkwargs) -> Future:
+        """Store data in Private XML Storage.
+
+        :param data: An XML element or list of xml element to store.
+        """
+        iq = self.xmpp.make_iq_set(ifrom=ifrom)
 
         if not isinstance(data, list):
             data = [data]
-
         for elem in data:
             iq['private'].append(elem)
 
-        return iq.send(timeout=timeout, callback=callback,
-                       timeout_callback=timeout_callback)
+        return iq.send(**iqkwargs)
+
+    def retrieve(self, name: str, ifrom: Optional[JID] = None, **iqkwargs) -> Future:
+        """Get previously stored data from Private XML Storage.
 
-    def retrieve(self, name, ifrom=None, timeout=None, callback=None,
-                 timeout_callback=None):
-        iq = self.xmpp.Iq()
-        iq['type'] = 'get'
-        iq['from'] = ifrom
+        :param name: Name of the payload to retrieve (slixmpp plugin attribute)
+        """
+        iq = self.xmpp.make_iq_get(ifrom=ifrom)
         iq['private'].enable(name)
-        return iq.send(timeout=timeout, callback=callback,
-                timeout_callback=timeout_callback)
+        return iq.send(**iqkwargs)
-- 
cgit v1.2.3


From 7860edefe9dfb34815433cacdb239d9180a92be8 Mon Sep 17 00:00:00 2001
From: mathieui <mathieui@mathieui.net>
Date: Thu, 4 Feb 2021 21:01:35 +0100
Subject: XEP-0054: Add type hints and switch to default args

---
 slixmpp/plugins/xep_0054/vcard_temp.py | 50 ++++++++++++++++++++--------------
 1 file changed, 30 insertions(+), 20 deletions(-)

diff --git a/slixmpp/plugins/xep_0054/vcard_temp.py b/slixmpp/plugins/xep_0054/vcard_temp.py
index 214746e9..12840052 100644
--- a/slixmpp/plugins/xep_0054/vcard_temp.py
+++ b/slixmpp/plugins/xep_0054/vcard_temp.py
@@ -7,8 +7,11 @@
 """
 
 import logging
+from asyncio import Future
+from typing import Optional
 
-from slixmpp import JID, Iq
+from slixmpp import JID
+from slixmpp.stanza import Iq
 from slixmpp.exceptions import XMPPError
 from slixmpp.xmlstream import register_stanza_plugin
 from slixmpp.xmlstream.handler import Callback
@@ -57,12 +60,22 @@ class XEP_0054(BasePlugin):
     def session_bind(self, jid):
         self.xmpp['xep_0030'].add_feature('vcard-temp')
 
-    def make_vcard(self):
+    def make_vcard(self) -> VCardTemp:
+        """Return an empty vcard element."""
         return VCardTemp()
 
     @future_wrapper
-    def get_vcard(self, jid=None, ifrom=None, local=None, cached=False,
-                  callback=None, timeout=None, timeout_callback=None):
+    def get_vcard(self, jid: Optional[JID] = None, *,
+                  local: Optional[bool] = None, cached: bool = False,
+                  ifrom: Optional[JID] = None,
+                  **iqkwargs) -> Future:
+        """Retrieve a VCard.
+
+        :param jid: JID of the entity to fetch the VCard from.
+        :param local: Only check internally for a vcard.
+        :param cached: Whether to check in the local cache before
+                       sending a query.
+        """
         if local is None:
             if jid is not None and not isinstance(jid, JID):
                 jid = JID(jid)
@@ -95,31 +108,28 @@ class XEP_0054(BasePlugin):
                     return iq
                 return vcard
 
-        iq = self.xmpp.Iq()
-        iq['to'] = jid
-        iq['from'] = ifrom
-        iq['type'] = 'get'
+        iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom)
         iq.enable('vcard_temp')
-
-        return iq.send(callback=callback, timeout=timeout,
-                       timeout_callback=timeout_callback)
+        return iq.send(**iqkwargs)
 
     @future_wrapper
-    def publish_vcard(self, vcard=None, jid=None, ifrom=None,
-                      callback=None, timeout=None, timeout_callback=None):
+    def publish_vcard(self, vcard: Optional[VCardTemp] = None,
+                      jid: Optional[JID] = None,
+                      ifrom: Optional[JID] = None, **iqkwargs) -> Future:
+        """Publish a vcard.
+
+        :param vcard: The VCard to publish.
+        :param jid: The JID to publish the VCard to.
+        """
         self.api['set_vcard'](jid, None, ifrom, vcard)
         if self.xmpp.is_component:
             return
 
-        iq = self.xmpp.Iq()
-        iq['to'] = jid
-        iq['from'] = ifrom
-        iq['type'] = 'set'
+        iq = self.xmpp.make_iq_set(ito=jid, ifrom=ifrom)
         iq.append(vcard)
-        return iq.send(callback=callback, timeout=timeout,
-                       timeout_callback=timeout_callback)
+        return iq.send(**iqkwargs)
 
-    def _handle_get_vcard(self, iq):
+    def _handle_get_vcard(self, iq: Iq):
         if iq['type'] == 'result':
             self.api['set_vcard'](jid=iq['from'], args=iq['vcard_temp'])
             return
-- 
cgit v1.2.3


From 8e612bf2295a2d0a9867b789d042a94fbde93baa Mon Sep 17 00:00:00 2001
From: mathieui <mathieui@mathieui.net>
Date: Thu, 4 Feb 2021 21:23:17 +0100
Subject: XEP-0084: Add type hints and switch to default args

---
 slixmpp/plugins/xep_0084/avatar.py | 115 +++++++++++++++++++++++++------------
 1 file changed, 79 insertions(+), 36 deletions(-)

diff --git a/slixmpp/plugins/xep_0084/avatar.py b/slixmpp/plugins/xep_0084/avatar.py
index e434d39c..536c4c24 100644
--- a/slixmpp/plugins/xep_0084/avatar.py
+++ b/slixmpp/plugins/xep_0084/avatar.py
@@ -6,15 +6,49 @@
     See the file LICENSE for copying permission.
 """
 
+from __future__ import annotations
+
 import hashlib
 import logging
 
-from slixmpp import Iq
+from asyncio import Future
+from typing import (
+    Dict,
+    Iterable,
+    List,
+    Optional,
+    Set,
+    Union,
+    TYPE_CHECKING,
+)
+
+from slixmpp.stanza import Iq
 from slixmpp.plugins import BasePlugin
 from slixmpp.xmlstream.handler import Callback
 from slixmpp.xmlstream.matcher import StanzaPath
 from slixmpp.xmlstream import register_stanza_plugin, JID
-from slixmpp.plugins.xep_0084 import stanza, Data, MetaData
+from slixmpp.plugins.xep_0084.stanza import Data, MetaData, Pointer
+from slixmpp.plugins.xep_0084 import stanza
+
+try:
+    from typing import TypedDict
+except ImportError:
+    from typing_extensions import TypedDict
+
+
+class AvatarMetadataItem(TypedDict, total=False):
+    bytes: int
+    id: str
+    type: str
+    height: int
+    width: int
+    url: str
+
+MetadataItems = Union[
+    AvatarMetadataItem,
+    List[AvatarMetadataItem],
+    Set[AvatarMetadataItem]
+]
 
 
 log = logging.getLogger(__name__)
@@ -41,32 +75,43 @@ class XEP_0084(BasePlugin):
     def session_bind(self, jid):
         self.xmpp['xep_0163'].register_pep('avatar_metadata', MetaData)
 
-    def generate_id(self, data):
+    def generate_id(self, data) -> str:
         return hashlib.sha1(data).hexdigest()
 
-    def retrieve_avatar(self, jid, id, url=None, ifrom=None,
-                              callback=None, timeout=None, timeout_callback=None):
-        return self.xmpp['xep_0060'].get_item(jid, Data.namespace, id,
-                ifrom=ifrom,
-                callback=callback,
-                timeout=timeout,
-                timeout_callback=timeout_callback)
+    def retrieve_avatar(self, jid: JID, id: str, **pubsubkwargs) -> Future:
+        """Retrieve an avatar.
 
-    def publish_avatar(self, data, ifrom=None, callback=None,
-                             timeout=None, timeout_callback=None):
+        :param jid: JID of the entity to get the avatar from.
+        :param id: Identifier of the item containing the avatar.
+        """
+        return self.xmpp['xep_0060'].get_item(
+            jid,
+            Data.namespace,
+            id,
+            **pubsubkwargs
+        )
+
+    def publish_avatar(self, data: bytes, **pubsubkwargs) -> Future:
+        """Publish an avatar.
+
+        :param data: The avatar, in bytes representation.
+        """
         payload = Data()
         payload['value'] = data
-        return self.xmpp['xep_0163'].publish(payload,
-                id=self.generate_id(data),
-                ifrom=ifrom,
-                callback=callback,
-                timeout=timeout,
-                timeout_callback=timeout_callback)
-
-    def publish_avatar_metadata(self, items=None, pointers=None,
-                                      ifrom=None,
-                                      callback=None, timeout=None,
-                                      timeout_callback=None):
+        return self.xmpp['xep_0163'].publish(
+            payload,
+            id=self.generate_id(data),
+            **pubsubkwargs
+        )
+
+    def publish_avatar_metadata(self, items: Optional[MetadataItems] = None,
+                                pointers: Optional[Iterable[Pointer]] = None,
+                                **pubsubkwargs) -> Future:
+        """Publish avatar metadata.
+
+        :param items: Metadata items to store
+        :param pointers: Optional pointers
+        """
         metadata = MetaData()
         if items is None:
             items = []
@@ -82,21 +127,19 @@ class XEP_0084(BasePlugin):
             for pointer in pointers:
                 metadata.add_pointer(pointer)
 
-        return self.xmpp['xep_0163'].publish(metadata,
-                id=info['id'],
-                ifrom=ifrom,
-                callback=callback,
-                timeout=timeout,
-                timeout_callback=timeout_callback)
+        return self.xmpp['xep_0163'].publish(
+            metadata,
+            id=info['id'],
+            **pubsubkwargs
+        )
 
-    def stop(self, ifrom=None, callback=None, timeout=None, timeout_callback=None):
+    def stop(self, **pubsubkwargs) -> Future:
         """
         Clear existing avatar metadata information to stop notifications.
         """
         metadata = MetaData()
-        return self.xmpp['xep_0163'].publish(metadata,
-                node=MetaData.namespace,
-                ifrom=ifrom,
-                callback=callback,
-                timeout=timeout,
-                timeout_callback=timeout_callback)
+        return self.xmpp['xep_0163'].publish(
+            metadata,
+            node=MetaData.namespace,
+            **pubsubkwargs
+        )
-- 
cgit v1.2.3


From 99c2e5cafdcb748e41b88e4e21a2cbe9d9c66b12 Mon Sep 17 00:00:00 2001
From: mathieui <mathieui@mathieui.net>
Date: Thu, 4 Feb 2021 21:26:20 +0100
Subject: XEP-0092: Add type hints and switch to default args

---
 slixmpp/plugins/xep_0092/version.py | 25 +++++++++++--------------
 1 file changed, 11 insertions(+), 14 deletions(-)

diff --git a/slixmpp/plugins/xep_0092/version.py b/slixmpp/plugins/xep_0092/version.py
index a60acd99..3a7b949f 100644
--- a/slixmpp/plugins/xep_0092/version.py
+++ b/slixmpp/plugins/xep_0092/version.py
@@ -8,8 +8,12 @@
 
 import logging
 
+from asyncio import Future
+from typing import Optional
+
 import slixmpp
-from slixmpp import Iq
+from slixmpp import JID
+from slixmpp.stanza import Iq
 from slixmpp.xmlstream import register_stanza_plugin
 from slixmpp.xmlstream.handler import Callback
 from slixmpp.xmlstream.matcher import StanzaPath
@@ -57,12 +61,11 @@ class XEP_0092(BasePlugin):
     def session_bind(self, jid):
         self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version')
 
-    def _handle_version(self, iq):
+    def _handle_version(self, iq: Iq):
         """
         Respond to a software version query.
 
-        Arguments:
-            iq -- The Iq stanza containing the software version query.
+        :param iq: The Iq stanza containing the software version query.
         """
         iq = iq.reply()
         if self.software_name:
@@ -75,18 +78,12 @@ class XEP_0092(BasePlugin):
             iq['error']['condition'] = 'service-unavailable'
         iq.send()
 
-    def get_version(self, jid, ifrom=None, timeout=None, callback=None,
-                    timeout_callback=None):
+    def get_version(self, jid: JID, ifrom: Optional[JID] = None, **iqkwargs) -> Future:
         """
         Retrieve the software version of a remote agent.
 
-        Arguments:
-            jid -- The JID of the entity to query.
+        :param jid: The JID of the entity to query.
         """
-        iq = self.xmpp.Iq()
-        iq['to'] = jid
-        iq['from'] = ifrom
-        iq['type'] = 'get'
+        iq = self.xmpp.make_iq_get(ito=jid, ifrom=ifrom)
         iq['query'] = Version.namespace
-        return iq.send(timeout=timeout, callback=callback,
-                       timeout_callback=timeout_callback)
+        return iq.send(**iqkwargs)
-- 
cgit v1.2.3


From 712ac671e14b563375d43607e11d8007e10e05c3 Mon Sep 17 00:00:00 2001
From: mathieui <mathieui@mathieui.net>
Date: Thu, 4 Feb 2021 21:28:18 +0100
Subject: XEP-0152: Add type hints and switch to default args

---
 slixmpp/plugins/xep_0152/reachability.py | 39 ++++++++++++--------------------
 1 file changed, 15 insertions(+), 24 deletions(-)

diff --git a/slixmpp/plugins/xep_0152/reachability.py b/slixmpp/plugins/xep_0152/reachability.py
index 0ca0ae68..3c1a23aa 100644
--- a/slixmpp/plugins/xep_0152/reachability.py
+++ b/slixmpp/plugins/xep_0152/reachability.py
@@ -8,6 +8,8 @@
 
 import logging
 
+from asyncio import Future
+
 from slixmpp import JID
 from typing import Dict, List, Optional, Callable
 from slixmpp.plugins.base import BasePlugin
@@ -37,17 +39,12 @@ class XEP_0152(BasePlugin):
         self.xmpp['xep_0163'].register_pep('reachability', Reachability)
 
     def publish_reachability(self, addresses: List[Dict[str, str]],
-                             options: Optional[Form] = None,
-                             ifrom: Optional[JID] = None,
-                             callback: Optional[Callable] = None,
-                             timeout: Optional[int] = None,
-                             timeout_callback: Optional[Callable] = None):
+                             **pubsubkwargs) -> Future:
         """
         Publish alternative addresses where the user can be reached.
 
         :param addresses: A list of dictionaries containing the URI and
                           optional description for each address.
-        :param options: Optional form of publish options.
         """
         if not isinstance(addresses, (list, tuple)):
             addresses = [addresses]
@@ -60,25 +57,19 @@ class XEP_0152(BasePlugin):
             for key, val in address.items():
                 addr[key] = val
             reach.append(addr)
-        return self.xmpp['xep_0163'].publish(reach,
-                node=Reachability.namespace,
-                options=options,
-                ifrom=ifrom,
-                callback=callback,
-                timeout=timeout,
-                timeout_callback=timeout_callback)
-
-    def stop(self, ifrom: Optional[JID] = None,
-             callback: Optional[Callable] = None,
-             timeout: Optional[int] = None,
-             timeout_callback: Optional[Callable] = None):
+        return self.xmpp['xep_0163'].publish(
+            reach,
+            node=Reachability.namespace,
+            **pubsubkwargs
+        )
+
+    def stop(self, **pubsubkwargs) -> Future:
         """
         Clear existing user activity information to stop notifications.
         """
         reach = Reachability()
-        return self.xmpp['xep_0163'].publish(reach,
-                node=Reachability.namespace,
-                ifrom=ifrom,
-                callback=callback,
-                timeout=timeout,
-                timeout_callback=timeout_callback)
+        return self.xmpp['xep_0163'].publish(
+            reach,
+            node=Reachability.namespace,
+            **pubsubkwargs
+        )
-- 
cgit v1.2.3


From ace5aeb80e28822635b80176ec39daab3cffdfa1 Mon Sep 17 00:00:00 2001
From: mathieui <mathieui@mathieui.net>
Date: Thu, 4 Feb 2021 21:30:55 +0100
Subject: XEP-0153: Fix a bug that would add muc elements to inbound presence

---
 slixmpp/plugins/xep_0153/vcard_avatar.py | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/slixmpp/plugins/xep_0153/vcard_avatar.py b/slixmpp/plugins/xep_0153/vcard_avatar.py
index cf10283a..0492042d 100644
--- a/slixmpp/plugins/xep_0153/vcard_avatar.py
+++ b/slixmpp/plugins/xep_0153/vcard_avatar.py
@@ -152,13 +152,14 @@ class XEP_0153(BasePlugin):
         self.xmpp['xep_0054'].get_vcard(jid=jid.bare, ifrom=ifrom,
                                         callback=callback)
 
-    def _recv_presence(self, pres):
+    def _recv_presence(self, pres: Presence):
         try:
-            if pres['muc']['affiliation']:
+            if pres.get_plugin('muc', check=True):
                 # Don't process vCard avatars for MUC occupants
                 # since they all share the same bare JID.
                 return
-        except: pass
+        except:
+            pass
 
         if not pres.match('presence/vcard_temp_update'):
             self.api['set_hash'](pres['from'], args=None)
-- 
cgit v1.2.3


From 69e04d7d2ef1cc2a00df1a74bd028d42244078ee Mon Sep 17 00:00:00 2001
From: mathieui <mathieui@mathieui.net>
Date: Thu, 4 Feb 2021 21:54:14 +0100
Subject: XEP-0153: Add typing and switch to default args

(and refactor a bit)
---
 slixmpp/plugins/xep_0153/vcard_avatar.py | 126 ++++++++++++++++---------------
 1 file changed, 65 insertions(+), 61 deletions(-)

diff --git a/slixmpp/plugins/xep_0153/vcard_avatar.py b/slixmpp/plugins/xep_0153/vcard_avatar.py
index 0492042d..ef328acc 100644
--- a/slixmpp/plugins/xep_0153/vcard_avatar.py
+++ b/slixmpp/plugins/xep_0153/vcard_avatar.py
@@ -8,13 +8,19 @@
 
 import hashlib
 import logging
+from asyncio import Future, ensure_future
+from typing import (
+    Dict,
+    Optional,
+)
 
+from slixmpp import JID
 from slixmpp.stanza import Presence
 from slixmpp.exceptions import XMPPError, IqTimeout
-from slixmpp.xmlstream import register_stanza_plugin
+from slixmpp.xmlstream import register_stanza_plugin, ElementBase
 from slixmpp.plugins.base import BasePlugin
 from slixmpp.plugins.xep_0153 import stanza, VCardTempUpdate
-from slixmpp import asyncio, future_wrapper
+from slixmpp import future_wrapper
 
 
 log = logging.getLogger(__name__)
@@ -35,7 +41,6 @@ class XEP_0153(BasePlugin):
         self.xmpp.add_filter('out', self._update_presence)
 
         self.xmpp.add_event_handler('session_start', self._start)
-        self.xmpp.add_event_handler('session_end', self._end)
 
         self.xmpp.add_event_handler('presence_available', self._recv_presence)
         self.xmpp.add_event_handler('presence_dnd', self._recv_presence)
@@ -58,45 +63,47 @@ class XEP_0153(BasePlugin):
         self.xmpp.del_event_handler('presence_away', self._recv_presence)
 
     @future_wrapper
-    def set_avatar(self, jid=None, avatar=None, mtype=None, timeout=None,
-                   callback=None, timeout_callback=None):
+    def set_avatar(self, jid: Optional[JID] = None,
+                   avatar: Optional[bytes] = None,
+                   mtype: Optional[str] = None, **iqkwargs) -> Future:
+        """Set a VCard avatar.
+
+        :param jid: The JID to set the avatar for.
+        :param avatar: Avatar content.
+        :param mtype: Avatar file type (e.g. image/jpeg).
+        """
         if jid is None:
             jid = self.xmpp.boundjid.bare
-
-        future = asyncio.Future()
-
-        def propagate_timeout_exception(fut):
+        async def get_and_set_avatar():
+            timeout = iqkwargs.get('timeout', None)
+            timeout_cb = iqkwargs.get('timeout_callback', None)
             try:
-                fut.done()
-            except IqTimeout as e:
-                future.set_exception(e)
-
-        def custom_callback(result):
+                result = await self.xmpp['xep_0054'].get_vcard(
+                    jid,
+                    cached=False,
+                    timeout=timeout
+                )
+            except IqTimeout as exc:
+                if timeout_cb is not None:
+                    timeout_cb(exc)
+                raise
             vcard = result['vcard_temp']
             vcard['PHOTO']['TYPE'] = mtype
             vcard['PHOTO']['BINVAL'] = avatar
 
-            new_future = self.xmpp['xep_0054'].publish_vcard(jid=jid,
-                                                             vcard=vcard,
-                                                             timeout=timeout,
-                                                             callback=next_callback,
-                                                             timeout_callback=timeout_callback)
-            new_future.add_done_callback(propagate_timeout_exception)
-
-        def next_callback(result):
-            if result['type'] == 'error':
-                future.set_exception(result)
-            else:
-                self.api['reset_hash'](jid)
-                self.xmpp.roster[jid].send_last_presence()
-
-                future.set_result(result)
+            try:
+                result = await self.xmpp['xep_0054'].publish_vcard(
+                    jid=jid,
+                    vcard=vcard,
+                    **iqkwargs
+                )
+            except IqTimeout as exc:
+                timeout_cb(exc)
+                raise
+            self.api['reset_hash'](jid)
+            self.xmpp.roster[jid].send_last_presence()
 
-        first_future = self.xmpp['xep_0054'].get_vcard(jid, cached=False, timeout=timeout,
-                                                       callback=custom_callback,
-                                                       timeout_callback=timeout_callback)
-        first_future.add_done_callback(propagate_timeout_exception)
-        return future
+        return ensure_future(get_and_set_avatar(), loop=self.xmpp.loop)
 
     async def _start(self, event):
         try:
@@ -110,10 +117,7 @@ class XEP_0153(BasePlugin):
         except XMPPError:
             log.debug('Could not retrieve vCard for %s', self.xmpp.boundjid.bare)
 
-    def _end(self, event):
-        pass
-
-    def _update_presence(self, stanza):
+    def _update_presence(self, stanza: ElementBase) -> ElementBase:
         if not isinstance(stanza, Presence):
             return stanza
 
@@ -124,7 +128,27 @@ class XEP_0153(BasePlugin):
         stanza['vcard_temp_update']['photo'] = current_hash
         return stanza
 
-    def _reset_hash(self, jid, node, ifrom, args):
+    def _recv_presence(self, pres: Presence):
+        try:
+            if pres.get_plugin('muc', check=True):
+                # Don't process vCard avatars for MUC occupants
+                # since they all share the same bare JID.
+                return
+        except:
+            pass
+
+        if not pres.match('presence/vcard_temp_update'):
+            self.api['set_hash'](pres['from'], args=None)
+            return
+
+        data = pres['vcard_temp_update']['photo']
+        if data is None:
+            return
+        self.xmpp.event('vcard_avatar_update', pres)
+
+    # =================================================================
+
+    def _reset_hash(self, jid: JID, node: str, ifrom: JID, args: Dict):
         own_jid = (jid.bare == self.xmpp.boundjid.bare)
         if self.xmpp.is_component:
             own_jid = (jid.domain == self.xmpp.boundjid.domain)
@@ -152,28 +176,8 @@ class XEP_0153(BasePlugin):
         self.xmpp['xep_0054'].get_vcard(jid=jid.bare, ifrom=ifrom,
                                         callback=callback)
 
-    def _recv_presence(self, pres: Presence):
-        try:
-            if pres.get_plugin('muc', check=True):
-                # Don't process vCard avatars for MUC occupants
-                # since they all share the same bare JID.
-                return
-        except:
-            pass
-
-        if not pres.match('presence/vcard_temp_update'):
-            self.api['set_hash'](pres['from'], args=None)
-            return
-
-        data = pres['vcard_temp_update']['photo']
-        if data is None:
-            return
-        self.xmpp.event('vcard_avatar_update', pres)
-
-    # =================================================================
-
-    def _get_hash(self, jid, node, ifrom, args):
+    def _get_hash(self, jid: JID, node: str, ifrom: JID, args: Dict):
         return self._hashes.get(jid.bare, None)
 
-    def _set_hash(self, jid, node, ifrom, args):
+    def _set_hash(self, jid: JID, node: str, ifrom: JID, args: Dict):
         self._hashes[jid.bare] = args
-- 
cgit v1.2.3


From cb8d2edc8d604b4f5125d492d27fe68a29c16a6c Mon Sep 17 00:00:00 2001
From: mathieui <mathieui@mathieui.net>
Date: Thu, 4 Feb 2021 22:05:50 +0100
Subject: XEP-0191: Add type hints and switch to default args

---
 slixmpp/plugins/xep_0191/blocking.py | 57 +++++++++++++++++++++---------------
 1 file changed, 34 insertions(+), 23 deletions(-)

diff --git a/slixmpp/plugins/xep_0191/blocking.py b/slixmpp/plugins/xep_0191/blocking.py
index 5425544d..53c279df 100644
--- a/slixmpp/plugins/xep_0191/blocking.py
+++ b/slixmpp/plugins/xep_0191/blocking.py
@@ -8,7 +8,15 @@
 
 import logging
 
-from slixmpp import Iq
+from asyncio import Future
+from typing import (
+    List,
+    Optional,
+    Set,
+    Union,
+)
+
+from slixmpp.stanza import Iq
 from slixmpp.plugins import BasePlugin
 from slixmpp.xmlstream.handler import Callback
 from slixmpp.xmlstream.matcher import StanzaPath
@@ -18,6 +26,12 @@ from slixmpp.plugins.xep_0191 import stanza, Block, Unblock, BlockList
 
 log = logging.getLogger(__name__)
 
+BlockedJIDs = Union[
+    JID,
+    Set[JID],
+    List[JID]
+]
+
 
 class XEP_0191(BasePlugin):
 
@@ -45,42 +59,39 @@ class XEP_0191(BasePlugin):
         self.xmpp.remove_handler('Blocked Contact')
         self.xmpp.remove_handler('Unblocked Contact')
 
-    def get_blocked(self, ifrom=None, timeout=None, callback=None,
-                          timeout_callback=None):
-        iq = self.xmpp.Iq()
-        iq['type'] = 'get'
-        iq['from'] = ifrom
+    def get_blocked(self, ifrom: Optional[JID] = None, **iqkwargs) -> Future:
+        """Get the list of blocked JIDs."""
+        iq = self.xmpp.make_iq_get(ifrom=ifrom)
         iq.enable('blocklist')
-        return iq.send(timeout=timeout, callback=callback,
-                       timeout_callback=timeout_callback)
+        return iq.send(**iqkwargs)
 
-    def block(self, jids, ifrom=None, timeout=None, callback=None,
-                          timeout_callback=None):
-        iq = self.xmpp.Iq()
-        iq['type'] = 'set'
-        iq['from'] = ifrom
+    def block(self, jids: BlockedJIDs,
+              ifrom: Optional[JID] = None, **iqkwargs) -> Future:
+        """Block a JID or a list of JIDs.
 
+        :param jids: JID(s) to block.
+        """
+        iq = self.xmpp.make_iq_set(ifrom=ifrom)
         if not isinstance(jids, (set, list)):
             jids = [jids]
 
         iq['block']['items'] = jids
-        return iq.send(timeout=timeout, callback=callback,
-                       timeout_callback=timeout_callback)
+        return iq.send(**iqkwargs)
 
-    def unblock(self, jids=None, ifrom=None, timeout=None, callback=None,
-                      timeout_callback=None):
-        iq = self.xmpp.Iq()
-        iq['type'] = 'set'
-        iq['from'] = ifrom
+    def unblock(self, jids: BlockedJIDs, ifrom: Optional[JID] = None, **iqkwargs) -> Future:
+        """Unblock a JID or a list of JIDs.
 
+        :param jids: JID(s) to unblock.
+        """
         if jids is None:
-            jids = []
+            raise ValueError("jids cannot be empty.")
+        iq = self.xmpp.make_iq_set(ifrom=ifrom)
+
         if not isinstance(jids, (set, list)):
             jids = [jids]
 
         iq['unblock']['items'] = jids
-        return iq.send(timeout=timeout, callback=callback,
-                       timeout_callback=timeout_callback)
+        return iq.send(**iqkwargs)
 
     def _handle_blocked(self, iq):
         self.xmpp.event('blocked', iq)
-- 
cgit v1.2.3


From d6ab3124cea389bc72d465cfe7a4306731eaaaa9 Mon Sep 17 00:00:00 2001
From: mathieui <mathieui@mathieui.net>
Date: Fri, 5 Feb 2021 19:10:52 +0100
Subject: XEP-0084: add integer wrappers for int stanza attributes

---
 slixmpp/plugins/xep_0084/stanza.py | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/slixmpp/plugins/xep_0084/stanza.py b/slixmpp/plugins/xep_0084/stanza.py
index 4b6370ce..57754265 100644
--- a/slixmpp/plugins/xep_0084/stanza.py
+++ b/slixmpp/plugins/xep_0084/stanza.py
@@ -65,6 +65,35 @@ class Info(ElementBase):
     plugin_multi_attrib = 'items'
     interfaces = {'bytes', 'height', 'id', 'type', 'url', 'width'}
 
+    def _get_int(self, name: str) -> int:
+        try:
+            return int(self._get_attr(name))
+        except ValueError:
+            return 0
+
+    def _set_int(self, name: str, value: int):
+        if value not in ('', None):
+            int(value)
+        self._set_attr(name, value)
+
+    def get_bytes(self) -> int:
+        return self._get_int('bytes')
+
+    def _set_bytes(self, value: int):
+        self._set_int('bytes', value)
+
+    def get_height(self) -> int:
+        self._get_int('height')
+
+    def set_height(self, value: int):
+        self._set_int('height', value)
+
+    def get_width(self) -> int:
+        self._get_int(self, 'width')
+
+    def set_width(self, value: int):
+        self._set_int('with', value)
+
 
 class Pointer(ElementBase):
     name = 'pointer'
-- 
cgit v1.2.3


From 6c3f26161e78285696dba0002907529ad73fba72 Mon Sep 17 00:00:00 2001
From: mathieui <mathieui@mathieui.net>
Date: Fri, 5 Feb 2021 19:11:19 +0100
Subject: itests: allow pconfig registration with plugins

---
 slixmpp/test/integration.py | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/slixmpp/test/integration.py b/slixmpp/test/integration.py
index 758ad239..7da3f86a 100644
--- a/slixmpp/test/integration.py
+++ b/slixmpp/test/integration.py
@@ -16,7 +16,9 @@ except ImportError:
     # not usable.
     from unittest import TestCase as IsolatedAsyncioTestCase
 from typing import (
+    Dict,
     List,
+    Optional,
 )
 
 from slixmpp import JID
@@ -39,11 +41,14 @@ class SlixIntegration(IsolatedAsyncioTestCase):
         """get a str from an env var"""
         return os.getenv(name)
 
-    def register_plugins(self, plugins: List[str]):
+    def register_plugins(self, plugins: List[str], configs: Optional[List[Dict]] = None):
         """Register plugins on all known clients"""
-        for plugin in plugins:
+        for index, plugin in enumerate(plugins):
             for client in self.clients:
-                client.register_plugin(plugin)
+                if configs is not None:
+                    client.register_plugin(plugin, pconfig=configs[index])
+                else:
+                    client.register_plugin(plugin)
 
     def add_client(self, jid: JID, password: str):
         """Register a new client"""
-- 
cgit v1.2.3


From 89601289fea2c6f2b47002926eb2609bd72d2a17 Mon Sep 17 00:00:00 2001
From: mathieui <mathieui@mathieui.net>
Date: Fri, 5 Feb 2021 19:01:23 +0100
Subject: itests: add 0012, 0054, 0084, 0092, 0153, 0191 tests

---
 itests/test_blocking.py      | 32 +++++++++++++++++++++++
 itests/test_last_activity.py | 33 ++++++++++++++++++++++++
 itests/test_user_avatar.py   | 61 ++++++++++++++++++++++++++++++++++++++++++++
 itests/test_vcard.py         | 49 +++++++++++++++++++++++++++++++++++
 itests/test_vcard_avatar.py  | 49 +++++++++++++++++++++++++++++++++++
 itests/test_version.py       | 37 +++++++++++++++++++++++++++
 6 files changed, 261 insertions(+)
 create mode 100644 itests/test_blocking.py
 create mode 100644 itests/test_last_activity.py
 create mode 100644 itests/test_user_avatar.py
 create mode 100644 itests/test_vcard.py
 create mode 100644 itests/test_vcard_avatar.py
 create mode 100644 itests/test_version.py

diff --git a/itests/test_blocking.py b/itests/test_blocking.py
new file mode 100644
index 00000000..7954c1dc
--- /dev/null
+++ b/itests/test_blocking.py
@@ -0,0 +1,32 @@
+import unittest
+from slixmpp import JID
+from slixmpp.test.integration import SlixIntegration
+
+
+class TestBlocking(SlixIntegration):
+    async def asyncSetUp(self):
+        await super().asyncSetUp()
+        self.add_client(
+            self.envjid('CI_ACCOUNT1'),
+            self.envstr('CI_ACCOUNT1_PASSWORD'),
+        )
+        self.register_plugins(['xep_0191'])
+        await self.connect_clients()
+
+    async def test_blocking(self):
+        """Check we can block, unblock, and list blocked"""
+        await self.clients[0]['xep_0191'].block(
+            [JID('toto@example.com'), JID('titi@example.com')]
+        )
+        blocked = {JID('toto@example.com'), JID('titi@example.com')}
+        iq = await self.clients[0]['xep_0191'].get_blocked()
+        self.assertEqual(iq['blocklist']['items'], blocked)
+
+        info = await self.clients[0]['xep_0191'].unblock(
+            blocked,
+        )
+        iq = await self.clients[0]['xep_0191'].get_blocked()
+        self.assertEqual(len(iq['blocklist']['items']), 0)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestBlocking)
diff --git a/itests/test_last_activity.py b/itests/test_last_activity.py
new file mode 100644
index 00000000..3d36b4b8
--- /dev/null
+++ b/itests/test_last_activity.py
@@ -0,0 +1,33 @@
+import unittest
+from slixmpp.test.integration import SlixIntegration
+
+
+class TestLastActivity(SlixIntegration):
+    async def asyncSetUp(self):
+        await super().asyncSetUp()
+        self.add_client(
+            self.envjid('CI_ACCOUNT1'),
+            self.envstr('CI_ACCOUNT1_PASSWORD'),
+        )
+        self.add_client(
+            self.envjid('CI_ACCOUNT2'),
+            self.envstr('CI_ACCOUNT2_PASSWORD'),
+        )
+        self.register_plugins(['xep_0012'])
+        await self.connect_clients()
+
+    async def test_activity(self):
+        """Check we can set and get last activity"""
+        self.clients[0]['xep_0012'].set_last_activity(
+            status='coucou',
+            seconds=4242,
+        )
+        act = await self.clients[1]['xep_0012'].get_last_activity(
+            self.clients[0].boundjid.full
+        )
+        self.assertEqual(act['last_activity']['status'], 'coucou')
+        self.assertGreater(act['last_activity']['seconds'], 4241)
+        self.assertGreater(4250, act['last_activity']['seconds'])
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestLastActivity)
diff --git a/itests/test_user_avatar.py b/itests/test_user_avatar.py
new file mode 100644
index 00000000..193bbe72
--- /dev/null
+++ b/itests/test_user_avatar.py
@@ -0,0 +1,61 @@
+import asyncio
+import unittest
+from slixmpp import JID
+from slixmpp.test.integration import SlixIntegration
+
+
+class TestUserAvatar(SlixIntegration):
+    async def asyncSetUp(self):
+        await super().asyncSetUp()
+        self.add_client(
+            self.envjid('CI_ACCOUNT1'),
+            self.envstr('CI_ACCOUNT1_PASSWORD'),
+        )
+        self.register_plugins(['xep_0084'])
+        self.data = b'coucou coucou'
+        await self.connect_clients()
+
+    async def _clear_avatar(self):
+        """Utility for purging remote state"""
+        await self.clients[0]['xep_0084'].stop()
+        await self.clients[0]['xep_0084'].publish_avatar(b'')
+
+    async def test_set_avatar(self):
+        """Check we can set and get a PEP avatar and metadata"""
+        await self._clear_avatar()
+
+        await self.clients[0]['xep_0084'].publish_avatar(
+            self.data
+        )
+        metadata = {
+            'id': self.clients[0]['xep_0084'].generate_id(self.data),
+            'bytes': 13,
+            'type': 'image/jpeg',
+        }
+        # Wait for metadata publish event
+        event = self.clients[0].wait_until('avatar_metadata_publish')
+        publish = self.clients[0]['xep_0084'].publish_avatar_metadata(
+            metadata,
+        )
+        res = await asyncio.gather(
+            event,
+            publish,
+        )
+        message = res[0]
+        recv_meta = message['pubsub_event']['items']['item']['avatar_metadata']
+        info = recv_meta['info']
+        self.assertEqual(info['bytes'], metadata['bytes'])
+        self.assertEqual(info['type'], metadata['type'])
+        self.assertEqual(info['id'], metadata['id'])
+
+        recv = await self.clients[0]['xep_0084'].retrieve_avatar(
+            JID(self.clients[0].boundjid.bare),
+            info['id']
+        )
+        avatar = recv['pubsub']['items']['item']['avatar_data']['value']
+        self.assertEqual(avatar, self.data)
+
+        await self._clear_avatar()
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestUserAvatar)
diff --git a/itests/test_vcard.py b/itests/test_vcard.py
new file mode 100644
index 00000000..800d5a5b
--- /dev/null
+++ b/itests/test_vcard.py
@@ -0,0 +1,49 @@
+import unittest
+from slixmpp.test.integration import SlixIntegration
+
+
+class TestVcardTemp(SlixIntegration):
+    async def asyncSetUp(self):
+        await super().asyncSetUp()
+        self.add_client(
+            self.envjid('CI_ACCOUNT1'),
+            self.envstr('CI_ACCOUNT1_PASSWORD'),
+        )
+        self.add_client(
+            self.envjid('CI_ACCOUNT2'),
+            self.envstr('CI_ACCOUNT2_PASSWORD'),
+        )
+        self.register_plugins(['xep_0054'])
+        await self.connect_clients()
+
+    async def _clear_vcard(self):
+        # cleanup
+        await self.clients[0]['xep_0054'].publish_vcard(
+            self.clients[0]['xep_0054'].make_vcard()
+        )
+
+    async def test_vcard(self):
+        """Check we can set and get a vcard"""
+        await self._clear_vcard()
+
+        # Check that vcard is empty
+        recv = await self.clients[1]['xep_0054'].get_vcard(
+            self.clients[0].boundjid.bare
+        )
+        self.assertEqual(recv['vcard_temp']['TITLE'], None)
+
+        vcard = self.clients[0]['xep_0054'].make_vcard()
+        vcard['TITLE'] = 'Coucou coucou'
+        await self.clients[0]['xep_0054'].publish_vcard(
+            vcard,
+        )
+        #
+        recv = await self.clients[1]['xep_0054'].get_vcard(
+            self.clients[0].boundjid.bare
+        )
+        self.assertEqual(recv['vcard_temp']['TITLE'], 'Coucou coucou')
+
+        await self._clear_vcard()
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestVcardTemp)
diff --git a/itests/test_vcard_avatar.py b/itests/test_vcard_avatar.py
new file mode 100644
index 00000000..284c08e1
--- /dev/null
+++ b/itests/test_vcard_avatar.py
@@ -0,0 +1,49 @@
+import asyncio
+import unittest
+from slixmpp import JID
+from slixmpp.test.integration import SlixIntegration
+from hashlib import sha1
+
+
+class TestVcardAvatar(SlixIntegration):
+    async def asyncSetUp(self):
+        await super().asyncSetUp()
+        self.add_client(
+            self.envjid('CI_ACCOUNT1'),
+            self.envstr('CI_ACCOUNT1_PASSWORD'),
+        )
+        self.register_plugins(['xep_0153'])
+        self.data = b'coucou coucou'
+        self.hashed_data = sha1(self.data).hexdigest()
+        await self.connect_clients()
+
+    async def _clear_avatar(self):
+        """Utility for purging remote state"""
+        await self.clients[0]['xep_0153'].set_avatar(avatar=b'')
+
+    async def test_set_avatar(self):
+        """Check we can set and get a PEP avatar and metadata"""
+        await self._clear_avatar()
+
+        event = self.clients[0].wait_until('vcard_avatar_update')
+        update = self.clients[0]['xep_0153'].set_avatar(
+            avatar=self.data
+        )
+        result = await asyncio.gather(
+            event,
+            update,
+        )
+        presence = result[0]
+        hash = presence['vcard_temp_update']['photo']
+        self.assertEqual(hash, self.hashed_data)
+
+        iq = await self.clients[0]['xep_0054'].get_vcard(
+            JID(self.clients[0].boundjid.bare)
+        )
+        photo = iq['vcard_temp']['PHOTO']['BINVAL']
+        self.assertEqual(photo, self.data)
+
+        await self._clear_avatar()
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestVcardAvatar)
diff --git a/itests/test_version.py b/itests/test_version.py
new file mode 100644
index 00000000..5b8e42fd
--- /dev/null
+++ b/itests/test_version.py
@@ -0,0 +1,37 @@
+import unittest
+from slixmpp.test.integration import SlixIntegration
+
+
+class TestVersion(SlixIntegration):
+    async def asyncSetUp(self):
+        await super().asyncSetUp()
+        self.add_client(
+            self.envjid('CI_ACCOUNT1'),
+            self.envstr('CI_ACCOUNT1_PASSWORD'),
+        )
+        self.add_client(
+            self.envjid('CI_ACCOUNT2'),
+            self.envstr('CI_ACCOUNT2_PASSWORD'),
+        )
+        self.register_plugins(
+            ['xep_0092'],
+            configs=[{
+                'software_name': 'Slix Test',
+                'version': '1.2.3.4',
+                'os': 'I use arch btw',
+            }]
+        )
+        await self.connect_clients()
+
+    async def test_version(self):
+        """Check we can set and query software version info"""
+        iq = await self.clients[1]['xep_0092'].get_version(
+            self.clients[0].boundjid.full
+        )
+        version = iq['software_version']
+        self.assertEqual(version['name'], 'Slix Test')
+        self.assertEqual(version['version'], '1.2.3.4')
+        self.assertEqual(version['os'], 'I use arch btw')
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestVersion)
-- 
cgit v1.2.3