Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Exact URL Matching. Allow Syntactically Correct Patterns. Better Tests #25

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c9b3e26
Add a Handler for Scripts + more
da3dsoul Mar 7, 2019
445f146
Stupid Whitespace Fix. Sort import better. Add a comment
da3dsoul Mar 7, 2019
02f531a
super().__init__ is not compatible with Py2
da3dsoul Mar 8, 2019
91c94b1
dispatch uses copied code...Fix it for exact matches. Fix test for pa…
da3dsoul Mar 11, 2019
045d5d8
Fix argv for scripts (handles don't exist). Bump Version
da3dsoul Mar 11, 2019
d875572
Fix a minor bug in routing. Fix Unsorted Files Playback
da3dsoul Mar 20, 2019
a4496bb
Fix a Minor Error in Converting Values
da3dsoul Mar 21, 2019
2cd8a88
Bump Version
da3dsoul Mar 21, 2019
a391633
Sanitize args as well as kwargs. Perform a full route test on paths.
da3dsoul Mar 22, 2019
c425491
Clean up a debug variable
da3dsoul Mar 22, 2019
00dacef
Bump Version
da3dsoul Mar 22, 2019
869dfc1
Fix tests to use assert_called_with(args).
da3dsoul Mar 23, 2019
8c82184
Change kwargs convert_args to use a prettier one-liner.
da3dsoul Mar 23, 2019
9dc4627
Add comment
da3dsoul Mar 23, 2019
6c86b57
Update path_args test to check query args as well.
da3dsoul Mar 23, 2019
dc7ab99
Exact Matching, Support i1 pattern names, More Comprehensive Testing
da3dsoul Sep 10, 2019
5e1bb33
Merge remote-tracking branch 'origin/master'
da3dsoul Sep 10, 2019
c92945d
Fix a stupid PyLint complaint
da3dsoul Sep 10, 2019
be8c654
Change b2 to var_with_num_underscore2. Support Underscores
da3dsoul Sep 10, 2019
3b17bfc
Double quote and unquote variable values
da3dsoul Oct 27, 2019
cfcb323
Fix double unquote on query. Fix Relevant Tests.
da3dsoul Jan 10, 2020
2e91ce2
Fix some pylint complaints about line length
da3dsoul Jan 10, 2020
fe210f3
One more stupid pylint
da3dsoul Jan 10, 2020
c0b4552
Remove the for loop in tests.py
da3dsoul Jan 12, 2020
8115f32
Remove the quoting stuff. I don't know what I was thinking....
da3dsoul Jan 12, 2020
078ac25
Remove irrelevant comment
da3dsoul Jan 12, 2020
2c7be81
More consistency in the argument checking in tests.py
da3dsoul Jan 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 38 additions & 11 deletions lib/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -133,27 +143,41 @@ 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)


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]+)>', '<string:\\1>', pattern)
p = re.sub('<string:([A-z]+)>', '(?P<\\1>[^/]+?)', p)
p = re.sub('<path:([A-z]+)>', '(?P<\\1>.*)', p)
p = re.sub('<([A-z][A-z0-9]*)>', '<string:\\1>', pattern)
p = re.sub('<string:([A-z][A-z0-9]*)>', '(?P<\\1>[^/]+?)', p)
p = re.sub('<path:([A-z][A-z0-9]*)>', '(?P<\\1>.*)', p)
self._compiled_pattern = p
self._regex = re.compile('^' + p + '$')

Expand All @@ -166,14 +190,17 @@ 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:
return None # can't use both args and 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

Expand Down
29 changes: 19 additions & 10 deletions lib/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ def test_url_for(plugin):


def test_url_for_kwargs(plugin):
f = lambda a, b: None
plugin.route("/foo/<a>/<b>")(f)
assert plugin.url_for(f, a=1, b=2) == plugin.base_url + "/foo/1/2"
f = lambda a, b2: None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the relevance is of renaming b to b2 in these tests ;-)

plugin.route("/foo/<a>/<b2>")(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("/<a>/<b>")(f)
assert plugin.url_for(f, 1, 2) == plugin.base_url + "/1/2"
f = lambda a, b2, c, d: None
plugin.route("/<a>/<b2>/<c>/<d>")(f)
assert plugin.url_for(f, 1, 2.6, True, 'baz') == plugin.base_url + "/1/2.6/True/baz"


def test_route_for(plugin):
Expand All @@ -75,8 +75,14 @@ def test_route_for(plugin):

def test_route_for_args(plugin):
f = lambda: None
plugin.route("/foo/<a>/<b>")(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/<a>/<b2>")(f)
plugin.route("/foo/a/b")(g)

# due to the unpredictable sorting of dict, just do it 100 times to see if it fails
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I doubt this is needed, dict-ordering is not guaranteed (in Python 3.5 or older) but it is deterministic. It is not random.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not in Py3, but it was an issue before this PR, so it's there to prove that it's fixed with the PR

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think this is a problem on Python 2 either. Again, the ordering is not guaranteed, but it is deterministic. So every single run for the 100 runs will be identical.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You would think, right? I expected that, since every other language I've worked with is like that. I can tell you that, in my attempts just to find this issue, I proved the opposite on Python 2.7. When it says that it can't guarantee order there, for the first time I've seen, it really means it.

Just for the sake of it, I'll write up a test case and run it. If it yields consistent results, then I'll concede.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PS C:\Users\da3ds> & "C:/Program Files/Python37/python.exe" c:/Users/da3ds/Documents/loop_test.py
apples
PS C:\Users\da3ds> & "C:/Program Files/Python37/python.exe" c:/Users/da3ds/Documents/loop_test.py
apples
PS C:\Users\da3ds> & "C:/Program Files/Python37/python.exe" c:/Users/da3ds/Documents/loop_test.py
apples
PS C:\Users\da3ds> & "C:/Program Files/Python27/python.exe" c:/Users/da3ds/Documents/loop_test.py
beetroot
PS C:\Users\da3ds> & "C:/Program Files/Python27/python.exe" c:/Users/da3ds/Documents/loop_test.py
beetroot
PS C:\Users\da3ds> & "C:/Program Files/Python27/python.exe" c:/Users/da3ds/Documents/loop_test.py
beetroot

You win. I don't know what was different before that was giving unreliable results, but it definitely was. That was a long time ago, a different installation of Windows, etc.

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


def test_dispatch(plugin):
Expand Down Expand Up @@ -111,8 +117,9 @@ 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. """
Expand All @@ -121,13 +128,15 @@ 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)
plugin.route("/foo")(f)
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("/<a>")(lambda: None)
Expand Down