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

[BREAKING] Only match non-empty string #58

Merged
merged 2 commits into from Feb 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion sanic_routing/patterns.py
Expand Up @@ -19,6 +19,12 @@ def slug(param: str) -> str:
return param


def nonemptystr(param: str) -> str:
if not param:
raise ValueError(f"Value {param} is an empty string")
return param


REGEX_PARAM_NAME = re.compile(r"^<([a-zA-Z_][a-zA-Z0-9_]*)(?::(.*))?>$")

# Predefined path parameter types. The value is a tuple consisteing of a
Expand All @@ -30,7 +36,8 @@ def slug(param: str) -> str:
# The regular expression is generally NOT used. Unless the path is forced
# to use regex patterns.
REGEX_TYPES = {
"str": (str, re.compile(r"^[^/]+$")),
"strorempty": (str, re.compile(r"^[^/]*$")),
"str": (nonemptystr, re.compile(r"^[^/]+$")),
"slug": (slug, re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")),
"alpha": (alpha, re.compile(r"^[A-Za-z]+$")),
"path": (str, re.compile(r"^[^/]?.*?$")),
Expand Down
2 changes: 1 addition & 1 deletion sanic_routing/router.py
Expand Up @@ -27,7 +27,7 @@
from datetime import datetime # noqa isort:skip
from urllib.parse import unquote # noqa isort:skip
from uuid import UUID # noqa isort:skip
from .patterns import parse_date, alpha, slug # noqa isort:skip
from .patterns import parse_date, alpha, slug, nonemptystr # noqa isort:skip


class BaseRouter(ABC):
Expand Down
103 changes: 102 additions & 1 deletion tests/test_builtin_param_types.py
@@ -1,5 +1,6 @@
import pytest
from unittest.mock import Mock

import pytest
from sanic_routing import BaseRouter
from sanic_routing.exceptions import NotFound

Expand Down Expand Up @@ -131,3 +132,103 @@ def test_correct_slug_v_string(handler):

assert isinstance(retval, str)
assert retval == "FooBar"


@pytest.mark.parametrize(
"value,matches",
(
("foo", True),
("FooBar", True),
("with123456789", True),
("", False),
),
)
def test_nonempty_string(handler, value, matches):
def test(path):
nonlocal handler
router = Router()

router.add(path, handler)
router.finalize()

if matches:
_, handler, params = router.get(f"/{value}", "BASE")
retval = handler(**params)

assert isinstance(retval, str)
assert retval == value
else:
with pytest.raises(NotFound):
router.get(f"/{value}", "BASE")

for path in ("/<foo>", "/<foo:str>"):
test(path)


@pytest.mark.parametrize(
"value",
(
"foo",
"FooBar",
"with123456789",
"",
),
)
def test_empty_string(handler, value):
router = Router()

router.add("/<foo:strorempty>", handler)
router.finalize()

_, handler, params = router.get(f"/{value}", "BASE")
retval = handler(**params)

assert isinstance(retval, str)
assert retval == value


def test_nonempty_hierarchy():
handler1 = Mock()
handler2 = Mock()
router = Router()

router.add("/one/<foo>", handler1)
router.add("/one/<foo>/<bar>", handler2)
router.finalize()

_, handler, params = router.get("/one/two/", "BASE")
expected = {"foo": "two"}
handler(**params)

assert params == expected
handler1.assert_called_once_with(**expected)
handler2.assert_not_called()

handler1.reset_mock()
handler2.reset_mock()

_, handler, params = router.get("/one/two/three/", "BASE")
expected = {"foo": "two", "bar": "three"}
handler(**params)

assert params == expected
handler1.assert_not_called()
handler2.assert_called_once_with(**expected)


def test_empty_hierarchy():
handler1 = Mock()
handler2 = Mock()
router = Router()

router.add("/one/<foo>", handler1)
router.add("/one/<foo>/<bar:strorempty>", handler2)
router.finalize()

_, handler, params = router.get("/one/two/", "BASE")
expected = {"foo": "two", "bar": ""}
handler(**params)

assert params == expected
handler1.assert_not_called()
handler2.assert_called_once_with(**expected)