Skip to content

Commit

Permalink
rework for new kodi addon sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
willforde committed Apr 27, 2018
1 parent 5ca9750 commit 359c883
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 44 deletions.
11 changes: 7 additions & 4 deletions script.module.codequick/lib/codequick/script.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

# Standard Library Imports
import logging
import sys
import os

# Kodi imports
Expand Down Expand Up @@ -145,9 +144,6 @@ class Script(object):
#: Kodi notification info image
NOTIFY_INFO = 'info'

#: The Kodi handle that this add-on was started with.
handle = int(sys.argv[1])

setting = Settings()
"""
Dictionary like interface of add-on settings,
Expand All @@ -163,6 +159,9 @@ class Script(object):
def __init__(self):
self._title = self.params.get(u"_title_", u"")

#: The Kodi handle that this add-on was started with.
self.handle = dispatcher.handle

@classmethod
def register(cls, callback):
"""
Expand All @@ -180,6 +179,10 @@ def register_delayed(func, *args, **kwargs):
Sence the function is called after the listitems have been shown, it will not slow anything down.
Very useful for fetching extra metadata for later use without slowing down the listing of content.
.. note::
Functions will be called in reverse order to the order they are added (LIFO).
:param func: Function that will be called after endOfDirectory is called.
:param args: Positional arguments that will be passed to function.
:param kwargs: Keyword arguments that will be passed to function.
Expand Down
63 changes: 35 additions & 28 deletions script.module.codequick/lib/codequick/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,25 +204,10 @@ class Dispatcher(object):
def __init__(self):
self.registered_delayed = []
self.registered_routes = {}

# Extract arguments given by Kodi
_, _, route, raw_params, _ = urlparse.urlsplit(sys.argv[0] + sys.argv[2])
self.selector = route if len(route) > 1 else "root"

if raw_params:
self.params = params = parse_qs(raw_params)

# Unpickle pickled data
if "_pickle_" in params:
unpickled = pickle.loads(binascii.unhexlify(params.pop("_pickle_")))
params.update(unpickled)

# Construct a separate dictionary for callback specific parameters
self.callback_params = {key: value for key, value in params.items()
if not (key.startswith(u"_") and key.endswith(u"_"))}
else:
self.callback_params = {}
self.params = {}
self.callback_params = {}
self.selector = "root"
self.params = {}
self.handle = -1

def reset(self):
"""Reset session parameters, this is needed for unittests to work properly."""
Expand All @@ -242,6 +227,25 @@ def get_route(self, path=None):
"""
return self.registered_routes[path if path else self.selector]

def parse_args(self):
"""Extract arguments given by Kodi"""
_, _, route, raw_params, _ = urlparse.urlsplit(sys.argv[0] + sys.argv[2])
self.selector = route if len(route) > 1 else "root"
self.handle = int(sys.argv[1])

if raw_params:
params = parse_qs(raw_params)
self.params.update(params)

# Unpickle pickled data
if "_pickle_" in params:
unpickled = pickle.loads(binascii.unhexlify(self.params.pop("_pickle_")))
self.params.update(unpickled)

# Construct a separate dictionary for callback specific parameters
self.callback_params = {key: value for key, value in self.params.items()
if not (key.startswith(u"_") and key.endswith(u"_"))}

def register_callback(self, callback, parent):
"""
Register route callback function
Expand All @@ -255,13 +259,13 @@ def register_callback(self, callback, parent):
if path != "root":
path = "/{}/{}".format(callback.__module__.strip("_").replace(".", "/"), callback.__name__).lower()

# Register callback only if it's route path is unique
# Register callback
if path in self.registered_routes:
raise ValueError("encountered duplicate route: '{}'".format(path))
else:
self.registered_routes[path] = route = Route(callback, parent, path)
callback.route = route
return callback
logger.debug("encountered duplicate route: '%s'", path)

self.registered_routes[path] = route = Route(callback, parent, path)
callback.route = route
return callback

def register_delayed(self, func, args, kwargs):
"""Register a function that will be called later, after content has been listed."""
Expand All @@ -279,7 +283,8 @@ def run_callback(self):
The 'root' callback is the callback that will be executed
when no plugin path is given. i.e. when the add-on starts.
"""

self.reset()
self.parse_args()
logger.debug("Dispatching to route: '%s'", self.selector)
logger.debug("Callback parameters: '%s'", self.callback_params)

Expand Down Expand Up @@ -313,8 +318,10 @@ def run_delayed(self):
# Time before executing callbacks
start_time = time.time()

# Execute each callback one by one
for func, args, kwargs in self.registered_delayed:
# Execute in order of last in first out (LIFO).
while self.registered_delayed:
func, args, kwargs = self.registered_delayed.pop()

try:
func(*args, **kwargs)
except Exception as e:
Expand Down
20 changes: 8 additions & 12 deletions tests/test_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,36 +101,40 @@ def test_reset(self):
self.assertDictEqual(self.dispatcher.params, dict())

def test_parse_sysargs(self):
dispatcher = support.Dispatcher()
with mock_argv(["plugin://script.module.codequick/test/tester", 96, ""]):
dispatcher = support.Dispatcher()
dispatcher.parse_args()

self.assertEqual(dispatcher.selector, "/test/tester")

def test_parse_sysargs_with_args(self):
dispatcher = support.Dispatcher()
with mock_argv(["plugin://script.module.codequick/test/tester", 96,
"?testdata=true&worker=false&_title_=test"]):
dispatcher = support.Dispatcher()
dispatcher.parse_args()

self.assertEqual(dispatcher.selector, "/test/tester")
self.assertDictContainsSubset({"testdata": "true", "worker": "false", "_title_": "test"}, dispatcher.params)
self.assertDictContainsSubset({"testdata": "true", "worker": "false"}, dispatcher.callback_params)

@unittest.skipIf(PY3, "The pickled string is specific to python 2")
def test_parse_params_pickle_py2(self):
dispatcher = support.Dispatcher()
with mock_argv(["plugin://script.module.codequick/test/tester", 96,
"?_pickle_=80027d7100285506776f726b65727101895508746573746461746171028855075f7469746c655f710355"
"04746573747104752e"]):
dispatcher = support.Dispatcher()
dispatcher.parse_args()

self.assertDictContainsSubset({"testdata": True, "worker": False, "_title_": "test"}, dispatcher.params)
self.assertDictContainsSubset({"testdata": True, "worker": False}, dispatcher.callback_params)

@unittest.skipUnless(PY3, "The pickled string is specific to python 3")
def test_parse_params_pickle_py3(self):
dispatcher = support.Dispatcher()
with mock_argv(["plugin://script.module.codequick/test/tester", 96,
"?_pickle_=8004952c000000000000007d94288c08746573746461746194888c06776f726b657294898c075f74697"
"46c655f948c047465737494752e"]):
dispatcher = support.Dispatcher()
dispatcher.parse_args()

self.assertDictContainsSubset({"testdata": True, "worker": False, "_title_": "test"}, dispatcher.params)
self.assertDictContainsSubset({"testdata": True, "worker": False}, dispatcher.callback_params)
Expand Down Expand Up @@ -172,14 +176,6 @@ def listing():
self.assertIsInstance(callback.route, support.Route)
self.assertTrue(inspect.ismethod(callback.test))

def test_register_duplicate(self):
def root():
pass

self.dispatcher.register_callback(root, route.Route)
with self.assertRaises(ValueError):
self.dispatcher.register_callback(root, route.Route)

def test_register_class(self):
class Videos(route.Route):
def run(self):
Expand Down

0 comments on commit 359c883

Please sign in to comment.