From 485496d3e86fb9e43efcc8cd52c03297aba742c2 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Sun, 15 Dec 2019 02:31:49 +0100 Subject: [PATCH 1/9] Assorted fixes This PR includes: - Fix SPDX to LGPL-2.1-or-later - Switch to using unicode_literals for Python 2 - Add _to_unicode() function - Replace RECEIVER global with static variable - Replace getAddonInfo() call with _addon_id() using static variable - Remove unused CallHandler.data And also cosmetic/testing improvements: - Add docstrings to all methods and functions - Remove disabled import-error and missing-docstring pylint tests - A codecov.yml file for code coverage - Add coverage support to Travis config - Small fixes to setup.py and tox config - Allow PYTHON to be set for Makefile - Unit test fix - Use snake_case for internal functions (no impact) --- .codecov.yml | 13 ++++++ .gitignore | 12 +++++- .pylintrc | 4 +- .travis.yml | 6 ++- Makefile | 16 ++++--- README.md | 2 +- addon.xml | 2 +- lib/AddonSignals.py | 97 +++++++++++++++++++++++++++---------------- requirements.txt | 1 + setup.py | 2 +- test/test_encoding.py | 4 +- tox.ini | 4 +- 12 files changed, 105 insertions(+), 58 deletions(-) create mode 100644 .codecov.yml diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..102c544 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,13 @@ +codecov: + branch: master +coverage: + round: nearest + status: + project: + default: + target: 75% + threshold: 6% + patch: false +comment: false +ignore: +- test/ diff --git a/.gitignore b/.gitignore index 4ee6f4b..2689eee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,18 @@ # Byte-compiled / optimized / DLL files .coverage +.pytest_cache/ .tox/ __pycache__/ *.py[cod] -*.zip - +# Packaging build/ dist/ +*.zip + +# Logs +kodi-addon-checker.log +kodi-addon-checker-report.log + +# Temporary +experiment/ diff --git a/.pylintrc b/.pylintrc index 8ab2d7f..933ddd7 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,7 +1,5 @@ [MESSAGES CONTROL] disable= - import-error, # Since xbmc and xbmcaddon are missing - invalid-name, # A lot of names do not follow conventions + invalid-name, # Module name or methods do not follow snake_case convention line-too-long, # We use flake8 for testing line-length - missing-docstring, # No docstrings are used old-style-class, diff --git a/.travis.yml b/.travis.yml index be2aaf1..c8f08e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ sudo: false env: PYTHONPATH: lib:test + PYTHONIOENCODING: utf-8 install: - pip install -r requirements.txt @@ -21,4 +22,7 @@ script: - pylint lib/AddonSignals.py test/ #- kodi-addon-checker . --branch=krypton #- kodi-addon-checker . --branch=leia -- python -m unittest discover +- coverage run -m unittest discover + +after_success: +- codecov diff --git a/Makefile b/Makefile index 5aa5abc..e90c1e9 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,8 @@ export PYTHONPATH := $(CURDIR)/lib:$(CURDIR)/test -addon_xml := addon.xml +PYTHON := python -# Collect information to build as sensible package name -name = $(shell xmllint --xpath 'string(/addon/@id)' $(addon_xml)) -version = $(shell xmllint --xpath 'string(/addon/@version)' $(addon_xml)) +name = $(shell xmllint --xpath 'string(/addon/@id)' addon.xml) +version = $(shell xmllint --xpath 'string(/addon/@version)' addon.xml) git_branch = $(shell git rev-parse --abbrev-ref HEAD) git_hash = $(shell git rev-parse --short HEAD) @@ -29,11 +28,11 @@ sanity: tox pylint tox: @echo -e "$(white)=$(blue) Starting sanity tox test$(reset)" - tox -q + $(PYTHON) -m tox -q pylint: @echo -e "$(white)=$(blue) Starting sanity pylint test$(reset)" - pylint lib/AddonSignals.py test + $(PYTHON) -m pylint lib/AddonSignals.py test/ addon: clean @echo -e "$(white)=$(blue) Starting sanity addon tests$(reset)" @@ -41,7 +40,7 @@ addon: clean unit: clean @echo -e "$(white)=$(blue) Starting unit tests$(reset)" - python -m unittest discover + $(PYTHON) -m unittest discover zip: clean @echo -e "$(white)=$(blue) Building new package$(reset)" @@ -51,7 +50,6 @@ zip: clean clean: @echo -e "$(white)=$(blue) Cleaning up$(reset)" - find . -name '*.pyc' -type f -delete - find . -name '*.pyo' -type f -delete + find . -name '*.py[cod]' -type f -delete find . -name '__pycache__' -type d -delete rm -rf .pytest_cache/ .tox/ *.log diff --git a/README.md b/README.md index c68047b..62a6ea8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![GitHub release](https://img.shields.io/github/release/ruuk/script.module.addon.signals.svg)](https://github.com/ruuk/script.module.addon.signals/releases) [![Build Status](https://travis-ci.org/ruuk/script.module.addon.signals.svg?branch=master)](https://travis-ci.org/ruuk/script.module.addon.signals) -[![License: LGPL-2.1](https://img.shields.io/badge/license-lgpl__2__1-blue)](https://opensource.org/licenses/LGPL-2.1) +[![License: LGPL-2.1 or later](https://img.shields.io/badge/license-LGPLv2.1_or_later-blue)](https://opensource.org/licenses/LGPL-2.1) [![Contributors](https://img.shields.io/github/contributors/ruuk/script.module.addon.signals.svg)](https://github.com/ruuk/script.module.addon.signals/graphs/contributors) # script.module.addon.signals diff --git a/addon.xml b/addon.xml index 9ba9d11..189f5be 100644 --- a/addon.xml +++ b/addon.xml @@ -11,7 +11,7 @@ Inter-addon signalling Provides signal/slot mechanism for inter-addon communication all - LGPL-2.1-only + LGPL-2.1-or-later https://github.com/ruuk/script.module.addon.signals icon.png diff --git a/lib/AddonSignals.py b/lib/AddonSignals.py index e0eb11e..19779d2 100644 --- a/lib/AddonSignals.py +++ b/lib/AddonSignals.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- +# GNU Lesser General Public License v2.1 (see COPYING or https://www.gnu.org/licenses/gpl-2.1.txt) +''' The AddonSignals module provides signal/slot mechanism for inter-addon communication in Kodi ''' +from __future__ import absolute_import, division, unicode_literals import base64 import json import sys @@ -7,11 +10,11 @@ import xbmc import xbmcaddon -RECEIVER = None class WaitTimeoutError(Exception): pass + def _perf_clock(): """Provides high resolution timing in seconds""" if hasattr(time, 'perf_counter'): @@ -22,54 +25,70 @@ def _perf_clock(): return time.time() # Fallback -def _getReceiver(): - global RECEIVER # pylint: disable=global-statement - if not RECEIVER: - RECEIVER = SignalReceiver() - return RECEIVER +def _addon_id(): + ''' Return the Kodi add-on id and cache it as a static variable ''' + if not hasattr(_addon_id, 'cached'): + _addon_id.cached = xbmcaddon.Addon().getAddonInfo('id') + return getattr(_addon_id, 'cached') -def _decodeData(data): +def _decode_data(data): + ''' Decode base64-encoded JSON data and return Python data structure ''' encoded_data = json.loads(data) - if encoded_data: - json_data = base64.b64decode(encoded_data[0]) - # NOTE: With Python 3.5 and older json.loads() does not support bytes or bytearray - if isinstance(json_data, bytes): - json_data = json_data.decode('utf-8') - return json.loads(json_data) - - return None + if not encoded_data: + return None + json_data = base64.b64decode(encoded_data[0]) + # NOTE: With Python 3.5 and older json.loads() does not support bytes or bytearray + return json.loads(_to_unicode(json_data)) -def _encodeData(data): +def _encode_data(data): + ''' Encode Python data structure into base64-encoded JSON data ''' json_data = json.dumps(data) if not isinstance(json_data, bytes): json_data = json_data.encode('utf-8') encoded_data = base64.b64encode(json_data) - if sys.version_info[0] > 2: - encoded_data = encoded_data.decode('ascii') - return encoded_data + return encoded_data.decode('ascii') + + +def _get_receiver(): + ''' Return a SignalReceiver instance and cache it as a static variable ''' + if not hasattr(_get_receiver, 'cached'): + _get_receiver.cached = SignalReceiver() + return getattr(_get_receiver, 'cached') def _jsonrpc(**kwargs): ''' Perform JSONRPC calls ''' if 'id' not in kwargs: - kwargs.update(id=1) + kwargs.update(id=0) if 'jsonrpc' not in kwargs: kwargs.update(jsonrpc='2.0') return json.loads(xbmc.executeJSONRPC(json.dumps(kwargs))) +def _to_unicode(text, encoding='utf-8', errors='strict'): + ''' Force text to unicode ''' + if isinstance(text, bytes): + return text.decode(encoding, errors=errors) + return text + + class SignalReceiver(xbmc.Monitor): + ''' The AddonSignals receiver class ''' + def __init__(self): # pylint: disable=super-init-not-called + ''' The SignalReceiver constructor ''' self._slots = {} def registerSlot(self, signaler_id, signal, callback): + ''' Register a slot in the AddonSignals receiver ''' if signaler_id not in self._slots: self._slots[signaler_id] = {} self._slots[signaler_id][signal] = callback def unRegisterSlot(self, signaler_id, signal): + ''' Unregister a slot in the AddonSignals receiver ''' if signaler_id not in self._slots: return if signal not in self._slots[signaler_id]: @@ -77,7 +96,8 @@ def unRegisterSlot(self, signaler_id, signal): del self._slots[signaler_id][signal] def onNotification(self, sender, method, data): - if not sender[-7:] == '.SIGNAL': + ''' The Kodi Monitor event handler for notifications ''' + if not sender.endswith('.SIGNAL'): return sender = sender[:-7] if sender not in self._slots: @@ -85,39 +105,43 @@ def onNotification(self, sender, method, data): signal = method.split('.', 1)[-1] if signal not in self._slots[sender]: return - self._slots[sender][signal](_decodeData(data)) + self._slots[sender][signal](_decode_data(data)) + +class CallHandler: + ''' The AddonSignals event handler class ''' -class CallHandler(object): - #pylint: disable=too-many-arguments def __init__(self, signal, data, source_id, timeout=1000, use_timeout_exception=False): + ''' The CallHandler constructor ''' self.signal = signal - self.data = data self.timeout = timeout - self.sourceID = source_id + self.source_id = source_id self._return = None self.is_callback_received = False self.use_timeout_exception = use_timeout_exception - registerSlot(self.sourceID, '_return.{0}'.format(self.signal), self.callback) - sendSignal(signal, data, self.sourceID) + registerSlot(self.source_id, '_return.{0}'.format(self.signal), self.callback) + sendSignal(signal, data, self.source_id) def callback(self, data): + ''' Method to register function as callback ''' self._return = data self.is_callback_received = True def waitForReturn(self): +<<<<<<< HEAD monitor = xbmc.Monitor() end_time = _perf_clock() + (self.timeout / 1000) while not self.is_callback_received: if _perf_clock() > end_time: if self.use_timeout_exception: - unRegisterSlot(self.sourceID, self.signal) + unRegisterSlot(self.source_id, self.signal) raise WaitTimeoutError break elif monitor.abortRequested(): raise OSError xbmc.sleep(10) - unRegisterSlot(self.sourceID, self.signal) + unRegisterSlot(self.source_id, self.signal) + return self._return @@ -128,28 +152,29 @@ def registerSlot(signaler_id, signal, callback): :param signal: name of the function to call (can be the same used in returnCall/makeCall/...) :param callback: the function to call """ - receiver = _getReceiver() + receiver = _get_receiver() receiver.registerSlot(signaler_id, signal, callback) def unRegisterSlot(signaler_id, signal): - receiver = _getReceiver() + ''' API method to unregister a slot ''' + receiver = _get_receiver() receiver.unRegisterSlot(signaler_id, signal) def sendSignal(signal, data=None, source_id=None, sourceID=None): + ''' API method to send a signal ''' if sourceID: xbmc.log('++++==== script.module.addon.signals: sourceID keyword is DEPRECATED - use source_id ====++++', xbmc.LOGNOTICE) - source_id = source_id or sourceID or xbmcaddon.Addon().getAddonInfo('id') - _jsonrpc(method='JSONRPC.NotifyAll', params=dict( - sender='%s.SIGNAL' % source_id, + sender='%s.SIGNAL' % source_id or sourceID or _addon_id(), message=signal, - data=[_encodeData(data)], + data=[_encode_data(data)], )) def registerCall(signaler_id, signal, callback): + ''' API method to register a callback slot ''' registerSlot(signaler_id, signal, callback) diff --git a/requirements.txt b/requirements.txt index 6e06c9f..4826343 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +codecov kodi-addon-checker pylint setuptools >= 41 diff --git a/setup.py b/setup.py index 9012fdf..6708b40 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, division, unicode_literals -from setuptools import setup, find_packages +from setuptools import setup import os from xml.dom.minidom import parse diff --git a/test/test_encoding.py b/test/test_encoding.py index 274e6bf..2244f9b 100644 --- a/test/test_encoding.py +++ b/test/test_encoding.py @@ -19,8 +19,8 @@ def test_addon_signals(self): base64_encoded_data = 'IkZcdTAwZjJcdTAwZjZiXHUwMGUwciI=' base64_encoded_json = '["%s"]' % base64_encoded_data - encoded_data = AddonSignals._encodeData(data) # pylint: disable=protected-access + encoded_data = AddonSignals._encode_data(data) # pylint: disable=protected-access self.assertEqual(encoded_data, base64_encoded_data) - decoded_data = AddonSignals._decodeData(base64_encoded_json) # pylint: disable=protected-access + decoded_data = AddonSignals._decode_data(base64_encoded_json) # pylint: disable=protected-access self.assertEqual(decoded_data, data) diff --git a/tox.ini b/tox.ini index 4c509e4..ae02da9 100644 --- a/tox.ini +++ b/tox.ini @@ -14,10 +14,10 @@ deps = [flake8] builtins = func max-line-length = 160 -ignore = E129,FI10,FI11,FI13,FI14,FI50,FI51,FI53,FI54,W503 +ignore = FI13,FI50,FI51,FI53,FI54 require-code = True min-version = 2.7 -exclude = .tox,experiment +exclude = .git,.tox,experiment [pytest] filterwarnings = default From 41c396f7b865aed77eab5c01fa3a7c9da4c57fbd Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Mon, 16 Dec 2019 03:09:52 +0100 Subject: [PATCH 2/9] Avoid loading libraries at import time This reduces the overhead of using AddonSignals at import time (as is most impacting on slower devices like Raspberry Pi). On Kodi all imports are performed at startup or at every invocation, even when not used. --- .pylintrc | 6 ++- lib/AddonSignals.py | 45 ++++++++++--------- test/test_encoding.py | 2 +- test/test_signals.py | 100 ++++++++++++++++++++++++++++++++++++++++++ test/xbmc.py | 49 +++++++++++++-------- test/xbmcaddon.py | 4 +- 6 files changed, 161 insertions(+), 45 deletions(-) create mode 100644 test/test_signals.py diff --git a/.pylintrc b/.pylintrc index 933ddd7..7acd383 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,5 +1,9 @@ [MESSAGES CONTROL] disable= + duplicate-code, + import-outside-toplevel, # Done on purpose to reduce overhead at startup invalid-name, # Module name or methods do not follow snake_case convention - line-too-long, # We use flake8 for testing line-length + line-too-long, # Flake8 is used for testing line-length old-style-class, + too-few-public-methods, + useless-object-inheritance, diff --git a/lib/AddonSignals.py b/lib/AddonSignals.py index 19779d2..73124a4 100644 --- a/lib/AddonSignals.py +++ b/lib/AddonSignals.py @@ -3,12 +3,7 @@ ''' The AddonSignals module provides signal/slot mechanism for inter-addon communication in Kodi ''' from __future__ import absolute_import, division, unicode_literals -import base64 -import json -import sys -import time -import xbmc -import xbmcaddon +from xbmc import executeJSONRPC, log, LOGNOTICE, Monitor, sleep class WaitTimeoutError(Exception): @@ -28,43 +23,49 @@ def _perf_clock(): def _addon_id(): ''' Return the Kodi add-on id and cache it as a static variable ''' if not hasattr(_addon_id, 'cached'): - _addon_id.cached = xbmcaddon.Addon().getAddonInfo('id') + from xbmcaddon import Addon + _addon_id.cached = Addon().getAddonInfo('id') return getattr(_addon_id, 'cached') def _decode_data(data): ''' Decode base64-encoded JSON data and return Python data structure ''' + import json encoded_data = json.loads(data) if not encoded_data: return None - json_data = base64.b64decode(encoded_data[0]) + from base64 import b64decode + json_data = b64decode(encoded_data[0]) # NOTE: With Python 3.5 and older json.loads() does not support bytes or bytearray return json.loads(_to_unicode(json_data)) def _encode_data(data): ''' Encode Python data structure into base64-encoded JSON data ''' + from base64 import b64encode + import json json_data = json.dumps(data) if not isinstance(json_data, bytes): json_data = json_data.encode('utf-8') - encoded_data = base64.b64encode(json_data) + encoded_data = b64encode(json_data) return encoded_data.decode('ascii') -def _get_receiver(): +def _receiver(): ''' Return a SignalReceiver instance and cache it as a static variable ''' - if not hasattr(_get_receiver, 'cached'): - _get_receiver.cached = SignalReceiver() - return getattr(_get_receiver, 'cached') + if not hasattr(_receiver, 'cached'): + _receiver.cached = SignalReceiver() + return getattr(_receiver, 'cached') def _jsonrpc(**kwargs): ''' Perform JSONRPC calls ''' + import json if 'id' not in kwargs: kwargs.update(id=0) if 'jsonrpc' not in kwargs: kwargs.update(jsonrpc='2.0') - return json.loads(xbmc.executeJSONRPC(json.dumps(kwargs))) + return json.loads(executeJSONRPC(json.dumps(kwargs))) def _to_unicode(text, encoding='utf-8', errors='strict'): @@ -74,12 +75,13 @@ def _to_unicode(text, encoding='utf-8', errors='strict'): return text -class SignalReceiver(xbmc.Monitor): +class SignalReceiver(Monitor, object): ''' The AddonSignals receiver class ''' def __init__(self): # pylint: disable=super-init-not-called ''' The SignalReceiver constructor ''' self._slots = {} + super(SignalReceiver, self).__init__() def registerSlot(self, signaler_id, signal, callback): ''' Register a slot in the AddonSignals receiver ''' @@ -108,7 +110,7 @@ def onNotification(self, sender, method, data): self._slots[sender][signal](_decode_data(data)) -class CallHandler: +class CallHandler(object): ''' The AddonSignals event handler class ''' def __init__(self, signal, data, source_id, timeout=1000, use_timeout_exception=False): @@ -128,7 +130,6 @@ def callback(self, data): self.is_callback_received = True def waitForReturn(self): -<<<<<<< HEAD monitor = xbmc.Monitor() end_time = _perf_clock() + (self.timeout / 1000) while not self.is_callback_received: @@ -152,22 +153,20 @@ def registerSlot(signaler_id, signal, callback): :param signal: name of the function to call (can be the same used in returnCall/makeCall/...) :param callback: the function to call """ - receiver = _get_receiver() - receiver.registerSlot(signaler_id, signal, callback) + _receiver().registerSlot(signaler_id, signal, callback) def unRegisterSlot(signaler_id, signal): ''' API method to unregister a slot ''' - receiver = _get_receiver() - receiver.unRegisterSlot(signaler_id, signal) + _receiver().unRegisterSlot(signaler_id, signal) def sendSignal(signal, data=None, source_id=None, sourceID=None): ''' API method to send a signal ''' if sourceID: - xbmc.log('++++==== script.module.addon.signals: sourceID keyword is DEPRECATED - use source_id ====++++', xbmc.LOGNOTICE) + log('++++==== script.module.addon.signals: sourceID keyword is DEPRECATED - use source_id ====++++', LOGNOTICE) _jsonrpc(method='JSONRPC.NotifyAll', params=dict( - sender='%s.SIGNAL' % source_id or sourceID or _addon_id(), + sender='%s.SIGNAL' % (source_id or sourceID or _addon_id()), message=signal, data=[_encode_data(data)], )) diff --git a/test/test_encoding.py b/test/test_encoding.py index 2244f9b..1814b0c 100644 --- a/test/test_encoding.py +++ b/test/test_encoding.py @@ -2,7 +2,7 @@ # Copyright: (c) 2019, Dag Wieers (@dagwieers) # GNU General Public License v2.0 (see COPYING or https://www.gnu.org/licenses/gpl-2.0.txt) -# pylint: disable=invalid-name,missing-docstring +# pylint: disable=missing-docstring from __future__ import absolute_import, division, print_function, unicode_literals import unittest diff --git a/test/test_signals.py b/test/test_signals.py new file mode 100644 index 0000000..547cc8d --- /dev/null +++ b/test/test_signals.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Dag Wieers (@dagwieers) +# GNU General Public License v2.0 (see COPYING or https://www.gnu.org/licenses/gpl-2.0.txt) + +# pylint: disable=missing-docstring + +from __future__ import absolute_import, division, print_function, unicode_literals +import unittest +import AddonSignals + +xbmc = __import__('xbmc') +xbmcaddon = __import__('xbmcaddon') +xbmcaddon.ADDON_ID = 'plugin.sender.id' + + +def callback(data): + xbmc.log('Received signal: %s' % data, 3) # Warning + callback.data = data + + +class TestSignals(unittest.TestCase): + + def test_signals(self): + data = dict(test='signals') + AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) + AddonSignals.sendSignal('test_signal', data=data) + AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + self.assertEqual(data, callback.data) + + def test_unicode(self): + data = dict(test='unicode', string='Föóbàr') + AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) + AddonSignals.sendSignal('test_signal', data=data) + AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + self.assertEqual(data, callback.data) + + def test_bogus_sender(self): + data = dict(test='bogus_sender') + AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) + AddonSignals.sendSignal('test_signal', data=data) + AddonSignals.sendSignal('test_signal', data=dict(bogus='data'), source_id='bogus.sender.id') + AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + self.assertEqual(data, callback.data) + + def test_send_multiple(self): + AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) + for idx in range(3): + AddonSignals.sendSignal('test_signal', data=dict(test='send_multiple', idx=idx)) + AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + self.assertEqual(dict(test='send_multiple', idx=2), callback.data) + + @staticmethod + def test_register_multiple(): + data = dict(test='register_multiple') + AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=print) + AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=xbmc.log) # Debug + AddonSignals.sendSignal('test_signal', data=data, source_id='plugin.sender.id') + AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + + def test_warning(self): + data = dict(test='warning') + AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) # Debug + AddonSignals.sendSignal('test_signal', data=data, sourceID='plugin.sender.id') + AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + self.assertEqual(data, callback.data) + + @staticmethod + def test_unregister_nonexisting(): + AddonSignals.unRegisterSlot('bogus.sender.id', signal='test_foo') + AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_foo') + AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_bar') + + def test_send_unregistered(self): + data = dict(test='send_unregistered') + AddonSignals.sendSignal('test_signal', data=data) + self.assertNotEqual(data, callback.data) + + def test_send_nonsignal(self): + data = dict(test='send_nonsignal') + AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) + AddonSignals.sendSignal('test_signal', data=data) + AddonSignals._jsonrpc(method='JSONRPC.NotifyAll', params=dict( # pylint: disable=protected-access + sender='plugin.sender.id', + message='test_nonsignal', + data=dict(bogus='data'), + )) + AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + self.assertEqual(data, callback.data) + + def test_send_none(self): + data = dict(test='send_none') + AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) + AddonSignals.sendSignal('test_signal', data=data) + AddonSignals._jsonrpc(method='JSONRPC.NotifyAll', params=dict( # pylint: disable=protected-access + sender='plugin.sender.id', + message='test_nonsignal', + data=None, + )) + AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + self.assertEqual(data, callback.data) diff --git a/test/xbmc.py b/test/xbmc.py index 1b719b5..f22e9d2 100644 --- a/test/xbmc.py +++ b/test/xbmc.py @@ -6,9 +6,9 @@ # pylint: disable=invalid-name,super-init-not-called,unused-argument from __future__ import absolute_import, division, print_function, unicode_literals - import json import time +import weakref LOGLEVELS = ['Debug', 'Info', 'Notice', 'Warning', 'Error', 'Severe', 'Fatal', 'None'] LOGDEBUG = 0 @@ -21,32 +21,45 @@ LOGNONE = 7 -class Monitor: +class Monitor(object): ''' A stub implementation of the xbmc Monitor class ''' + _instances = set() + def __init__(self, line='', heading=''): ''' A stub constructor for the xbmc Monitor class ''' + self._instances.add(weakref.ref(self)) - @staticmethod - def abortRequested(): - ''' A stub implementation for the xbmc Keyboard class abortRequested() method ''' - return False - - @staticmethod - def waitForAbort(): - ''' A stub implementation for the xbmc Keyboard class waitForAbort() method ''' - return - - -def executebuiltin(string, wait=False): # pylint: disable=unused-argument - ''' A stub implementation of the xbmc executebuiltin() function ''' - return + @classmethod + def getinstances(cls): + ''' Return the instances for this class ''' + dead = set() + for ref in cls._instances: + obj = ref() + if obj is not None: + yield obj + else: + dead.add(ref) + cls._instances -= dead def executeJSONRPC(jsonrpccommand): ''' A reimplementation of the xbmc executeJSONRPC() function ''' command = json.loads(jsonrpccommand) - log("executeJSONRPC does not implement method '{method}'".format(**command), 'Error') - return json.dumps(dict(error=dict(code=-1, message='Not implemented'), id=1, jsonrpc='2.0')) + + ret = dict(id=command.get('id'), jsonrpc='2.0', result='OK') + if command.get('method') == 'JSONRPC.NotifyAll': + # Send a notification to all instances of subclasses + for sub in Monitor.__subclasses__(): + for obj in sub.getinstances(): + obj.onNotification( + sender=command.get('params').get('sender'), + method=command.get('params').get('message'), + data=json.dumps(command.get('params').get('data')), + ) + else: + log("executeJSONRPC does not implement method '{method}'".format(**command), LOGERROR) + return json.dumps(dict(error=dict(code=-1, message='Not implemented'), id=command.get('id'), jsonrpc='2.0')) + return json.dumps(ret) def log(msg, level=0): diff --git a/test/xbmcaddon.py b/test/xbmcaddon.py index ab52700..644a2d1 100644 --- a/test/xbmcaddon.py +++ b/test/xbmcaddon.py @@ -13,9 +13,9 @@ class Addon: ''' A reimplementation of the xbmcaddon Addon class ''' - def __init__(self, id=ADDON_ID): # pylint: disable=redefined-builtin + def __init__(self, id=None): # pylint: disable=redefined-builtin ''' A stub constructor for the xbmcaddon Addon class ''' - self.id = id + self.id = id or ADDON_ID def getAddonInfo(self, key): ''' A working implementation for the xbmcaddon Addon class getAddonInfo() method ''' From f70513f0178036af39f8afe39310efc951013277 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Mon, 16 Dec 2019 14:24:43 +0100 Subject: [PATCH 3/9] Replace triple quotes by triple double quotes --- lib/AddonSignals.py | 35 ++++++++++++++++++----------------- test/xbmc.py | 14 +++++++------- test/xbmcaddon.py | 8 ++++---- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/lib/AddonSignals.py b/lib/AddonSignals.py index 73124a4..fd3ac73 100644 --- a/lib/AddonSignals.py +++ b/lib/AddonSignals.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # GNU Lesser General Public License v2.1 (see COPYING or https://www.gnu.org/licenses/gpl-2.1.txt) -''' The AddonSignals module provides signal/slot mechanism for inter-addon communication in Kodi ''' +"""The AddonSignals module provides signal/slot mechanism for inter-addon communication in Kodi""" from __future__ import absolute_import, division, unicode_literals from xbmc import executeJSONRPC, log, LOGNOTICE, Monitor, sleep @@ -21,7 +21,7 @@ def _perf_clock(): def _addon_id(): - ''' Return the Kodi add-on id and cache it as a static variable ''' + """Return the Kodi add-on id and cache it as a static variable""" if not hasattr(_addon_id, 'cached'): from xbmcaddon import Addon _addon_id.cached = Addon().getAddonInfo('id') @@ -29,7 +29,7 @@ def _addon_id(): def _decode_data(data): - ''' Decode base64-encoded JSON data and return Python data structure ''' + """Decode base64-encoded JSON data and return Python data structure""" import json encoded_data = json.loads(data) if not encoded_data: @@ -41,7 +41,7 @@ def _decode_data(data): def _encode_data(data): - ''' Encode Python data structure into base64-encoded JSON data ''' + """Encode Python data structure into base64-encoded JSON data""" from base64 import b64encode import json json_data = json.dumps(data) @@ -52,14 +52,14 @@ def _encode_data(data): def _receiver(): - ''' Return a SignalReceiver instance and cache it as a static variable ''' + """Return a SignalReceiver instance and cache it as a static variable""" if not hasattr(_receiver, 'cached'): _receiver.cached = SignalReceiver() return getattr(_receiver, 'cached') def _jsonrpc(**kwargs): - ''' Perform JSONRPC calls ''' + """Perform JSONRPC calls""" import json if 'id' not in kwargs: kwargs.update(id=0) @@ -69,28 +69,28 @@ def _jsonrpc(**kwargs): def _to_unicode(text, encoding='utf-8', errors='strict'): - ''' Force text to unicode ''' + """Force text to unicode""" if isinstance(text, bytes): return text.decode(encoding, errors=errors) return text class SignalReceiver(Monitor, object): - ''' The AddonSignals receiver class ''' + """The AddonSignals receiver class""" def __init__(self): # pylint: disable=super-init-not-called - ''' The SignalReceiver constructor ''' + """The SignalReceiver constructor""" self._slots = {} super(SignalReceiver, self).__init__() def registerSlot(self, signaler_id, signal, callback): - ''' Register a slot in the AddonSignals receiver ''' + """Register a slot in the AddonSignals receiver""" if signaler_id not in self._slots: self._slots[signaler_id] = {} self._slots[signaler_id][signal] = callback def unRegisterSlot(self, signaler_id, signal): - ''' Unregister a slot in the AddonSignals receiver ''' + """Unregister a slot in the AddonSignals receiver""" if signaler_id not in self._slots: return if signal not in self._slots[signaler_id]: @@ -98,7 +98,7 @@ def unRegisterSlot(self, signaler_id, signal): del self._slots[signaler_id][signal] def onNotification(self, sender, method, data): - ''' The Kodi Monitor event handler for notifications ''' + """The Kodi Monitor event handler for notifications""" if not sender.endswith('.SIGNAL'): return sender = sender[:-7] @@ -111,7 +111,7 @@ def onNotification(self, sender, method, data): class CallHandler(object): - ''' The AddonSignals event handler class ''' + """The AddonSignals event handler class""" def __init__(self, signal, data, source_id, timeout=1000, use_timeout_exception=False): ''' The CallHandler constructor ''' @@ -125,11 +125,12 @@ def __init__(self, signal, data, source_id, timeout=1000, use_timeout_exception= sendSignal(signal, data, self.source_id) def callback(self, data): - ''' Method to register function as callback ''' + """Method to register function as callback""" self._return = data self.is_callback_received = True def waitForReturn(self): + """Wait for callback to trigger""" monitor = xbmc.Monitor() end_time = _perf_clock() + (self.timeout / 1000) while not self.is_callback_received: @@ -157,12 +158,12 @@ def registerSlot(signaler_id, signal, callback): def unRegisterSlot(signaler_id, signal): - ''' API method to unregister a slot ''' + """API method to unregister a slot""" _receiver().unRegisterSlot(signaler_id, signal) def sendSignal(signal, data=None, source_id=None, sourceID=None): - ''' API method to send a signal ''' + """API method to send a signal""" if sourceID: log('++++==== script.module.addon.signals: sourceID keyword is DEPRECATED - use source_id ====++++', LOGNOTICE) _jsonrpc(method='JSONRPC.NotifyAll', params=dict( @@ -173,7 +174,7 @@ def sendSignal(signal, data=None, source_id=None, sourceID=None): def registerCall(signaler_id, signal, callback): - ''' API method to register a callback slot ''' + """API method to register a callback slot""" registerSlot(signaler_id, signal, callback) diff --git a/test/xbmc.py b/test/xbmc.py index f22e9d2..296c6a4 100644 --- a/test/xbmc.py +++ b/test/xbmc.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2019, Dag Wieers (@dagwieers) # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -''' This file implements the Kodi xbmc module, either using stubs or alternative functionality ''' +"""This file implements the Kodi xbmc module, either using stubs or alternative functionality""" # pylint: disable=invalid-name,super-init-not-called,unused-argument @@ -22,16 +22,16 @@ class Monitor(object): - ''' A stub implementation of the xbmc Monitor class ''' + """A stub implementation of the xbmc Monitor class""" _instances = set() def __init__(self, line='', heading=''): - ''' A stub constructor for the xbmc Monitor class ''' + """A stub constructor for the xbmc Monitor class""" self._instances.add(weakref.ref(self)) @classmethod def getinstances(cls): - ''' Return the instances for this class ''' + """Return the instances for this class""" dead = set() for ref in cls._instances: obj = ref() @@ -43,7 +43,7 @@ def getinstances(cls): def executeJSONRPC(jsonrpccommand): - ''' A reimplementation of the xbmc executeJSONRPC() function ''' + """A reimplementation of the xbmc executeJSONRPC() function""" command = json.loads(jsonrpccommand) ret = dict(id=command.get('id'), jsonrpc='2.0', result='OK') @@ -63,7 +63,7 @@ def executeJSONRPC(jsonrpccommand): def log(msg, level=0): - ''' A reimplementation of the xbmc log() function ''' + """A reimplementation of the xbmc log() function""" color1 = '\033[32;1m' color2 = '\033[32;0m' name = LOGLEVELS[level] @@ -79,5 +79,5 @@ def log(msg, level=0): def sleep(timemillis): - ''' A reimplementation of the xbmc sleep() function ''' + """A reimplementation of the xbmc sleep() function""" time.sleep(timemillis / 1000) diff --git a/test/xbmcaddon.py b/test/xbmcaddon.py index 644a2d1..88fcd50 100644 --- a/test/xbmcaddon.py +++ b/test/xbmcaddon.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2019, Dag Wieers (@dagwieers) # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -''' This file implements the Kodi xbmcaddon module, either using stubs or alternative functionality ''' +"""This file implements the Kodi xbmcaddon module, either using stubs or alternative functionality""" # pylint: disable=invalid-name,too-few-public-methods @@ -11,13 +11,13 @@ class Addon: - ''' A reimplementation of the xbmcaddon Addon class ''' + """A reimplementation of the xbmcaddon Addon class""" def __init__(self, id=None): # pylint: disable=redefined-builtin - ''' A stub constructor for the xbmcaddon Addon class ''' + """A stub constructor for the xbmcaddon Addon class""" self.id = id or ADDON_ID def getAddonInfo(self, key): - ''' A working implementation for the xbmcaddon Addon class getAddonInfo() method ''' + """A working implementation for the xbmcaddon Addon class getAddonInfo() method""" addon_info = dict(id=self.id, name='Foobar', version='1.2.3', type='kodi.video', profile='special://userdata', path='special://userdata') return addon_info.get(key) From 0fea64bf494b16af2444b40acac43d678a3f00c0 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Mon, 16 Dec 2019 15:31:39 +0100 Subject: [PATCH 4/9] Add integration tests for calls --- lib/AddonSignals.py | 1 - test/test_calls.py | 94 ++++++++++++++++++++++++++++++++++++++++++++ test/test_signals.py | 66 +++++++++++++++---------------- tox.ini | 2 +- 4 files changed, 128 insertions(+), 35 deletions(-) create mode 100644 test/test_calls.py diff --git a/lib/AddonSignals.py b/lib/AddonSignals.py index fd3ac73..f3bf39b 100644 --- a/lib/AddonSignals.py +++ b/lib/AddonSignals.py @@ -2,7 +2,6 @@ # GNU Lesser General Public License v2.1 (see COPYING or https://www.gnu.org/licenses/gpl-2.1.txt) """The AddonSignals module provides signal/slot mechanism for inter-addon communication in Kodi""" -from __future__ import absolute_import, division, unicode_literals from xbmc import executeJSONRPC, log, LOGNOTICE, Monitor, sleep diff --git a/test/test_calls.py b/test/test_calls.py new file mode 100644 index 0000000..33d6e6a --- /dev/null +++ b/test/test_calls.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# Copyright: (c) 2019, Dag Wieers (@dagwieers) +# GNU General Public License v2.0 (see COPYING or https://www.gnu.org/licenses/gpl-2.0.txt) + +# pylint: disable=missing-docstring + +from __future__ import absolute_import, division, print_function, unicode_literals +import unittest +from AddonSignals import _jsonrpc, makeCall, registerCall, returnCall + +xbmc = __import__('xbmc') +xbmcaddon = __import__('xbmcaddon') +xbmcaddon.ADDON_ID = 'plugin.sender.id' + + +def callback(data): + xbmc.log('Received signal: %s' % data, 3) # Warning + callback.data = data + + +class TestCalls(unittest.TestCase): + + def test_calls(self): + data = dict(test='signals') + registerCall('plugin.sender.id', signal='test_signal', callback=callback) + returnCall('test_signal', data=data) + self.assertEqual(data, callback.data) + makeCall(signal='test_signal', data=data, source_id='plugin.sender.id') + self.assertEqual(data, callback.data) + + def test_unicode(self): + data = dict(test='unicode', string='Föóbàr') + registerCall('plugin.sender.id', signal='test_signal', callback=callback) + returnCall('test_signal', data=data) + self.assertEqual(data, callback.data) + makeCall(signal='test_signal', data=data, source_id='plugin.sender.id') + self.assertEqual(data, callback.data) + + def test_bogus_sender(self): + data = dict(test='bogus_sender') + registerCall('plugin.sender.id', signal='test_signal', callback=callback) + returnCall('test_signal', data=data) + returnCall('test_signal', data=dict(bogus='data'), source_id='bogus.sender.id') + self.assertEqual(data, callback.data) + makeCall(signal='test_signal', data=data, source_id='plugin.sender.id') + self.assertEqual(data, callback.data) + + def test_send_multiple(self): + registerCall('plugin.sender.id', signal='test_signal', callback=callback) + for idx in range(3): + returnCall('test_signal', data=dict(test='send_multiple', idx=idx)) + self.assertEqual(dict(test='send_multiple', idx=2), callback.data) + + @staticmethod + def test_register_multiple(): + data = dict(test='register_multiple') + registerCall('plugin.sender.id', signal='test_signal', callback=print) + registerCall('plugin.sender.id', signal='test_signal', callback=xbmc.log) # Debug + returnCall('test_signal', data=data, source_id='plugin.sender.id') + makeCall(signal='test_signal', data=data, source_id='plugin.sender.id') + + @staticmethod + def test_unregister_nonexisting(): + data = dict(test='unregister_nonexisting') + makeCall(signal='test_foo', source_id='bogus.sender.id', data=data) + makeCall(signal='test_foo', source_id='plugin.sender.id', data=data) + makeCall(signal='test_bar', source_id='plugin.sender.id', data=data) + + def test_send_unregistered(self): + data = dict(test='send_unregistered') + returnCall('test_signal', data=data) + self.assertEqual(data, callback.data) + + def test_send_nonsignal(self): + data = dict(test='send_nonsignal') + registerCall('plugin.sender.id', signal='test_signal', callback=callback) + returnCall('test_signal', data=data) + _jsonrpc(method='JSONRPC.NotifyAll', params=dict( # pylint: disable=protected-access + sender='plugin.sender.id', + message='test_nonsignal', + data=dict(bogus='data'), + )) + self.assertEqual(data, callback.data) + + def test_send_none(self): + data = dict(test='send_none') + registerCall('plugin.sender.id', signal='test_signal', callback=callback) + returnCall('test_signal', data=data) + _jsonrpc(method='JSONRPC.NotifyAll', params=dict( # pylint: disable=protected-access + sender='plugin.sender.id', + message='test_nonsignal', + data=None, + )) + self.assertEqual(data, callback.data) diff --git a/test/test_signals.py b/test/test_signals.py index 547cc8d..bb309da 100644 --- a/test/test_signals.py +++ b/test/test_signals.py @@ -6,7 +6,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals import unittest -import AddonSignals +from AddonSignals import _jsonrpc, registerSlot, sendSignal, unRegisterSlot xbmc = __import__('xbmc') xbmcaddon = __import__('xbmcaddon') @@ -22,79 +22,79 @@ class TestSignals(unittest.TestCase): def test_signals(self): data = dict(test='signals') - AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) - AddonSignals.sendSignal('test_signal', data=data) - AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + registerSlot('plugin.sender.id', signal='test_signal', callback=callback) + sendSignal('test_signal', data=data) + unRegisterSlot('plugin.sender.id', signal='test_signal') self.assertEqual(data, callback.data) def test_unicode(self): data = dict(test='unicode', string='Föóbàr') - AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) - AddonSignals.sendSignal('test_signal', data=data) - AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + registerSlot('plugin.sender.id', signal='test_signal', callback=callback) + sendSignal('test_signal', data=data) + unRegisterSlot('plugin.sender.id', signal='test_signal') self.assertEqual(data, callback.data) def test_bogus_sender(self): data = dict(test='bogus_sender') - AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) - AddonSignals.sendSignal('test_signal', data=data) - AddonSignals.sendSignal('test_signal', data=dict(bogus='data'), source_id='bogus.sender.id') - AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + registerSlot('plugin.sender.id', signal='test_signal', callback=callback) + sendSignal('test_signal', data=data) + sendSignal('test_signal', data=dict(bogus='data'), source_id='bogus.sender.id') + unRegisterSlot('plugin.sender.id', signal='test_signal') self.assertEqual(data, callback.data) def test_send_multiple(self): - AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) + registerSlot('plugin.sender.id', signal='test_signal', callback=callback) for idx in range(3): - AddonSignals.sendSignal('test_signal', data=dict(test='send_multiple', idx=idx)) - AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + sendSignal('test_signal', data=dict(test='send_multiple', idx=idx)) + unRegisterSlot('plugin.sender.id', signal='test_signal') self.assertEqual(dict(test='send_multiple', idx=2), callback.data) @staticmethod def test_register_multiple(): data = dict(test='register_multiple') - AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=print) - AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=xbmc.log) # Debug - AddonSignals.sendSignal('test_signal', data=data, source_id='plugin.sender.id') - AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + registerSlot('plugin.sender.id', signal='test_signal', callback=print) + registerSlot('plugin.sender.id', signal='test_signal', callback=xbmc.log) # Debug + sendSignal('test_signal', data=data, source_id='plugin.sender.id') + unRegisterSlot('plugin.sender.id', signal='test_signal') def test_warning(self): data = dict(test='warning') - AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) # Debug - AddonSignals.sendSignal('test_signal', data=data, sourceID='plugin.sender.id') - AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + registerSlot('plugin.sender.id', signal='test_signal', callback=callback) # Debug + sendSignal('test_signal', data=data, sourceID='plugin.sender.id') + unRegisterSlot('plugin.sender.id', signal='test_signal') self.assertEqual(data, callback.data) @staticmethod def test_unregister_nonexisting(): - AddonSignals.unRegisterSlot('bogus.sender.id', signal='test_foo') - AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_foo') - AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_bar') + unRegisterSlot('bogus.sender.id', signal='test_foo') + unRegisterSlot('plugin.sender.id', signal='test_foo') + unRegisterSlot('plugin.sender.id', signal='test_bar') def test_send_unregistered(self): data = dict(test='send_unregistered') - AddonSignals.sendSignal('test_signal', data=data) + sendSignal('test_signal', data=data) self.assertNotEqual(data, callback.data) def test_send_nonsignal(self): data = dict(test='send_nonsignal') - AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) - AddonSignals.sendSignal('test_signal', data=data) - AddonSignals._jsonrpc(method='JSONRPC.NotifyAll', params=dict( # pylint: disable=protected-access + registerSlot('plugin.sender.id', signal='test_signal', callback=callback) + sendSignal('test_signal', data=data) + _jsonrpc(method='JSONRPC.NotifyAll', params=dict( # pylint: disable=protected-access sender='plugin.sender.id', message='test_nonsignal', data=dict(bogus='data'), )) - AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + unRegisterSlot('plugin.sender.id', signal='test_signal') self.assertEqual(data, callback.data) def test_send_none(self): data = dict(test='send_none') - AddonSignals.registerSlot('plugin.sender.id', signal='test_signal', callback=callback) - AddonSignals.sendSignal('test_signal', data=data) - AddonSignals._jsonrpc(method='JSONRPC.NotifyAll', params=dict( # pylint: disable=protected-access + registerSlot('plugin.sender.id', signal='test_signal', callback=callback) + sendSignal('test_signal', data=data) + _jsonrpc(method='JSONRPC.NotifyAll', params=dict( # pylint: disable=protected-access sender='plugin.sender.id', message='test_nonsignal', data=None, )) - AddonSignals.unRegisterSlot('plugin.sender.id', signal='test_signal') + unRegisterSlot('plugin.sender.id', signal='test_signal') self.assertEqual(data, callback.data) diff --git a/tox.ini b/tox.ini index ae02da9..2f3be75 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ deps = [flake8] builtins = func max-line-length = 160 -ignore = FI13,FI50,FI51,FI53,FI54 +ignore = FI10,FI11,FI13,FI14,FI50,FI51,FI53,FI54 require-code = True min-version = 2.7 exclude = .git,.tox,experiment From 655898636541e4a71583f82da146041e82cc6115 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Mon, 16 Dec 2019 16:15:41 +0100 Subject: [PATCH 5/9] Clean up pylint some more --- .pylintrc | 5 +---- lib/AddonSignals.py | 4 ++-- test/xbmc.py | 5 ++--- test/xbmcaddon.py | 5 ++--- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/.pylintrc b/.pylintrc index 7acd383..fbc55fd 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,9 +1,6 @@ [MESSAGES CONTROL] disable= - duplicate-code, + duplicate-code, # Integration tests tend to test similar things import-outside-toplevel, # Done on purpose to reduce overhead at startup invalid-name, # Module name or methods do not follow snake_case convention line-too-long, # Flake8 is used for testing line-length - old-style-class, - too-few-public-methods, - useless-object-inheritance, diff --git a/lib/AddonSignals.py b/lib/AddonSignals.py index f3bf39b..46be097 100644 --- a/lib/AddonSignals.py +++ b/lib/AddonSignals.py @@ -74,7 +74,7 @@ def _to_unicode(text, encoding='utf-8', errors='strict'): return text -class SignalReceiver(Monitor, object): +class SignalReceiver(Monitor, object): # pylint: disable=bad-option-value,useless-object-inheritance """The AddonSignals receiver class""" def __init__(self): # pylint: disable=super-init-not-called @@ -109,7 +109,7 @@ def onNotification(self, sender, method, data): self._slots[sender][signal](_decode_data(data)) -class CallHandler(object): +class CallHandler(object): # pylint: disable=bad-option-value,useless-object-inheritance """The AddonSignals event handler class""" def __init__(self, signal, data, source_id, timeout=1000, use_timeout_exception=False): diff --git a/test/xbmc.py b/test/xbmc.py index 296c6a4..c028522 100644 --- a/test/xbmc.py +++ b/test/xbmc.py @@ -3,8 +3,7 @@ # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """This file implements the Kodi xbmc module, either using stubs or alternative functionality""" -# pylint: disable=invalid-name,super-init-not-called,unused-argument - +# pylint: disable=bad-option-value,too-few-public-methods,useless-object-inheritance from __future__ import absolute_import, division, print_function, unicode_literals import json import time @@ -25,7 +24,7 @@ class Monitor(object): """A stub implementation of the xbmc Monitor class""" _instances = set() - def __init__(self, line='', heading=''): + def __init__(self, line='', heading=''): # pylint: disable=unused-argument """A stub constructor for the xbmc Monitor class""" self._instances.add(weakref.ref(self)) diff --git a/test/xbmcaddon.py b/test/xbmcaddon.py index 88fcd50..1635bc2 100644 --- a/test/xbmcaddon.py +++ b/test/xbmcaddon.py @@ -3,14 +3,13 @@ # GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) """This file implements the Kodi xbmcaddon module, either using stubs or alternative functionality""" -# pylint: disable=invalid-name,too-few-public-methods - +# pylint: disable=bad-option-value,too-few-public-methods,useless-object-inheritance from __future__ import absolute_import, division, print_function, unicode_literals ADDON_ID = 'plugin.video.foobar' -class Addon: +class Addon(object): """A reimplementation of the xbmcaddon Addon class""" def __init__(self, id=None): # pylint: disable=redefined-builtin From 9417e6e260a824b3768e66c1325dc81904245152 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Wed, 18 Dec 2019 20:21:03 +0100 Subject: [PATCH 6/9] Fix a bug in the return signal This PR includes: - Fix a bug in the return signal name - Improve callback integration tests - Ensure receiver and sender use different SignalReceiver instances --- lib/AddonSignals.py | 1 - test/test_calls.py | 109 +++++++++++++++++-------------------------- test/test_signals.py | 64 ++++++++++++------------- 3 files changed, 74 insertions(+), 100 deletions(-) diff --git a/lib/AddonSignals.py b/lib/AddonSignals.py index 46be097..5cddf1f 100644 --- a/lib/AddonSignals.py +++ b/lib/AddonSignals.py @@ -142,7 +142,6 @@ def waitForReturn(self): raise OSError xbmc.sleep(10) unRegisterSlot(self.source_id, self.signal) - return self._return diff --git a/test/test_calls.py b/test/test_calls.py index 33d6e6a..8e5f2b6 100644 --- a/test/test_calls.py +++ b/test/test_calls.py @@ -6,89 +6,64 @@ from __future__ import absolute_import, division, print_function, unicode_literals import unittest -from AddonSignals import _jsonrpc, makeCall, registerCall, returnCall +from AddonSignals import _receiver, makeCall, registerCall, returnCall xbmc = __import__('xbmc') xbmcaddon = __import__('xbmcaddon') xbmcaddon.ADDON_ID = 'plugin.sender.id' -def callback(data): - xbmc.log('Received signal: %s' % data, 3) # Warning - callback.data = data +def callback_calls(data): + # xbmc.log('Received signal: %s' % data, 3) # Warning + callback_calls.data = data + returnCall('test_calls', data=dict(value='return_calls', data=data)) + + +def callback_unicode(data): + # xbmc.log('Received signal: %s' % data, 3) # Warning + callback_unicode.data = data + returnCall('test_unicode', data=dict(value='return_unicode', data=data)) + + +def callback_bogus_sender(data): + # xbmc.log('Received signal: %s' % data, 3) # Warning + callback_bogus_sender.data = data + returnCall('test_bogus_sender', data=dict(value='return_bogus_sender', data=data)) + + +def callback_send_multiple(data): + # xbmc.log('Received signal: %s' % data, 3) # Warning + callback_send_multiple.data = data + returnCall('test_send_multiple', data=dict(value='return_send_multiple', data=data)) class TestCalls(unittest.TestCase): + registerCall('plugin.sender.id', signal='test_calls', callback=callback_calls) + registerCall('plugin.sender.id', signal='test_unicode', callback=callback_unicode) + registerCall('plugin.sender.id', signal='test_bogus_sender', callback=callback_bogus_sender) + registerCall('plugin.sender.id', signal='test_send_multiple', callback=callback_send_multiple) + old_receiver = _receiver() # Ensure the old SignalReceiver instance is not garbage-collected + del _receiver.cached # Ensure we have a different SignalReceiver instance for the sender def test_calls(self): - data = dict(test='signals') - registerCall('plugin.sender.id', signal='test_signal', callback=callback) - returnCall('test_signal', data=data) - self.assertEqual(data, callback.data) - makeCall(signal='test_signal', data=data, source_id='plugin.sender.id') - self.assertEqual(data, callback.data) + data = dict(test='calls') + ret = makeCall(signal='test_calls', data=data, source_id='plugin.sender.id') + self.assertEqual(data, callback_calls.data) + self.assertEqual(ret, dict(value='return_calls', data=data)) def test_unicode(self): data = dict(test='unicode', string='Föóbàr') - registerCall('plugin.sender.id', signal='test_signal', callback=callback) - returnCall('test_signal', data=data) - self.assertEqual(data, callback.data) - makeCall(signal='test_signal', data=data, source_id='plugin.sender.id') - self.assertEqual(data, callback.data) + ret = makeCall(signal='test_unicode', data=data, source_id='plugin.sender.id') + self.assertEqual(data, callback_unicode.data) + self.assertEqual(ret, dict(value='return_unicode', data=data)) def test_bogus_sender(self): - data = dict(test='bogus_sender') - registerCall('plugin.sender.id', signal='test_signal', callback=callback) - returnCall('test_signal', data=data) - returnCall('test_signal', data=dict(bogus='data'), source_id='bogus.sender.id') - self.assertEqual(data, callback.data) - makeCall(signal='test_signal', data=data, source_id='plugin.sender.id') - self.assertEqual(data, callback.data) + ret = makeCall(signal='test_bogus_sender', data=dict(bogus='data'), source_id='bogus.sender.id') +# self.assertNotEqual(data, callback_bogus_sender.data) + self.assertEqual(ret, None) def test_send_multiple(self): - registerCall('plugin.sender.id', signal='test_signal', callback=callback) for idx in range(3): - returnCall('test_signal', data=dict(test='send_multiple', idx=idx)) - self.assertEqual(dict(test='send_multiple', idx=2), callback.data) - - @staticmethod - def test_register_multiple(): - data = dict(test='register_multiple') - registerCall('plugin.sender.id', signal='test_signal', callback=print) - registerCall('plugin.sender.id', signal='test_signal', callback=xbmc.log) # Debug - returnCall('test_signal', data=data, source_id='plugin.sender.id') - makeCall(signal='test_signal', data=data, source_id='plugin.sender.id') - - @staticmethod - def test_unregister_nonexisting(): - data = dict(test='unregister_nonexisting') - makeCall(signal='test_foo', source_id='bogus.sender.id', data=data) - makeCall(signal='test_foo', source_id='plugin.sender.id', data=data) - makeCall(signal='test_bar', source_id='plugin.sender.id', data=data) - - def test_send_unregistered(self): - data = dict(test='send_unregistered') - returnCall('test_signal', data=data) - self.assertEqual(data, callback.data) - - def test_send_nonsignal(self): - data = dict(test='send_nonsignal') - registerCall('plugin.sender.id', signal='test_signal', callback=callback) - returnCall('test_signal', data=data) - _jsonrpc(method='JSONRPC.NotifyAll', params=dict( # pylint: disable=protected-access - sender='plugin.sender.id', - message='test_nonsignal', - data=dict(bogus='data'), - )) - self.assertEqual(data, callback.data) - - def test_send_none(self): - data = dict(test='send_none') - registerCall('plugin.sender.id', signal='test_signal', callback=callback) - returnCall('test_signal', data=data) - _jsonrpc(method='JSONRPC.NotifyAll', params=dict( # pylint: disable=protected-access - sender='plugin.sender.id', - message='test_nonsignal', - data=None, - )) - self.assertEqual(data, callback.data) + ret = makeCall(signal='test_send_multiple', data=dict(idx=idx), source_id='plugin.sender.id') + self.assertEqual(dict(idx=idx), callback_send_multiple.data) + self.assertEqual(ret, dict(value='return_send_multiple', data=dict(idx=idx))) diff --git a/test/test_signals.py b/test/test_signals.py index bb309da..3ea88a4 100644 --- a/test/test_signals.py +++ b/test/test_signals.py @@ -14,7 +14,7 @@ def callback(data): - xbmc.log('Received signal: %s' % data, 3) # Warning + # xbmc.log('Received signal: %s' % data, 3) # Warning callback.data = data @@ -22,46 +22,46 @@ class TestSignals(unittest.TestCase): def test_signals(self): data = dict(test='signals') - registerSlot('plugin.sender.id', signal='test_signal', callback=callback) - sendSignal('test_signal', data=data) - unRegisterSlot('plugin.sender.id', signal='test_signal') + registerSlot('plugin.sender.id', signal='test_signals', callback=callback) + sendSignal('test_signals', data=data) + unRegisterSlot('plugin.sender.id', signal='test_signals') self.assertEqual(data, callback.data) def test_unicode(self): data = dict(test='unicode', string='Föóbàr') - registerSlot('plugin.sender.id', signal='test_signal', callback=callback) - sendSignal('test_signal', data=data) - unRegisterSlot('plugin.sender.id', signal='test_signal') + registerSlot('plugin.sender.id', signal='test_unicode', callback=callback) + sendSignal('test_unicode', data=data) + unRegisterSlot('plugin.sender.id', signal='test_unicode') self.assertEqual(data, callback.data) def test_bogus_sender(self): data = dict(test='bogus_sender') - registerSlot('plugin.sender.id', signal='test_signal', callback=callback) - sendSignal('test_signal', data=data) - sendSignal('test_signal', data=dict(bogus='data'), source_id='bogus.sender.id') - unRegisterSlot('plugin.sender.id', signal='test_signal') + registerSlot('plugin.sender.id', signal='test_bogus_sender', callback=callback) + sendSignal('test_bogus_sender', data=data) + sendSignal('test_bogus_sender', data=dict(bogus='data'), source_id='bogus.sender.id') + unRegisterSlot('plugin.sender.id', signal='test_bogus_sender') self.assertEqual(data, callback.data) def test_send_multiple(self): - registerSlot('plugin.sender.id', signal='test_signal', callback=callback) + registerSlot('plugin.sender.id', signal='test_multiple', callback=callback) for idx in range(3): - sendSignal('test_signal', data=dict(test='send_multiple', idx=idx)) - unRegisterSlot('plugin.sender.id', signal='test_signal') + sendSignal('test_multiple', data=dict(test='send_multiple', idx=idx)) + unRegisterSlot('plugin.sender.id', signal='test_multiple') self.assertEqual(dict(test='send_multiple', idx=2), callback.data) @staticmethod def test_register_multiple(): data = dict(test='register_multiple') - registerSlot('plugin.sender.id', signal='test_signal', callback=print) - registerSlot('plugin.sender.id', signal='test_signal', callback=xbmc.log) # Debug - sendSignal('test_signal', data=data, source_id='plugin.sender.id') - unRegisterSlot('plugin.sender.id', signal='test_signal') + registerSlot('plugin.sender.id', signal='test_register_multiple', callback=print) + registerSlot('plugin.sender.id', signal='test_register_multiple', callback=xbmc.log) # Debug + sendSignal('test_register_multiple', data=data, source_id='plugin.sender.id') + unRegisterSlot('plugin.sender.id', signal='test_register_multiple') def test_warning(self): data = dict(test='warning') - registerSlot('plugin.sender.id', signal='test_signal', callback=callback) # Debug - sendSignal('test_signal', data=data, sourceID='plugin.sender.id') - unRegisterSlot('plugin.sender.id', signal='test_signal') + registerSlot('plugin.sender.id', signal='test_warning', callback=callback) # Debug + sendSignal('test_warning', data=data, sourceID='plugin.sender.id') + unRegisterSlot('plugin.sender.id', signal='test_warning') self.assertEqual(data, callback.data) @staticmethod @@ -72,29 +72,29 @@ def test_unregister_nonexisting(): def test_send_unregistered(self): data = dict(test='send_unregistered') - sendSignal('test_signal', data=data) + sendSignal('test_send_unregistered', data=data) self.assertNotEqual(data, callback.data) def test_send_nonsignal(self): data = dict(test='send_nonsignal') - registerSlot('plugin.sender.id', signal='test_signal', callback=callback) - sendSignal('test_signal', data=data) + registerSlot('plugin.sender.id', signal='test_nonsignal', callback=callback) + sendSignal('test_nonsignal', data=data) _jsonrpc(method='JSONRPC.NotifyAll', params=dict( # pylint: disable=protected-access sender='plugin.sender.id', message='test_nonsignal', data=dict(bogus='data'), )) - unRegisterSlot('plugin.sender.id', signal='test_signal') + unRegisterSlot('plugin.sender.id', signal='test_nonsignal') self.assertEqual(data, callback.data) def test_send_none(self): data = dict(test='send_none') - registerSlot('plugin.sender.id', signal='test_signal', callback=callback) - sendSignal('test_signal', data=data) + registerSlot('plugin.sender.id', signal='test_send_none', callback=callback) + sendSignal('test_send_none', data=data) _jsonrpc(method='JSONRPC.NotifyAll', params=dict( # pylint: disable=protected-access - sender='plugin.sender.id', - message='test_nonsignal', - data=None, + sender='plugin.sender.id.SIGNAL', + message='test_send_none', + data=[], )) - unRegisterSlot('plugin.sender.id', signal='test_signal') - self.assertEqual(data, callback.data) + unRegisterSlot('plugin.sender.id', signal='test_send_none') + self.assertEqual(None, callback.data) From 35493e421dbc902c448d0893ae22d8817b4bd011 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Sun, 3 May 2020 20:10:32 +0200 Subject: [PATCH 7/9] Move from Travis CI to GitHub CI This commit includes: - Add .gitattributes to get a proper ZIP pakage - Remove Travis CI - Add a GitHub CI workflow for unit testing, codecov and SonarCloud - Add a GitHub CI workflow for kodi-addon-checker - Move add-on icon to resources/ --- .gitattributes | 10 +++++++ .github/workflows/addon-check.yml | 39 ++++++++++++++++++++++++ .github/workflows/ci.yml | 48 ++++++++++++++++++++++++++++++ .travis.yml | 28 ----------------- Makefile | 35 +++++++++++----------- addon.xml | 2 +- requirements.txt | 4 +-- icon.png => resources/icon.png | Bin {test => tests}/__init__.py | 0 {test => tests}/test_calls.py | 0 {test => tests}/test_encoding.py | 0 {test => tests}/test_signals.py | 0 {test => tests}/xbmc.py | 0 {test => tests}/xbmcaddon.py | 0 14 files changed, 117 insertions(+), 49 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/workflows/addon-check.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml rename icon.png => resources/icon.png (100%) rename {test => tests}/__init__.py (100%) rename {test => tests}/test_calls.py (100%) rename {test => tests}/test_encoding.py (100%) rename {test => tests}/test_signals.py (100%) rename {test => tests}/xbmc.py (100%) rename {test => tests}/xbmcaddon.py (100%) diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3db4094 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +.github/ export-ignore +tests/ export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.pylintrc export-ignore +Makefile export-ignore +changelog.txt export-ignore +requirements.txt export-ignore +setup.py export-ignore +tox.ini export-ignore diff --git a/.github/workflows/addon-check.yml b/.github/workflows/addon-check.yml new file mode 100644 index 0000000..928fe2f --- /dev/null +++ b/.github/workflows/addon-check.yml @@ -0,0 +1,39 @@ +name: Kodi +on: +- pull_request +- push +jobs: + tests: + name: Addon checker + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + kodi-branch: [krypton, leia, matrix] + steps: + - uses: actions/checkout@v2 + with: + path: ${{ github.repository }} + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install dependencies + run: | + sudo apt-get install libxml2-utils xmlstarlet + python -m pip install --upgrade pip + # FIXME: Requires changes from xbmc/addon-check#217 + #pip install kodi-addon-checker + pip install git+git://github.com/xbmc/addon-check.git@master + - name: Remove unwanted files + run: awk '/export-ignore/ { print $1 }' .gitattributes | xargs rm -rf -- + working-directory: ${{ github.repository }} + - name: Rewrite addon.xml for Matrix + run: | + xmlstarlet ed -L -u '/addon/requires/import[@addon="xbmc.python"]/@version' -v "3.0.0" addon.xml + version=$(xmllint --xpath 'string(/addon/@version)' addon.xml) + xmlstarlet ed -L -u '/addon/@version' -v "${version}+matrix.99" addon.xml + working-directory: ${{ github.repository }} + if: matrix.kodi-branch == 'matrix' + - name: Run kodi-addon-checker + run: kodi-addon-checker --branch=${{ matrix.kodi-branch }} ${{ github.repository }}/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4b1a3b1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI +on: +- pull_request +- push +jobs: + tests: + name: Add-on testing + runs-on: ubuntu-latest + env: + PYTHONIOENCODING: utf-8 + PYTHONPATH: ${{ github.workspace }}/lib:${{ github.workspace }}/tests + strategy: + fail-fast: false + matrix: +# max-parallel: 2 + python-version: [ 2.7, 3.5, 3.6, 3.7, 3.8 ] + steps: + - name: Check out ${{ github.sha }} from repository ${{ github.repository }} + uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + sudo apt-get install gettext + sudo pip install coverage --install-option="--install-scripts=/usr/bin" + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run tox + run: python -m tox -q -e flake8,py + - name: Run pylint + run: python -m pylint lib/AddonSignals.py tests/ + - name: Run unit tests + run: coverage run -m unittest discover + - name: Upload code coverage to CodeCov + uses: codecov/codecov-action@v1 + continue-on-error: true + # FIXME: Requires changes from SonarSource/sonarcloud-github-action#9 + - name: Analyze with SonarCloud + uses: dagwieers/sonarcloud-github-action@more-options + with: + organization: add-ons + projectKey: add-ons_plugin.video.vrt.nu + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + continue-on-error: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c8f08e1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -language: python - -python: -- '2.7' -- '3.5' -- '3.6' -- '3.7' -- '3.8' - -sudo: false - -env: - PYTHONPATH: lib:test - PYTHONIOENCODING: utf-8 - -install: -- pip install -r requirements.txt - -script: -- tox -- tox -e flake8 -- pylint lib/AddonSignals.py test/ -#- kodi-addon-checker . --branch=krypton -#- kodi-addon-checker . --branch=leia -- coverage run -m unittest discover - -after_success: -- codecov diff --git a/Makefile b/Makefile index e90c1e9..060516f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -export PYTHONPATH := $(CURDIR)/lib:$(CURDIR)/test +export PYTHONPATH := $(CURDIR)/lib:$(CURDIR)/tests PYTHON := python name = $(shell xmllint --xpath 'string(/addon/@id)' addon.xml) @@ -7,7 +7,7 @@ git_branch = $(shell git rev-parse --abbrev-ref HEAD) git_hash = $(shell git rev-parse --short HEAD) zip_name = $(name)-$(version)-$(subst /,_,$(git_branch))-$(git_hash).zip -include_files = addon.xml LICENSE.txt README.md icon.png lib/ +include_files = addon.xml LICENSE.txt README.md icon.png lib/ resources/ include_paths = $(patsubst %,$(name)/%,$(include_files)) exclude_files = \*.new \*.orig \*.pyc \*.pyo zip_dir = $(name)/ @@ -16,33 +16,31 @@ blue = \e[1;34m white = \e[1;37m reset = \e[0;39m -.PHONY: test +all: check test build +zip: build +test: check test-unit test-service test-run -all: test zip +check: check-tox check-pylint -package: zip - -test: sanity unit - -sanity: tox pylint - -tox: +check-tox: @echo -e "$(white)=$(blue) Starting sanity tox test$(reset)" $(PYTHON) -m tox -q -pylint: +check-pylint: @echo -e "$(white)=$(blue) Starting sanity pylint test$(reset)" - $(PYTHON) -m pylint lib/AddonSignals.py test/ + $(PYTHON) -m pylint lib/AddonSignals.py tests/ -addon: clean +check-addon: clean @echo -e "$(white)=$(blue) Starting sanity addon tests$(reset)" kodi-addon-checker . --branch=gotham -unit: clean +unit: test-unit + +test-unit: clean @echo -e "$(white)=$(blue) Starting unit tests$(reset)" - $(PYTHON) -m unittest discover + $(PYTHON) -m unittest discover -v -zip: clean +build: clean @echo -e "$(white)=$(blue) Building new package$(reset)" @rm -f ../$(zip_name) cd ..; zip -r $(zip_name) $(include_paths) -x $(exclude_files) @@ -52,4 +50,5 @@ clean: @echo -e "$(white)=$(blue) Cleaning up$(reset)" find . -name '*.py[cod]' -type f -delete find . -name '__pycache__' -type d -delete - rm -rf .pytest_cache/ .tox/ *.log + rm -rf .pytest_cache/ .tox/ + rm -f *.log diff --git a/addon.xml b/addon.xml index 189f5be..4e8aad0 100644 --- a/addon.xml +++ b/addon.xml @@ -14,7 +14,7 @@ LGPL-2.1-or-later https://github.com/ruuk/script.module.addon.signals - icon.png + resources/icon.png diff --git a/requirements.txt b/requirements.txt index 4826343..f205787 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ codecov -kodi-addon-checker +kodi-addon-checker; python_version >= '3.5' pylint setuptools >= 41 -tox-travis +tox diff --git a/icon.png b/resources/icon.png similarity index 100% rename from icon.png rename to resources/icon.png diff --git a/test/__init__.py b/tests/__init__.py similarity index 100% rename from test/__init__.py rename to tests/__init__.py diff --git a/test/test_calls.py b/tests/test_calls.py similarity index 100% rename from test/test_calls.py rename to tests/test_calls.py diff --git a/test/test_encoding.py b/tests/test_encoding.py similarity index 100% rename from test/test_encoding.py rename to tests/test_encoding.py diff --git a/test/test_signals.py b/tests/test_signals.py similarity index 100% rename from test/test_signals.py rename to tests/test_signals.py diff --git a/test/xbmc.py b/tests/xbmc.py similarity index 100% rename from test/xbmc.py rename to tests/xbmc.py diff --git a/test/xbmcaddon.py b/tests/xbmcaddon.py similarity index 100% rename from test/xbmcaddon.py rename to tests/xbmcaddon.py From 23ba50ca7dc5bdf263b99dad7ae3baceb07dd5c5 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Sun, 27 Sep 2020 13:40:24 +0200 Subject: [PATCH 8/9] Fixes after rebase --- .pylintrc | 1 + lib/AddonSignals.py | 51 ++++++++++++++++++++++----------------------- tests/test_calls.py | 5 +++-- tests/xbmc.py | 19 ++++++++++++++++- 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/.pylintrc b/.pylintrc index fbc55fd..df77861 100644 --- a/.pylintrc +++ b/.pylintrc @@ -4,3 +4,4 @@ disable= import-outside-toplevel, # Done on purpose to reduce overhead at startup invalid-name, # Module name or methods do not follow snake_case convention line-too-long, # Flake8 is used for testing line-length +max-line-length=160 diff --git a/lib/AddonSignals.py b/lib/AddonSignals.py index 5cddf1f..bf94c3a 100644 --- a/lib/AddonSignals.py +++ b/lib/AddonSignals.py @@ -6,11 +6,12 @@ class WaitTimeoutError(Exception): - pass + """ Exception used when waiting times out""" def _perf_clock(): """Provides high resolution timing in seconds""" + import time if hasattr(time, 'perf_counter'): return time.perf_counter() # pylint: disable=no-member if hasattr(time, 'clock'): @@ -27,6 +28,20 @@ def _addon_id(): return getattr(_addon_id, 'cached') +def _to_unicode(text, encoding='utf-8', errors='strict'): + """Force text to unicode""" + if isinstance(text, bytes): + return text.decode(encoding, errors=errors) + return text + + +def _receiver(): + """Return a SignalReceiver instance and cache it as a static variable""" + if not hasattr(_receiver, 'cached'): + _receiver.cached = SignalReceiver() + return getattr(_receiver, 'cached') + + def _decode_data(data): """Decode base64-encoded JSON data and return Python data structure""" import json @@ -50,13 +65,6 @@ def _encode_data(data): return encoded_data.decode('ascii') -def _receiver(): - """Return a SignalReceiver instance and cache it as a static variable""" - if not hasattr(_receiver, 'cached'): - _receiver.cached = SignalReceiver() - return getattr(_receiver, 'cached') - - def _jsonrpc(**kwargs): """Perform JSONRPC calls""" import json @@ -67,20 +75,11 @@ def _jsonrpc(**kwargs): return json.loads(executeJSONRPC(json.dumps(kwargs))) -def _to_unicode(text, encoding='utf-8', errors='strict'): - """Force text to unicode""" - if isinstance(text, bytes): - return text.decode(encoding, errors=errors) - return text - - class SignalReceiver(Monitor, object): # pylint: disable=bad-option-value,useless-object-inheritance """The AddonSignals receiver class""" - def __init__(self): # pylint: disable=super-init-not-called - """The SignalReceiver constructor""" self._slots = {} - super(SignalReceiver, self).__init__() + super(SignalReceiver, self).__init__() # pylint: disable=bad-option-value,super-with-arguments def registerSlot(self, signaler_id, signal, callback): """Register a slot in the AddonSignals receiver""" @@ -111,16 +110,16 @@ def onNotification(self, sender, method, data): class CallHandler(object): # pylint: disable=bad-option-value,useless-object-inheritance """The AddonSignals event handler class""" - - def __init__(self, signal, data, source_id, timeout=1000, use_timeout_exception=False): - ''' The CallHandler constructor ''' + def __init__(self, signal, data, source_id, timeout=1000, use_timeout_exception=False): # pylint: disable=too-many-arguments + """The CallHandler constructor""" self.signal = signal + self.data = data self.timeout = timeout self.source_id = source_id self._return = None self.is_callback_received = False self.use_timeout_exception = use_timeout_exception - registerSlot(self.source_id, '_return.{0}'.format(self.signal), self.callback) + registerSlot(self.source_id, '_return_{0}'.format(self.signal), self.callback) sendSignal(signal, data, self.source_id) def callback(self, data): @@ -130,7 +129,7 @@ def callback(self, data): def waitForReturn(self): """Wait for callback to trigger""" - monitor = xbmc.Monitor() + monitor = Monitor() end_time = _perf_clock() + (self.timeout / 1000) while not self.is_callback_received: if _perf_clock() > end_time: @@ -138,9 +137,9 @@ def waitForReturn(self): unRegisterSlot(self.source_id, self.signal) raise WaitTimeoutError break - elif monitor.abortRequested(): + if monitor.abortRequested(): raise OSError - xbmc.sleep(10) + sleep(10) unRegisterSlot(self.source_id, self.signal) return self._return @@ -183,7 +182,7 @@ def returnCall(signal, data=None, source_id=None): :param data: data to send :param source_id: the name used for call/answer (e.g. add-on id) """ - sendSignal('_return.{0}'.format(signal), data, source_id) + sendSignal('_return_{0}'.format(signal), data, source_id) def makeCall(signal, data=None, source_id=None, timeout_ms=1000, use_timeout_exception=False): diff --git a/tests/test_calls.py b/tests/test_calls.py index 8e5f2b6..1dfe115 100644 --- a/tests/test_calls.py +++ b/tests/test_calls.py @@ -58,9 +58,10 @@ def test_unicode(self): self.assertEqual(ret, dict(value='return_unicode', data=data)) def test_bogus_sender(self): - ret = makeCall(signal='test_bogus_sender', data=dict(bogus='data'), source_id='bogus.sender.id') + with self.assertRaises(OSError) as ctxt: + makeCall(signal='test_bogus_sender', data=dict(bogus='data'), source_id='bogus.sender.id') + self.assertTrue(isinstance(ctxt.exception, OSError)) # self.assertNotEqual(data, callback_bogus_sender.data) - self.assertEqual(ret, None) def test_send_multiple(self): for idx in range(3): diff --git a/tests/xbmc.py b/tests/xbmc.py index c028522..c32a2e7 100644 --- a/tests/xbmc.py +++ b/tests/xbmc.py @@ -26,13 +26,30 @@ class Monitor(object): def __init__(self, line='', heading=''): # pylint: disable=unused-argument """A stub constructor for the xbmc Monitor class""" + self.iteration = 0 self._instances.add(weakref.ref(self)) + def abortRequested(self): + """A stub implementation for the xbmc Keyboard class abortRequested() method""" + self.iteration += 1 + print('Iteration: %s' % self.iteration) + return self.iteration % 5 == 0 + + def waitForAbort(self, timeout=None): # pylint: disable=no-self-use + """A stub implementation for the xbmc Monitor class waitForAbort() method""" + try: + time.sleep(timeout) + except KeyboardInterrupt: + return True + except Exception: # pylint: disable=broad-except + return True + return False + @classmethod def getinstances(cls): """Return the instances for this class""" dead = set() - for ref in cls._instances: + for ref in cls._instances.copy(): obj = ref() if obj is not None: yield obj From 6a9cb6fb1a4a9cfb7c5417b8f299d3772568e6f7 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Sun, 27 Sep 2020 14:48:18 +0200 Subject: [PATCH 9/9] Add release workflow The release workflow helps pushing releases to the Kodi repositories. Also add support starting from Gotham --- .github/workflows/addon-check.yml | 6 +-- .github/workflows/ci.yml | 8 ++-- .github/workflows/release.yml | 76 +++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/addon-check.yml b/.github/workflows/addon-check.yml index 928fe2f..ad37b8a 100644 --- a/.github/workflows/addon-check.yml +++ b/.github/workflows/addon-check.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - kodi-branch: [krypton, leia, matrix] + kodi-branch: [gotham, helix, isengard, jarvis, krypton, leia, matrix] steps: - uses: actions/checkout@v2 with: @@ -22,9 +22,7 @@ jobs: run: | sudo apt-get install libxml2-utils xmlstarlet python -m pip install --upgrade pip - # FIXME: Requires changes from xbmc/addon-check#217 - #pip install kodi-addon-checker - pip install git+git://github.com/xbmc/addon-check.git@master + pip install kodi-addon-checker - name: Remove unwanted files run: awk '/export-ignore/ { print $1 }' .gitattributes | xargs rm -rf -- working-directory: ${{ github.repository }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b1a3b1..e5a50b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,12 +36,12 @@ jobs: - name: Upload code coverage to CodeCov uses: codecov/codecov-action@v1 continue-on-error: true - # FIXME: Requires changes from SonarSource/sonarcloud-github-action#9 - name: Analyze with SonarCloud - uses: dagwieers/sonarcloud-github-action@more-options + uses: SonarSource/sonarcloud-github-action@v1.4 with: - organization: add-ons - projectKey: add-ons_plugin.video.vrt.nu + args: > + -Dsonar.organization=add-ons + -Dsonar.projectKey=add-ons_plugin.video.vrt.nu env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1fd796e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,76 @@ +name: Release +on: + push: + tags: + - 'v*' +jobs: + build: + name: Release script.module.addon.signals + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Build zip files + run: | + sudo apt-get install libxml2-utils + make multizip release=1 + - name: Get Leia filename + id: get-leia-filename + run: | + echo ::set-output name=leia-filename::$(cd ..;ls script.module.addon.signals*.zip | grep -v '+matrix.' | head -1) + - name: Get Matrix filename + id: get-matrix-filename + run: | + echo ::set-output name=matrix-filename::$(cd ..;ls script.module.addon.signals*+matrix.*.zip | head -1) + - name: Get body + id: get-body + run: | + description=$(sed '1,/^## Releases$/d;/## v[0-9\.]* ([0-9-]*)/d;/^$/,$d' README.md) + echo $description + description="${description//'%'/'%25'}" + description="${description//$'\n'/'%0A'}" + description="${description//$'\r'/'%0D'}" + echo ::set-output name=body::$description + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + body: ${{ steps.get-body.outputs.body }} + draft: false + prerelease: false + - name: Upload Leia zip + id: upload-leia-zip + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_name: ${{ steps.get-leia-filename.outputs.leia-filename }} + asset_path: ../${{ steps.get-leia-filename.outputs.leia-filename }} + asset_content_type: application/zip + - name: Upload Matrix zip + id: upload-matrix-zip + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_name: ${{ steps.get-matrix-filename.outputs.matrix-filename }} + asset_path: ../${{ steps.get-matrix-filename.outputs.matrix-filename }} + asset_content_type: application/zip + - name: Generate distribution zip and submit to official kodi repository + id: kodi-addon-submitter + uses: xbmc/action-kodi-addon-submitter@v1.2 + with: + kodi-repository: repo-scripts + kodi-version: gotham + addon-id: script.module.addon.signals + kodi-matrix: true + env: + GH_USERNAME: ${{secrets.GH_USERNAME}} + GH_TOKEN: ${{secrets.GH_TOKEN}} + EMAIL: ${{secrets.EMAIL}}