From c9b3e269d4541f691668e8b196004602287952b8 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Thu, 7 Mar 2019 01:45:13 -0500 Subject: [PATCH 01/26] Add a Handler for Scripts + more Fix exact url matches. Basic Type Conversion for Dispatcher (breaking for plugins that assume always str). Fix numbers in argument names. More comprehensive tests. --- lib/routing.py | 109 +++++++++++++++++++++++++++++++++++++++++-------- lib/tests.py | 42 +++++++++++++------ 2 files changed, 123 insertions(+), 28 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index de9e93b..7e0f269 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -43,22 +43,26 @@ class RoutingError(Exception): pass -class Plugin(object): +class Addon(object): """ - :ivar handle: The plugin handle from kodi - :type handle: int - + The base class for routing.Plugin, Script, and any others that may be added :ivar args: The parsed query string. :type args: dict of byte strings + + :ivar base_url: the base_url of the addon, ex. plugin://plugin.video.something_plugin + :type base_url: str + + :ivar convert_args: Convert arguments to basic types + :type convert_args: bool """ - def __init__(self, base_url=None): + def __init__(self, base_url=None, convert_args=False): self._rules = {} # function to list of rules - self.handle = int(sys.argv[1]) if sys.argv[1].isdigit() else -1 self.args = {} self.base_url = base_url + self.convert_args = convert_args if self.base_url is None: - self.base_url = "plugin://" + xbmcaddon.Addon().getAddonInfo('id') + self.base_url = xbmcaddon.Addon().getAddonInfo('id') def route_for(self, path): """ @@ -69,6 +73,13 @@ def route_for(self, path): if path.startswith(self.base_url): path = path.split(self.base_url, 1)[1] + # first, search for exact matches + for view_fun, rules in iter(self._rules.items()): + for rule in rules: + if rule.exact_match(path): + return view_fun + + # then, search for regex matches for view_fun, rules in iter(self._rules.items()): for rule in rules: if rule.match(path) is not None: @@ -121,23 +132,58 @@ def _dispatch(self, path): for view_func, rules in iter(self._rules.items()): for rule in rules: kwargs = rule.match(path) - if kwargs is not None: - log("Dispatching to '%s', args: %s" % (view_func.__name__, kwargs)) - view_func(**kwargs) + if not kwargs: return + if self.convert_args: + for k, v in kwargs.items(): + new_val = try_convert(v) + if new_val: + kwargs[k] = new_val + log("Dispatching to '%s', args: %s" % (view_func.__name__, kwargs)) + view_func(**kwargs) + return raise RoutingError('No route to path "%s"' % path) -class UrlRule(object): +class Plugin(Addon): + """ + A routing handler bound to a kodi plugin + :ivar handle: The plugin handle from kodi + :type handle: int + """ + + def __init__(self, base_url=None, convert_args=False): + self.base_url = base_url + if self.base_url is None: + self.base_url = "plugin://" + xbmcaddon.Addon().getAddonInfo('id') + super().__init__(self.base_url, convert_args) + if len(sys.argv) < 2: + # we are probably not dealing with a plugin, or it was called incorrectly from an addon + raise TypeError('There was no handle provided. This needs to be called from a Kodi Plugin.') + self.handle = int(sys.argv[1]) if sys.argv[1].isdigit() else -1 + + +class Script(Addon): + """ + A routing handler bound to a kodi script + """ + + def __init__(self, base_url=None, convert_args=False): + super().__init__(base_url, convert_args) + +class UrlRule(object): def __init__(self, pattern): - kw_pattern = r'<(?:[^:]+:)?([A-z]+)>' + arg_regex = re.compile('<([A-z][A-z0-9]*)>') + self._has_args = bool(arg_regex.search(pattern)) + + kw_pattern = r'<(?:[^:]+:)?([A-z][A-z0-9]*)>' self._pattern = re.sub(kw_pattern, '{\\1}', pattern) self._keywords = re.findall(kw_pattern, pattern) - p = re.sub('<([A-z]+)>', '', pattern) - p = re.sub('', '(?P<\\1>[^/]+?)', p) - p = re.sub('', '(?P<\\1>.*)', p) + p = re.sub('<([A-z][A-z0-9]*)>', '', pattern) + p = re.sub('', '(?P<\\1>[^/]+?)', p) + p = re.sub('', '(?P<\\1>.*)', p) self._compiled_pattern = p self._regex = re.compile('^' + p + '$') @@ -150,6 +196,9 @@ def match(self, path): match = self._regex.search(path) return match.groupdict() if match else None + def exact_match(self, path): + return not self._has_args and self._pattern == path + def make_path(self, *args, **kwargs): """Construct a path from arguments.""" if args and kwargs: @@ -157,7 +206,7 @@ def make_path(self, *args, **kwargs): if args: # Replace the named groups %s and format try: - return re.sub(r'{[A-z]+}', r'%s', self._pattern) % args + return re.sub(r'{[A-z][A-z0-9]*}', r'%s', self._pattern) % args except TypeError: return None @@ -174,3 +223,31 @@ def make_path(self, *args, **kwargs): def __str__(self): return b"Rule(pattern=%s, keywords=%s)" % (self._pattern, self._keywords) + + +def try_convert(value): + """ + Try to convert to some common types + :param value: the string to convert + :type value: str + """ + # for some of these, they are simplistic and not the generally preferred way + # this is a special case, so I don't care + + # try to convert to int + if all(x.isdigit() for x in value): + return int(value) + + # try to convert to float. We've already check ints, so just try/except + try: + return float(value) + except: + pass + + # try to convert to bool + if value.lower() == 'true': + return True + if value.lower() == 'false': + return False + + return None diff --git a/lib/tests.py b/lib/tests.py index c838d7f..4e82e4c 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -17,7 +17,7 @@ import pytest import mock -from routing import Plugin, UrlRule, RoutingError +from routing import Plugin, UrlRule, RoutingError, Script @pytest.fixture() @@ -25,6 +25,11 @@ def plugin(): return Plugin('plugin://py.test') +@pytest.fixture() +def script(): + return Script('script://py.test') + + def test_match(): assert UrlRule("/p/").match("/p/bar") == {'foo': 'bar'} @@ -54,15 +59,15 @@ def test_url_for(plugin): def test_url_for_kwargs(plugin): - f = lambda a, b: None - plugin.route("/foo//")(f) - assert plugin.url_for(f, a=1, b=2) == plugin.base_url + "/foo/1/2" - + f = lambda a, b2: None + plugin.route("/foo//")(f) + assert plugin.url_for(f, a=1, b2=2) == plugin.base_url + "/foo/1/2" + def test_url_for_args(plugin): - f = lambda a, b: None - plugin.route("//")(f) - assert plugin.url_for(f, 1, 2) == plugin.base_url + "/1/2" + f = lambda a, b2, c, d: None + plugin.route("////")(f) + assert plugin.url_for(f, 1, 2.6, True, 'baz') == plugin.base_url + "/1/2.6/True/baz" def test_route_for(plugin): @@ -73,8 +78,14 @@ def test_route_for(plugin): def test_route_for_args(plugin): f = lambda: None - plugin.route("/foo//")(f) - assert plugin.route_for(plugin.base_url + "/foo/1/2") is f + g = lambda: (None, None) # just to make sure that they are easily different + plugin.route("/foo//")(f) + plugin.route("/foo/a/b")(g) + + # due to the unpredictable sorting of dict, just do it 100 times to see if it fails + for i in range(0, 100): + assert plugin.route_for(plugin.base_url + "/foo/1/2") is f + assert plugin.route_for(plugin.base_url + "/foo/a/b") is g def test_dispatch(plugin): @@ -99,5 +110,12 @@ def test_no_route(plugin): def test_arg_parsing(plugin): f = mock.create_autospec(lambda: None) plugin.route("/foo")(f) - plugin.run(['plugin://py.test/foo', '0', '?bar=baz']) - assert plugin.args['bar'][0] == 'baz' + plugin.run(['plugin://py.test/foo', '0', '?bar=baz&bar2=baz2']) + assert plugin.args['bar'][0] == 'baz' and plugin.args['bar2'][0] == 'baz2' + + +def test_arg_conversion(plugin): + def test_arg_conversion_inner(a, b2, c, d): + assert a == 'bar' and b2 == True and c == 16.4 and d == 9 + plugin.route("/foo////")(test_arg_conversion_inner) + plugin.run(['plugin://py.test/foo/bar/true/16.4/9', '0', '']) From 445f1463d2e4b795475c49f9c2556e9bf97efd53 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Thu, 7 Mar 2019 01:53:35 -0500 Subject: [PATCH 02/26] Stupid Whitespace Fix. Sort import better. Add a comment --- lib/tests.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/tests.py b/lib/tests.py index 4e82e4c..9ef77a5 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -17,7 +17,7 @@ import pytest import mock -from routing import Plugin, UrlRule, RoutingError, Script +from routing import Plugin, Script, UrlRule, RoutingError @pytest.fixture() @@ -25,6 +25,7 @@ def plugin(): return Plugin('plugin://py.test') +# Normally, we'd be testing both, but there's currently nothing that Script does and Plugin doesn't do @pytest.fixture() def script(): return Script('script://py.test') @@ -62,7 +63,7 @@ def test_url_for_kwargs(plugin): f = lambda a, b2: None plugin.route("/foo//")(f) assert plugin.url_for(f, a=1, b2=2) == plugin.base_url + "/foo/1/2" - + def test_url_for_args(plugin): f = lambda a, b2, c, d: None From 02f531a575bf89cb360e6efc420b6a3e6af7c21c Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Thu, 7 Mar 2019 20:40:17 -0500 Subject: [PATCH 03/26] super().__init__ is not compatible with Py2 --- lib/routing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index 7e0f269..0c346b9 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -156,7 +156,7 @@ def __init__(self, base_url=None, convert_args=False): self.base_url = base_url if self.base_url is None: self.base_url = "plugin://" + xbmcaddon.Addon().getAddonInfo('id') - super().__init__(self.base_url, convert_args) + Addon.__init__(self, self.base_url, convert_args) if len(sys.argv) < 2: # we are probably not dealing with a plugin, or it was called incorrectly from an addon raise TypeError('There was no handle provided. This needs to be called from a Kodi Plugin.') @@ -169,7 +169,7 @@ class Script(Addon): """ def __init__(self, base_url=None, convert_args=False): - super().__init__(base_url, convert_args) + Addon.__init__(self, base_url, convert_args) class UrlRule(object): From 91c94b1228e7064fb242bba04ea764ffc239880c Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sun, 10 Mar 2019 20:39:47 -0400 Subject: [PATCH 04/26] dispatch uses copied code...Fix it for exact matches. Fix test for parameter conversion --- lib/routing.py | 13 +++++++++++-- lib/tests.py | 11 ++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index 0c346b9..62399fa 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -129,11 +129,20 @@ def redirect(self, path): self._dispatch(path) def _dispatch(self, path): + for view_func, rules in iter(self._rules.items()): + for rule in rules: + if not rule.exact_match(path): + continue + log("Dispatching to '%s', exact match" % view_func.__name__) + view_func() + return + + # then, search for regex matches for view_func, rules in iter(self._rules.items()): for rule in rules: kwargs = rule.match(path) - if not kwargs: - return + if kwargs is None: + continue if self.convert_args: for k, v in kwargs.items(): new_val = try_convert(v) diff --git a/lib/tests.py b/lib/tests.py index 9ef77a5..579ce6d 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -25,6 +25,11 @@ def plugin(): return Plugin('plugin://py.test') +@pytest.fixture() +def plugin_convert(): + return Plugin('plugin://py.test', convert_args=True) + + # Normally, we'd be testing both, but there's currently nothing that Script does and Plugin doesn't do @pytest.fixture() def script(): @@ -115,8 +120,8 @@ def test_arg_parsing(plugin): assert plugin.args['bar'][0] == 'baz' and plugin.args['bar2'][0] == 'baz2' -def test_arg_conversion(plugin): +def test_arg_conversion(plugin_convert): def test_arg_conversion_inner(a, b2, c, d): assert a == 'bar' and b2 == True and c == 16.4 and d == 9 - plugin.route("/foo////")(test_arg_conversion_inner) - plugin.run(['plugin://py.test/foo/bar/true/16.4/9', '0', '']) + plugin_convert.route("/foo////")(test_arg_conversion_inner) + plugin_convert.run(['plugin://py.test/foo/bar/true/16.4/9', '0', '']) From 045d5d8d3cf44ce8203d6e5ea926dc2c6dcd8e46 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Mon, 11 Mar 2019 16:24:48 -0400 Subject: [PATCH 05/26] Fix argv for scripts (handles don't exist). Bump Version --- addon.xml | 2 +- lib/routing.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index 494c22c..cd672fb 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/lib/routing.py b/lib/routing.py index 62399fa..d29abab 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -180,6 +180,15 @@ class Script(Addon): def __init__(self, base_url=None, convert_args=False): Addon.__init__(self, base_url, convert_args) + def run(self, argv=sys.argv): + if len(argv) > 1: + self.args = parse_qs(argv[1].lstrip('?')) + path = urlsplit(argv[1]).path or '/' + else: + temp = urlsplit(argv[0]).path + path = temp if temp != self.base_url else '/' + self._dispatch(path) + class UrlRule(object): def __init__(self, pattern): From d8755726914ef502a9fa40186351eb63a5db5b3d Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Wed, 20 Mar 2019 18:00:48 -0400 Subject: [PATCH 06/26] Fix a minor bug in routing. Fix Unsorted Files Playback --- lib/routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routing.py b/lib/routing.py index d29abab..5f26e5c 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -146,7 +146,7 @@ def _dispatch(self, path): if self.convert_args: for k, v in kwargs.items(): new_val = try_convert(v) - if new_val: + if new_val != v: kwargs[k] = new_val log("Dispatching to '%s', args: %s" % (view_func.__name__, kwargs)) view_func(**kwargs) From a4496bbda6fda9337b5a82cb276cec63300994d6 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Thu, 21 Mar 2019 01:30:19 -0400 Subject: [PATCH 07/26] Fix a Minor Error in Converting Values --- lib/routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routing.py b/lib/routing.py index 5f26e5c..b22c5d9 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -268,4 +268,4 @@ def try_convert(value): if value.lower() == 'false': return False - return None + return value From 2cd8a88b9f19906dee9d07e3d7efe18efb1f28e4 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Thu, 21 Mar 2019 03:34:23 -0400 Subject: [PATCH 08/26] Bump Version --- addon.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index cd672fb..044374e 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + From a3916332623dfab5814146498f4be3187e75820d Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Fri, 22 Mar 2019 03:18:28 -0400 Subject: [PATCH 09/26] Sanitize args as well as kwargs. Perform a full route test on paths. --- lib/routing.py | 12 ++++++++---- lib/tests.py | 12 ++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index b22c5d9..759f078 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -22,9 +22,9 @@ except ImportError: from urllib.parse import urlsplit, parse_qs try: - from urllib import urlencode + from urllib import urlencode, quote, unquote except ImportError: - from urllib.parse import urlencode + from urllib.parse import urlencode, quote, unquote try: import xbmc @@ -212,7 +212,10 @@ def match(self, path): """ # match = self._regex.search(urlsplit(path).path) match = self._regex.search(path) - return match.groupdict() if match else None + if match is None: + return None + match = dict((k, unquote(unquote(v))) for k, v in match.groupdict().items()) + return match def exact_match(self, path): return not self._has_args and self._pattern == path @@ -224,8 +227,9 @@ def make_path(self, *args, **kwargs): if args: # Replace the named groups %s and format try: + args = tuple(quote(quote(str(x), ''), '') for x in args) return re.sub(r'{[A-z][A-z0-9]*}', r'%s', self._pattern) % args - except TypeError: + except TypeError as ex: return None # We need to find the keys from kwargs that occur in our pattern. diff --git a/lib/tests.py b/lib/tests.py index 579ce6d..0bb2983 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -94,6 +94,18 @@ def test_route_for_args(plugin): assert plugin.route_for(plugin.base_url + "/foo/a/b") is g +def test_path_args(plugin): + url = 'http://foo.bar:80/baz/bax.json?foo=bar&baz=bay' + + def test_path_args_inner(something): + assert something == url + + plugin.route('/do/')(test_path_args_inner) + path = plugin.url_for(test_path_args_inner, url) + assert plugin.route_for(path) is test_path_args_inner + plugin.run([path, '0', '']) + + def test_dispatch(plugin): f = mock.create_autospec(lambda: None) plugin.route("/foo")(f) From c42549135c780064abcabf3c52cda580764305da Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Fri, 22 Mar 2019 03:20:11 -0400 Subject: [PATCH 10/26] Clean up a debug variable --- lib/routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/routing.py b/lib/routing.py index 759f078..8792c1b 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -229,7 +229,7 @@ def make_path(self, *args, **kwargs): try: args = tuple(quote(quote(str(x), ''), '') for x in args) return re.sub(r'{[A-z][A-z0-9]*}', r'%s', self._pattern) % args - except TypeError as ex: + except TypeError: return None # We need to find the keys from kwargs that occur in our pattern. From 00dacef6e77d8b038ec454b9773880b6c06b953d Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Fri, 22 Mar 2019 03:20:51 -0400 Subject: [PATCH 11/26] Bump Version --- addon.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon.xml b/addon.xml index 044374e..e73259b 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + From 869dfc11b388d481ff476f4f67a9365d94354f28 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sat, 23 Mar 2019 13:32:38 -0400 Subject: [PATCH 12/26] Fix tests to use assert_called_with(args). Add some comments and move Plugin's run() to it --- lib/routing.py | 17 +++++++++++++---- lib/tests.py | 19 ++++++++++--------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index 8792c1b..461a166 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -120,10 +120,7 @@ def add_route(self, func, pattern): self._rules[func].append(rule) def run(self, argv=sys.argv): - if len(argv) > 2: - self.args = parse_qs(argv[2].lstrip('?')) - path = urlsplit(argv[0]).path or '/' - self._dispatch(path) + pass def redirect(self, path): self._dispatch(path) @@ -171,6 +168,15 @@ def __init__(self, base_url=None, convert_args=False): raise TypeError('There was no handle provided. This needs to be called from a Kodi Plugin.') self.handle = int(sys.argv[1]) if sys.argv[1].isdigit() else -1 + def run(self, argv=sys.argv): + # argv[1] is handle, so skip to 2 + if len(argv) > 2: + # parse query + self.args = parse_qs(argv[2].lstrip('?')) + # handle ['plugin.video.fun/some/menu'] + path = urlsplit(argv[0]).path or '/' + self._dispatch(path) + class Script(Addon): """ @@ -182,9 +188,12 @@ def __init__(self, base_url=None, convert_args=False): def run(self, argv=sys.argv): if len(argv) > 1: + # parse query self.args = parse_qs(argv[1].lstrip('?')) + # handle ['script.module.fun', '/do/something'] path = urlsplit(argv[1]).path or '/' else: + # handle ['script.module.fun/do/something'] temp = urlsplit(argv[0]).path path = temp if temp != self.base_url else '/' self._dispatch(path) diff --git a/lib/tests.py b/lib/tests.py index 0bb2983..ab521ef 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -89,21 +89,22 @@ def test_route_for_args(plugin): plugin.route("/foo/a/b")(g) # due to the unpredictable sorting of dict, just do it 100 times to see if it fails + # This is done because the exact_match processing did not exist, and it failed depending on order for i in range(0, 100): assert plugin.route_for(plugin.base_url + "/foo/1/2") is f assert plugin.route_for(plugin.base_url + "/foo/a/b") is g def test_path_args(plugin): + # full test to ensure that a path is both created properly and preserved throughout url = 'http://foo.bar:80/baz/bax.json?foo=bar&baz=bay' + f = mock.create_autospec(lambda a: None) - def test_path_args_inner(something): - assert something == url - - plugin.route('/do/')(test_path_args_inner) - path = plugin.url_for(test_path_args_inner, url) - assert plugin.route_for(path) is test_path_args_inner + plugin.route('/do/')(f) + path = plugin.url_for(f, url) + assert plugin.route_for(path) is f plugin.run([path, '0', '']) + f.assert_called_with(url) def test_dispatch(plugin): @@ -133,7 +134,7 @@ def test_arg_parsing(plugin): def test_arg_conversion(plugin_convert): - def test_arg_conversion_inner(a, b2, c, d): - assert a == 'bar' and b2 == True and c == 16.4 and d == 9 - plugin_convert.route("/foo////")(test_arg_conversion_inner) + f = mock.create_autospec(lambda a, b2, c, d: None) + plugin_convert.route("/foo////")(f) plugin_convert.run(['plugin://py.test/foo/bar/true/16.4/9', '0', '']) + f.assert_called_with('bar', True, 16.4, 9) From 8c821847c52fd4fe34fb33c4167238a52a66ad68 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sat, 23 Mar 2019 13:40:38 -0400 Subject: [PATCH 13/26] Change kwargs convert_args to use a prettier one-liner. Not as fast, but we shouldn't be doing this 1000 times, realistically. --- lib/routing.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index 461a166..f84e42c 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -141,10 +141,7 @@ def _dispatch(self, path): if kwargs is None: continue if self.convert_args: - for k, v in kwargs.items(): - new_val = try_convert(v) - if new_val != v: - kwargs[k] = new_val + kwargs = dict((k, try_convert(v)) for k, v in kwargs.items()) log("Dispatching to '%s', args: %s" % (view_func.__name__, kwargs)) view_func(**kwargs) return From 9dc4627843adf4a2a79583c7d023a80ca54a8a6d Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sat, 23 Mar 2019 13:49:47 -0400 Subject: [PATCH 14/26] Add comment --- lib/routing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/routing.py b/lib/routing.py index f84e42c..d0f1552 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -278,4 +278,5 @@ def try_convert(value): if value.lower() == 'false': return False + # the original is str, so we can "convert" to str by just returning return value From 6c86b572d25e01ae0483f89a4050c8372dce0022 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sat, 23 Mar 2019 14:03:51 -0400 Subject: [PATCH 15/26] Update path_args test to check query args as well. --- lib/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/tests.py b/lib/tests.py index ab521ef..5eef14f 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -103,8 +103,9 @@ def test_path_args(plugin): plugin.route('/do/')(f) path = plugin.url_for(f, url) assert plugin.route_for(path) is f - plugin.run([path, '0', '']) + plugin.run([path, '0', '?foo=bar&baz=bay']) f.assert_called_with(url) + assert plugin.args['foo'][0] == 'bar' and plugin.args['baz'][0] == 'bay' def test_dispatch(plugin): From dc7ab99503d7fe1abe7947da366aeec4a8090039 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Tue, 10 Sep 2019 02:51:33 -0400 Subject: [PATCH 16/26] Exact Matching, Support i1 pattern names, More Comprehensive Testing --- lib/routing.py | 49 ++++++++++++++++++++++++++++++++++++++----------- lib/tests.py | 26 ++++++++++++++++---------- 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index 1417dfb..683b7bb 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -81,7 +81,17 @@ def route_for(self, path): if path.startswith(self.base_url): path = path.split(self.base_url, 1)[1] - for view_fun, rules in iter(list(self._rules.items())): + # only list convert once + list_rules = list(self._rules.items()) + + # first, search for exact matches + for view_fun, rules in iter(list_rules): + for rule in rules: + if rule.exact_match(path): + return view_fun + + # then, search for regex matches + for view_fun, rules in iter(list_rules): for rule in rules: if rule.match(path) is not None: return view_fun @@ -133,13 +143,24 @@ def redirect(self, path): self._dispatch(path) def _dispatch(self, path): - for view_func, rules in iter(list(self._rules.items())): + list_rules = list(self._rules.items()) + for view_func, rules in iter(list_rules): + for rule in rules: + if not rule.exact_match(path): + continue + log("Dispatching to '%s', exact match" % view_func.__name__) + view_func() + return + + # then, search for regex matches + for view_func, rules in iter(list_rules): for rule in rules: kwargs = rule.match(path) - if kwargs is not None: - log("Dispatching to '%s', args: %s" % (view_func.__name__, kwargs)) - view_func(**kwargs) - return + if kwargs is None: + continue + log("Dispatching to '%s', args: %s" % (view_func.__name__, kwargs)) + view_func(**kwargs) + return raise RoutingError('No route to path "%s"' % path) @@ -147,13 +168,16 @@ class UrlRule: def __init__(self, pattern): pattern = pattern.rstrip('/') - kw_pattern = r'<(?:[^:]+:)?([A-z]+)>' + arg_regex = re.compile('<([A-z][A-z0-9]*)>') + self._has_args = bool(arg_regex.search(pattern)) + + kw_pattern = r'<(?:[^:]+:)?([A-z][A-z0-9]*)>' self._pattern = re.sub(kw_pattern, '{\\1}', pattern) self._keywords = re.findall(kw_pattern, pattern) - p = re.sub('<([A-z]+)>', '', pattern) - p = re.sub('', '(?P<\\1>[^/]+?)', p) - p = re.sub('', '(?P<\\1>.*)', p) + p = re.sub('<([A-z][A-z0-9]*)>', '', pattern) + p = re.sub('', '(?P<\\1>[^/]+?)', p) + p = re.sub('', '(?P<\\1>.*)', p) self._compiled_pattern = p self._regex = re.compile('^' + p + '$') @@ -166,6 +190,9 @@ def match(self, path): match = self._regex.search(path) return match.groupdict() if match else None + def exact_match(self, path): + return not self._has_args and self._pattern == path + def make_path(self, *args, **kwargs): """Construct a path from arguments.""" if args and kwargs: @@ -173,7 +200,7 @@ def make_path(self, *args, **kwargs): if args: # Replace the named groups %s and format try: - return re.sub(r'{[A-z]+}', r'%s', self._pattern) % args + return re.sub(r'{[A-z][A-z0-9]*}', r'%s', self._pattern) % args except TypeError: return None diff --git a/lib/tests.py b/lib/tests.py index 4482b74..c4f2ce1 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -56,15 +56,15 @@ def test_url_for(plugin): def test_url_for_kwargs(plugin): - f = lambda a, b: None - plugin.route("/foo//")(f) - assert plugin.url_for(f, a=1, b=2) == plugin.base_url + "/foo/1/2" + f = lambda a, b2: None + plugin.route("/foo//")(f) + assert plugin.url_for(f, a=1, b2=2) == plugin.base_url + "/foo/1/2" def test_url_for_args(plugin): - f = lambda a, b: None - plugin.route("//")(f) - assert plugin.url_for(f, 1, 2) == plugin.base_url + "/1/2" + f = lambda a, b2, c, d: None + plugin.route("////")(f) + assert plugin.url_for(f, 1, 2.6, True, 'baz') == plugin.base_url + "/1/2.6/True/baz" def test_route_for(plugin): @@ -75,8 +75,14 @@ def test_route_for(plugin): def test_route_for_args(plugin): f = lambda: None - plugin.route("/foo//")(f) - assert plugin.route_for(plugin.base_url + "/foo/1/2") is f + g = lambda: (None, None) # just to make sure that they are easily different + plugin.route("/foo//")(f) + plugin.route("/foo/a/b")(g) + + # due to the unpredictable sorting of dict, just do it 100 times to see if it fails + for i in range(0, 100): + assert plugin.route_for(plugin.base_url + "/foo/1/2") is f + assert plugin.route_for(plugin.base_url + "/foo/a/b") is g def test_dispatch(plugin): @@ -111,8 +117,8 @@ def test_no_route(plugin): def test_arg_parsing(plugin): f = mock.create_autospec(lambda: None) plugin.route("/foo")(f) - plugin.run(['plugin://py.test/foo', '0', '?bar=baz']) - assert plugin.args['bar'][0] == 'baz' + plugin.run(['plugin://py.test/foo', '0', '?bar=baz&bar2=baz2']) + assert plugin.args['bar'][0] == 'baz' and plugin.args['bar2'][0] == 'baz2' def test_trailing_slash_in_route_definition(plugin): """ Should call registered route with trailing slash. """ From c92945de5f67ace41ef84480b031348e86b81d48 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Tue, 10 Sep 2019 03:05:46 -0400 Subject: [PATCH 17/26] Fix a stupid PyLint complaint --- lib/tests.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/tests.py b/lib/tests.py index c4f2ce1..77843a4 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -80,7 +80,7 @@ def test_route_for_args(plugin): plugin.route("/foo/a/b")(g) # due to the unpredictable sorting of dict, just do it 100 times to see if it fails - for i in range(0, 100): + for _ in range(0, 100): assert plugin.route_for(plugin.base_url + "/foo/1/2") is f assert plugin.route_for(plugin.base_url + "/foo/a/b") is g @@ -120,6 +120,7 @@ def test_arg_parsing(plugin): plugin.run(['plugin://py.test/foo', '0', '?bar=baz&bar2=baz2']) assert plugin.args['bar'][0] == 'baz' and plugin.args['bar2'][0] == 'baz2' + def test_trailing_slash_in_route_definition(plugin): """ Should call registered route with trailing slash. """ f = mock.create_autospec(lambda: None) @@ -127,6 +128,7 @@ def test_trailing_slash_in_route_definition(plugin): plugin.run(['plugin://py.test/foo', '0']) assert f.call_count == 1 + def test_trailing_slashes_in_run(plugin): """ Should call registered route without trailing slash. """ f = mock.create_autospec(lambda: None) @@ -134,6 +136,7 @@ def test_trailing_slashes_in_run(plugin): plugin.run(['plugin://py.test/foo/', '0']) assert f.call_count == 1 + def test_trailing_slash_handling_for_root(plugin): f = mock.create_autospec(lambda: None) plugin.route("/")(lambda: None) From be8c654c0ecf607cab4be89dc905fd9d1e04255a Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Tue, 10 Sep 2019 16:45:38 -0400 Subject: [PATCH 18/26] Change b2 to var_with_num_underscore2. Support Underscores --- lib/routing.py | 12 ++++++------ lib/tests.py | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index 683b7bb..535257c 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -168,16 +168,16 @@ class UrlRule: def __init__(self, pattern): pattern = pattern.rstrip('/') - arg_regex = re.compile('<([A-z][A-z0-9]*)>') + arg_regex = re.compile('<([A-z_][A-z0-9_]*)>') self._has_args = bool(arg_regex.search(pattern)) - kw_pattern = r'<(?:[^:]+:)?([A-z][A-z0-9]*)>' + kw_pattern = r'<(?:[^:]+:)?([A-z_][A-z0-9_]*)>' self._pattern = re.sub(kw_pattern, '{\\1}', pattern) self._keywords = re.findall(kw_pattern, pattern) - p = re.sub('<([A-z][A-z0-9]*)>', '', pattern) - p = re.sub('', '(?P<\\1>[^/]+?)', p) - p = re.sub('', '(?P<\\1>.*)', p) + p = re.sub('<([A-z_][A-z0-9_]*)>', '', pattern) + p = re.sub('', '(?P<\\1>[^/]+?)', p) + p = re.sub('', '(?P<\\1>.*)', p) self._compiled_pattern = p self._regex = re.compile('^' + p + '$') @@ -200,7 +200,7 @@ def make_path(self, *args, **kwargs): if args: # Replace the named groups %s and format try: - return re.sub(r'{[A-z][A-z0-9]*}', r'%s', self._pattern) % args + return re.sub(r'{[A-z_][A-z0-9_]*}', r'%s', self._pattern) % args except TypeError: return None diff --git a/lib/tests.py b/lib/tests.py index 77843a4..bf4ccf3 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -56,14 +56,14 @@ def test_url_for(plugin): def test_url_for_kwargs(plugin): - f = lambda a, b2: None - plugin.route("/foo//")(f) - assert plugin.url_for(f, a=1, b2=2) == plugin.base_url + "/foo/1/2" + f = lambda a, var_with_num_underscore2: None + plugin.route("/foo//")(f) + assert plugin.url_for(f, a=1, var_with_num_underscore2=2) == plugin.base_url + "/foo/1/2" def test_url_for_args(plugin): - f = lambda a, b2, c, d: None - plugin.route("////")(f) + f = lambda a, var_with_num_underscore2, c, d: None + plugin.route("////")(f) assert plugin.url_for(f, 1, 2.6, True, 'baz') == plugin.base_url + "/1/2.6/True/baz" @@ -74,9 +74,9 @@ def test_route_for(plugin): def test_route_for_args(plugin): - f = lambda: None + f = lambda a, var_with_num_underscore2: None g = lambda: (None, None) # just to make sure that they are easily different - plugin.route("/foo//")(f) + plugin.route("/foo//")(f) plugin.route("/foo/a/b")(g) # due to the unpredictable sorting of dict, just do it 100 times to see if it fails From 3b17bfc0bc1415d3ecf73cbd325704fdd1fc87a8 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sun, 27 Oct 2019 15:18:32 -0400 Subject: [PATCH 19/26] Double quote and unquote variable values --- lib/routing.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index 535257c..817ce38 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -24,9 +24,9 @@ except ImportError: from urllib.parse import urlsplit, parse_qs try: - from urllib import urlencode + from urllib import urlencode, quote as q, unquote as uq except ImportError: - from urllib.parse import urlencode + from urllib.parse import urlencode, quote as q, unquote as uq try: import xbmc @@ -188,7 +188,9 @@ def match(self, path): """ # match = self._regex.search(urlsplit(path).path) match = self._regex.search(path) - return match.groupdict() if match else None + if match is None: + return None + return dict((k, uq(uq(v))) for k, v in match.groupdict().items()) def exact_match(self, path): return not self._has_args and self._pattern == path @@ -200,14 +202,15 @@ def make_path(self, *args, **kwargs): if args: # Replace the named groups %s and format try: + args = tuple(q(q(str(x), ''), '') for x in args) return re.sub(r'{[A-z_][A-z0-9_]*}', r'%s', self._pattern) % args except TypeError: return None # We need to find the keys from kwargs that occur in our pattern. # Unknown keys are pushed to the query string. - url_kwargs = dict(((k, v) for k, v in list(kwargs.items()) if k in self._keywords)) - qs_kwargs = dict(((k, v) for k, v in list(kwargs.items()) if k not in self._keywords)) + url_kwargs = dict(((k, q(q(str(v), ''), '')) for k, v in list(kwargs.items()) if k in self._keywords)) + qs_kwargs = dict(((k, q(q(str(v), ''), '')) for k, v in list(kwargs.items()) if k not in self._keywords)) query = '?' + urlencode(qs_kwargs) if qs_kwargs else '' try: From cfcb323b704c053b65684dcfaf21eefc9a20006b Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Fri, 10 Jan 2020 01:40:30 -0500 Subject: [PATCH 20/26] Fix double unquote on query. Fix Relevant Tests. --- lib/routing.py | 4 ++-- lib/tests.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index 817ce38..f367910 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -106,8 +106,7 @@ def url_for(self, func, *args, **kwargs): path = rule.make_path(*args, **kwargs) if path is not None: return self.url_for_path(path) - raise RoutingError("No known paths to '{0}' with args {1} and " - "kwargs {2}".format(func.__name__, args, kwargs)) + raise RoutingError("No known paths to '{0}' with args {1} and kwargs {2}".format(func.__name__, args, kwargs)) def url_for_path(self, path): """ @@ -137,6 +136,7 @@ def run(self, argv=None): self.path = self.path.rstrip('/') if len(argv) > 2: self.args = parse_qs(argv[2].lstrip('?')) + self.args = dict((k, list(uq(uq(v2)) for v2 in v)) for k, v in self.args.items()) self._dispatch(self.path) def redirect(self, path): diff --git a/lib/tests.py b/lib/tests.py index bf4ccf3..ac0eacb 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -39,9 +39,14 @@ def test_make_path(): assert rule.make_path(1) is None -def test_make_path_should_urlencode_args(): - rule = UrlRule("/foo") - assert rule.make_path(bar="b a&r") == "/foo?bar=b+a%26r" +def test_make_path_should_urlencode_args(plugin): + f = mock.create_autospec(lambda: None) + plugin.route('/foo')(f) + # we wanted double quote for the +, %, and any others that might be in the string + assert plugin.url_for(f, bar='b a&r+c') == plugin.base_url + '/foo?bar=b%252520a%252526r%25252Bc' + plugin.run(['plugin://py.test/foo', '0', '?bar=b%252520a%252526r%25252Bc']) + f.assert_called_with() + assert plugin.args['bar'] == ['b a&r+c'] def test_url_for_path(): @@ -90,6 +95,7 @@ def test_dispatch(plugin): plugin.route("/foo")(f) plugin.run(['plugin://py.test/foo', '0', '?bar=baz']) f.assert_called_with() + assert plugin.args['bar'] == ['baz'] def test_path(plugin): From 2e91ce251729d4db0af7e99de5b12fa151183986 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Fri, 10 Jan 2020 01:44:19 -0500 Subject: [PATCH 21/26] Fix some pylint complaints about line length --- lib/routing.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index f367910..087d5b5 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -106,7 +106,8 @@ def url_for(self, func, *args, **kwargs): path = rule.make_path(*args, **kwargs) if path is not None: return self.url_for_path(path) - raise RoutingError("No known paths to '{0}' with args {1} and kwargs {2}".format(func.__name__, args, kwargs)) + raise RoutingError("No known paths to '{0}' with args {1} " + "and kwargs {2}".format(func.__name__, args, kwargs)) def url_for_path(self, path): """ @@ -209,8 +210,10 @@ def make_path(self, *args, **kwargs): # We need to find the keys from kwargs that occur in our pattern. # Unknown keys are pushed to the query string. - url_kwargs = dict(((k, q(q(str(v), ''), '')) for k, v in list(kwargs.items()) if k in self._keywords)) - qs_kwargs = dict(((k, q(q(str(v), ''), '')) for k, v in list(kwargs.items()) if k not in self._keywords)) + url_kwargs = dict(((k, q(q(str(v), ''), '')) for k, v in list(kwargs.items()) + if k in self._keywords)) + qs_kwargs = dict(((k, q(q(str(v), ''), '')) for k, v in list(kwargs.items()) + if k not in self._keywords)) query = '?' + urlencode(qs_kwargs) if qs_kwargs else '' try: From fe210f3fbe703d689cbb60ca7ccf386b6c6a8e3e Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Fri, 10 Jan 2020 01:46:13 -0500 Subject: [PATCH 22/26] One more stupid pylint --- lib/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/tests.py b/lib/tests.py index ac0eacb..a5cfaea 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -43,7 +43,8 @@ def test_make_path_should_urlencode_args(plugin): f = mock.create_autospec(lambda: None) plugin.route('/foo')(f) # we wanted double quote for the +, %, and any others that might be in the string - assert plugin.url_for(f, bar='b a&r+c') == plugin.base_url + '/foo?bar=b%252520a%252526r%25252Bc' + assert plugin.url_for(f, bar='b a&r+c') == \ + plugin.base_url + '/foo?bar=b%252520a%252526r%25252Bc' plugin.run(['plugin://py.test/foo', '0', '?bar=b%252520a%252526r%25252Bc']) f.assert_called_with() assert plugin.args['bar'] == ['b a&r+c'] From c0b45528c1452e46e034d7fa3445701c35df31ff Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sat, 11 Jan 2020 23:15:26 -0500 Subject: [PATCH 23/26] Remove the for loop in tests.py --- lib/tests.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/tests.py b/lib/tests.py index a5cfaea..bf7b565 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -85,10 +85,8 @@ def test_route_for_args(plugin): plugin.route("/foo//")(f) plugin.route("/foo/a/b")(g) - # due to the unpredictable sorting of dict, just do it 100 times to see if it fails - for _ in range(0, 100): - assert plugin.route_for(plugin.base_url + "/foo/1/2") is f - assert plugin.route_for(plugin.base_url + "/foo/a/b") is g + assert plugin.route_for(plugin.base_url + "/foo/1/2") is f + assert plugin.route_for(plugin.base_url + "/foo/a/b") is g def test_dispatch(plugin): From 8115f3247825a590cd0c7a1410838e5932dd4100 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sun, 12 Jan 2020 00:24:13 -0500 Subject: [PATCH 24/26] Remove the quoting stuff. I don't know what I was thinking.... --- lib/routing.py | 16 +++++----------- lib/tests.py | 4 ++-- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/lib/routing.py b/lib/routing.py index 087d5b5..52ce7ac 100644 --- a/lib/routing.py +++ b/lib/routing.py @@ -24,9 +24,9 @@ except ImportError: from urllib.parse import urlsplit, parse_qs try: - from urllib import urlencode, quote as q, unquote as uq + from urllib import urlencode except ImportError: - from urllib.parse import urlencode, quote as q, unquote as uq + from urllib.parse import urlencode try: import xbmc @@ -137,7 +137,6 @@ def run(self, argv=None): self.path = self.path.rstrip('/') if len(argv) > 2: self.args = parse_qs(argv[2].lstrip('?')) - self.args = dict((k, list(uq(uq(v2)) for v2 in v)) for k, v in self.args.items()) self._dispatch(self.path) def redirect(self, path): @@ -189,9 +188,7 @@ def match(self, path): """ # match = self._regex.search(urlsplit(path).path) match = self._regex.search(path) - if match is None: - return None - return dict((k, uq(uq(v))) for k, v in match.groupdict().items()) + return match.groupdict() if match else None def exact_match(self, path): return not self._has_args and self._pattern == path @@ -203,17 +200,14 @@ def make_path(self, *args, **kwargs): if args: # Replace the named groups %s and format try: - args = tuple(q(q(str(x), ''), '') for x in args) return re.sub(r'{[A-z_][A-z0-9_]*}', r'%s', self._pattern) % args except TypeError: return None # We need to find the keys from kwargs that occur in our pattern. # Unknown keys are pushed to the query string. - url_kwargs = dict(((k, q(q(str(v), ''), '')) for k, v in list(kwargs.items()) - if k in self._keywords)) - qs_kwargs = dict(((k, q(q(str(v), ''), '')) for k, v in list(kwargs.items()) - if k not in self._keywords)) + url_kwargs = dict(((k, v) for k, v in list(kwargs.items()) if k in self._keywords)) + qs_kwargs = dict(((k, v) for k, v in list(kwargs.items()) if k not in self._keywords)) query = '?' + urlencode(qs_kwargs) if qs_kwargs else '' try: diff --git a/lib/tests.py b/lib/tests.py index bf7b565..26ae1f8 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -44,8 +44,8 @@ def test_make_path_should_urlencode_args(plugin): plugin.route('/foo')(f) # we wanted double quote for the +, %, and any others that might be in the string assert plugin.url_for(f, bar='b a&r+c') == \ - plugin.base_url + '/foo?bar=b%252520a%252526r%25252Bc' - plugin.run(['plugin://py.test/foo', '0', '?bar=b%252520a%252526r%25252Bc']) + plugin.base_url + '/foo?bar=b+a%26r%2Bc' + plugin.run(['plugin://py.test/foo', '0', '?bar=b+a%26r%2Bc']) f.assert_called_with() assert plugin.args['bar'] == ['b a&r+c'] From 078ac250da27f35aca3eeebc6994113688213b59 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sun, 12 Jan 2020 00:31:25 -0500 Subject: [PATCH 25/26] Remove irrelevant comment --- lib/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tests.py b/lib/tests.py index 26ae1f8..8440360 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -42,7 +42,7 @@ def test_make_path(): def test_make_path_should_urlencode_args(plugin): f = mock.create_autospec(lambda: None) plugin.route('/foo')(f) - # we wanted double quote for the +, %, and any others that might be in the string + assert plugin.url_for(f, bar='b a&r+c') == \ plugin.base_url + '/foo?bar=b+a%26r%2Bc' plugin.run(['plugin://py.test/foo', '0', '?bar=b+a%26r%2Bc']) From 2c7be8129c12a29b5a196e6f29e750b8b97b1f10 Mon Sep 17 00:00:00 2001 From: da3dsoul Date: Sun, 12 Jan 2020 00:34:38 -0500 Subject: [PATCH 26/26] More consistency in the argument checking in tests.py --- lib/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tests.py b/lib/tests.py index 8440360..b15b5c2 100644 --- a/lib/tests.py +++ b/lib/tests.py @@ -47,7 +47,7 @@ def test_make_path_should_urlencode_args(plugin): plugin.base_url + '/foo?bar=b+a%26r%2Bc' plugin.run(['plugin://py.test/foo', '0', '?bar=b+a%26r%2Bc']) f.assert_called_with() - assert plugin.args['bar'] == ['b a&r+c'] + assert plugin.args['bar'][0] == 'b a&r+c' def test_url_for_path(): @@ -94,7 +94,7 @@ def test_dispatch(plugin): plugin.route("/foo")(f) plugin.run(['plugin://py.test/foo', '0', '?bar=baz']) f.assert_called_with() - assert plugin.args['bar'] == ['baz'] + assert plugin.args['bar'][0] == 'baz' def test_path(plugin):