GithubHelp home page GithubHelp logo

sanic-org / sanic-routing Goto Github PK

View Code? Open in Web Editor NEW
13.0 13.0 11.0 118 KB

Internal handler routing for Sanic beginning with v21.3.

Home Page: https://sanicframework.org/

License: MIT License

Python 99.72% Makefile 0.28%
python routing sanic webframework

sanic-routing's People

Contributors

ahankinson avatar ahopkins avatar ashleysommer avatar eric-spitler avatar kserhii avatar matemax avatar nichmor avatar wochinge avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

sanic-routing's Issues

path_to_parts parsing bug

Describe the bug

In my project, I have an endpoint

@api.route(
    r'/image/iiif/'
    r'<image_id>/'
    r'<region:full|square|\d+,\d+,\d+,\d+>/'
    r'<size:max|\d+,|,\d+|\d+,\d+>/'
    r'<rotation:int>/'
    r'default.jpg')

It works for Sanic==20.12.1, but after migration to Sanic==21.3.4 it's not a valid router anymore.

parts = ('api', 'image', 'iiif', '<image_id>/<region:full|square|\\d+,\\d+,\\d+,\\d+>', '<size:max|\\d+,|,\\d+|\\d+,\\d+>', '<rotation:int>', ...)
delimiter = '/'

    def parts_to_path(parts, delimiter="/"):
        path = []
        for part in parts:
            if part.startswith("<"):
                try:
                    match = REGEX_PARAM_NAME.match(part)
                    param_type = ""
>                   if match.group(2):
E                   AttributeError: 'NoneType' object has no attribute 'group'

../venv/lib/python3.8/site-packages/sanic_routing/utils.py:76: AttributeError

How to reproduce

>>> from sanic_routing.utils import path_to_parts
>>> s = (
...     r'/image/iiif/'
...     r'<image_id>/'
...     r'<region:full|square|\d+,\d+,\d+,\d+>/'
...     r'<size:max|\d+,|,\d+|\d+,\d+>/'
...     r'<rotation:int>/'
...     r'default.jpg')
>> path_to_parts(s)
('image', 'iiif', '<image_id>/<region:full|square|\\d+,\\d+,\\d+,\\d+>', '<size:max|\\d+,|,\\d+|\\d+,\\d+>', '<rotation:int>', 'default.jpg')

Expected behavior

>> path_to_parts(s)
('image', 'iiif', '<image_id>', '<region:full|square|\\d+,\\d+,\\d+,\\d+>', '<size:max|\\d+,|,\\d+|\\d+,\\d+>', '<rotation:int>', 'default.jpg')

Environment

  • OS: Linux (Ubuntu 20.04)
  • Python: 3.8
  • Sanic version: 21.3.4
  • Sanic-routing version: 0.6.2

Routes with extension

A very common endpoint is to do some sort of a match on an extension:

/foo/bar.jpg

Usually, you want to get bar as a variable value. I propose a new route segment type: ext that would allow make it super simple to setup these types of routes.

There are three general scenarios:

  1. Match any extension
  2. Match a single extension
  3. Match one of a specific set of extensions

I think something like the following should work:

/foo/<file:ext>
/foo/<file:ext:jpg>
/foo/<file:ext:jpg|png|gif|svg>

This would result in two passed variables: whatever is defined, and the extension as ext.

For example:

@app.get("/invoice/<invoice_id:ext:pdf|txt>")
async def invoice_handler(request, invoice_id, ext):
    ...

"unquote=True" breaks int path parameters

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

Routes that both set unquote=True and have int path parameters will cause a bug in the Sanic router, resulting in 500 errors when visited.

Code snippet

Run this code:

from sanic import Sanic
from sanic.response import text

app = Sanic('app')

@app.route("/test/<x:int>", unquote=True)
async def handler(request, x: int):
    return text(f'hi {x}')

if __name__=='__main__':
    app.run(port=4321, debug=True)

Then visit /test/1.

Sanic raises a 500 error:

[2023-07-17 03:41:24 +0800] [52892] [ERROR] Exception occurred while handling uri: 'http://127.0.0.1:4321/test/1'
Traceback (most recent call last):
  File "C:\Users\xmcp\AppData\Roaming\Python\Python310\site-packages\sanic\app.py", line 904, in handle_request
    route, handler, kwargs = self.router.get(
  File "C:\Users\xmcp\AppData\Roaming\Python\Python310\site-packages\sanic\router.py", line 64, in get
    return self._get(path, method, host)
  File "C:\Users\xmcp\AppData\Roaming\Python\Python310\site-packages\sanic\router.py", line 36, in _get
    return self.resolve(
  File "C:\Users\xmcp\AppData\Roaming\Python\Python310\site-packages\sanic_routing\router.py", line 82, in resolve
    route, param_basket = self.find_route(
  File "", line 22, in find_route
  File "C:\Program Files\Python310\lib\urllib\parse.py", line 656, in unquote
    if '%' not in string:
TypeError: argument of type 'int' is not iterable
[2023-07-17 03:41:24 +0800] - (sanic.access)[INFO][127.0.0.1:50831]: GET http://127.0.0.1:4321/test/1  500 2402

Expected Behavior

It should return hi 1 without a 500 error.

How do you run Sanic?

As a script (app.run or Sanic.serve)

Operating System

Windows

Sanic Version

Sanic 22.12.0; Routing 22.8.0

Additional context

self.find_route_src in the sanic router says something like that:

if num == 4:  # CHECK 1
    try:
        basket['__matches__'][3] = int(parts[3])
    except ValueError:
        pass
    else:
        basket['__matches__'][3] = unquote(basket['__matches__'][3])

You can see that Sanic converts the request arg into an integer and then tries to unquote it.

Expandable routes have more priority than static routes when adding a trailing slash to the path

Problem

There seems to be an unexpected behavior when mixing paths with trailing slashes and a route with a path/regex parameter. When creating an app with strict_slashes=False, one would expect that static routes will match paths with and without the trailing slash as a top priority. But paths with a trailing slash are actually a "last resource" when looking for a matching route.

Code snippet

from sanic import Sanic, response

app = Sanic("MyApp", strict_slashes=False)


async def hello_world(request, *args, **kwargs):
  return response.text('Hello World!')


async def catch_all(request, path):
  return response.text(f'Caught one: {path}')


app.add_route(hello_world, '/hello/')  # same behavior using '/hello'
app.add_route(catch_all, '/<path:path>')

if __name__ == '__main__':
  app.run(port=8080)

Expected behavior

Because strict_slashes=False, the hello_world route is supposed to match both /hello and /hello/. But the latter is being matched with the catch_all route.

Looks like this is happening due to how the resolve method works rigt now: https://github.com/sanic-org/sanic-routing/blob/main/sanic_routing/router.py#L92; it's looking for the route with the trailing slash only when no other route would match.

Sanic version

  • Sanic v23.6.0
  • sanic-routing==23.6.0

SyntaxError: Compiler improperly indenting try/else blocks

After updating to Sanic 21.3 and bringing in sanic-routing, I am running into issues starting the server.

I can't find where the try/else part is being built to propose a solution, but here is a quick example that produces the error.

EXAMPLE APP

from multiprocessing import Process
import time

import requests
from sanic import Blueprint, HTTPResponse, Request, Sanic


async def get(request: Request, *args, **kwargs) -> HTTPResponse:
    print(request.route.path)
    print(args)
    print(kwargs)
    return HTTPResponse()


bp0 = Blueprint(name='base')
bp1 = Blueprint(name='first', url_prefix='/<first>')
bp2 = Blueprint(name='second', url_prefix='/<first>/<second>')
bp3 = Blueprint(name='third', url_prefix='/<first>/<second>/<third>')

bp0.add_route(get, '/')
bp1.add_route(get, '/')
bp2.add_route(get, '/')
bp3.add_route(get, '/')

app = Sanic(name='test')
app.blueprint(bp0)
app.blueprint(bp1)
app.blueprint(bp2)
app.blueprint(bp3)

p = Process(target=app.run, kwargs={'port': 9000})
try:
    p.start()
    time.sleep(2)

    print(requests.get('http://localhost:9000/'))
    print(requests.get('http://localhost:9000/1'))
    print(requests.get('http://localhost:9000/1/2'))
    print(requests.get('http://localhost:9000/1/2/3'))
finally:
    p.terminate()

EXCEPTION

[2021-03-30 23:04:01 +0000] [6332] [ERROR] Experienced exception while trying to serve
Traceback (most recent call last):
  File "/home/my-project/env/lib/python3.7/site-packages/sanic/app.py", line 918, in run
    serve_single(server_settings)
  File "/home/my-project/env/lib/python3.7/site-packages/sanic/server.py", line 725, in serve_single
    serve(**server_settings)
  File "/home/my-project/env/lib/python3.7/site-packages/sanic/server.py", line 554, in serve
    trigger_events(before_start, loop)
  File "/home/my-project/env/lib/python3.7/site-packages/sanic/server.py", line 354, in trigger_events
    loop.run_until_complete(result)
  File "uvloop/loop.pyx", line 1494, in uvloop.loop.Loop.run_until_complete
  File "/home/my-project/env/lib/python3.7/site-packages/sanic/app.py", line 1280, in finalize
    app.router.finalize()
  File "/home/my-project/env/lib/python3.7/site-packages/sanic/router.py", line 179, in finalize
    super().finalize(*args, **kwargs)
  File "/home/my-project/env/lib/python3.7/site-packages/sanic_routing/router.py", line 185, in finalize
    self._render(do_compile)
  File "/home/my-project/env/lib/python3.7/site-packages/sanic_routing/router.py", line 263, in _render
    "exec",
  File "<string>", line 34
    basket['__params__']['first'] = str(basket[0])
         ^
IndentationError: expected an indented block

ERROR
When I make a local modification to router.py to print(self.find_route_src), it reveals this:

def find_route(path, router, basket, extra):
    parts = tuple(path[1:].split(router.delimiter))
    try:
        route = router.static_routes[parts]
        basket['__raw_path__'] = path
        return route, basket
    except KeyError:
        pass
    num = len(parts)
    if num > 0:
        basket[0] = parts[0]
        if num > 1:
            basket[1] = parts[1]
            if num == 3:
                basket[2] = parts[2]
                try:
                    basket['__params__']['first'] = str(basket[0])
                    basket['__params__']['second'] = str(basket[1])
                    basket['__params__']['third'] = str(basket[2])
                except ValueError:
                    ...
                else:
                    basket['__raw_path__'] = '<first>/<second>/<third>'
                    return router.dynamic_routes[('<first>', '<second>', '<third>')], basket
    try:
        basket['__params__']['first'] = str(basket[0])
        basket['__params__']['second'] = str(basket[1])
    except ValueError:
        ...
    else:
        basket['__raw_path__'] = '<first>/<second>'
        return router.dynamic_routes[('<first>', '<second>')], basket
try:
basket['__params__']['first'] = str(basket[0])
except ValueError:
...
else:
basket['__raw_path__'] = '<first>'
return router.dynamic_routes[('<first>',)], basket

Alpha type

Because alpha types are no longer using regex, they are acting the same as string. We need to add a new constructor to be used inside the router in place of str.

API for adding patterns

Currently .patterns supports a fixed set of patterns using REGEX_TYPES.

This should be expanded so that custom patterns can be added at run time.

router.register_pattern("ipv4", ipaddress.ip_address, r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$")

Then, it should be usable:

@app.route("/foo/<ip:ipv4>")
async def handler(request, ip: IPv4Address):
    ....

Requirements conditional check uses wrong index

Currently, this produces a syntax error:

app.route("/<foo>/", strict_slashes=True)(lambda _: text("..."))
app.route("/<foo>/", strict_slashes=True, host="foo.com")(
    lambda _: text("...")
)
    compiled_src = compile(
  File "", line 8
    elif extra == {'host': 'foo.com'}:
    ^
SyntaxError: invalid syntax

This is because when deciding the type of conditional, it is looking at the wrong idx.

# tree.py
    def _inject_requirements(self, location, indent):
        for idx, reqs in self.route.requirements.items():
            conditional = "if" if idx == 0 else "elif"

To solve:

    def _inject_requirements(self, location, indent):
        for k, (idx, reqs) in enumerate(self.route.requirements.items()):
            conditional = "if" if k == 0 else "elif"

No requirements fallback

When adding routes where there both is and is not a set of requirements ...

app.route("/<foo>")(lambda _, **__: text("..."))
app.route("/<foo>", host="foo.com")(lambda _: text("..."))

The requirements are evaluated first, and therefore a request without fails:

$ curl localhost:9999/ssss/
⚠️ 404 — Not Found
==================
Requested URL /ssss not found

The generated router:

def find_route(path, router, basket, extra):
    parts = tuple(path[1:].split(router.delimiter))
    num = len(parts)
    if num > 0:
        basket[0] = parts[0]
        if extra == {'host': 'foo.com'}:
            basket['__handler_idx__'] = 1
        else:
            raise NotFound
        try:
            basket['__params__']['foo'] = str(basket[0])
        except ValueError:
            ...
        else:
            basket['__raw_path__'] = '<foo>'
            return router.dynamic_routes[('<foo>',)], basket
    raise NotFound

The fix for this is to add a check when injecting the requirements section to see if there is a fallback. If so, then the solution is to not raise NotFound in the else block after the extra evaluation. That exception should only be raised if extra is required, and all have been exhausted.

def find_route(path, router, basket, extra):
    parts = tuple(path[1:].split(router.delimiter))
    num = len(parts)
    if num > 0:
        basket[0] = parts[0]
        if extra == {'host': 'foo.com'}:
            basket['__handler_idx__'] = 1

        try:
            basket['__params__']['foo'] = str(basket[0])
        except ValueError:
            ...
        else:
            basket['__raw_path__'] = '<foo>'
            return router.dynamic_routes[('<foo>',)], basket
    raise NotFound

Parameterized Route returns 404 instead of 405

Observation / Problem

When a route is parametrized (e.g. /<identifier> or /<identifier:int>) and an HTTP method is used that is not defined, the response comes back as a 404 Not Found instead of 405 Method Not Allowed.

Simple routes w/o parameterization properly return a 405.

Test Setup

from sanic import Sanic, Blueprint
from sanic.response import HTTPResponse
from sanic.views import HTTPMethodView

app = Sanic(name='test')


@app.get(uri='/with-func', name='get-func')
def handler_str(request):
    print(None)
    return HTTPResponse()


@app.get(uri='/with-func/str/<identifier>', name='str-id-get-func')
def handler_str(request, identifier: str):
    print(identifier, type(identifier))
    return HTTPResponse()


@app.get(uri='/with-func/int/<identifier:int>', name='int-id-get-func')
def handler_int(request, identifier: int):
    print(identifier, type(identifier))
    return HTTPResponse()


class StrView(HTTPMethodView, attach=app, uri='/with-view', name='get-view'):
    @staticmethod
    def get(request):
        print(None)
        return HTTPResponse()


class StrView(HTTPMethodView, attach=app, uri='/with-view/str/<identifier>', name='str-id-get-view'):
    @staticmethod
    def get(request, identifier):
        print(identifier, type(identifier))
        return HTTPResponse()


class IntView(HTTPMethodView, attach=app, uri='/with-view/int/<identifier:int>', name='int-id-get-view'):
    @staticmethod
    def get(request, identifier: int):
        print(identifier, type(identifier))
        return HTTPResponse()


app.run(host='localhost', port=8080, single_process=True)

Test Execution

from httpx import get, post

paths = [
    '/with-func',
    '/with-func/str/test',
    '/with-func/int/1',
    '/with-view',
    '/with-view/str/test',
    '/with-view/int/1'
]

for path in paths:
    print(path)

    for method in [get, post]:
        response = method(f'http://localhost:8080{path}')
        print(f'\t{method.__name__:8}{response}')

App Logs

[2023-11-02 21:35:08 +0000] [32026] [INFO] Sanic v23.6.0
[2023-11-02 21:35:08 +0000] [32026] [INFO] Goin' Fast @ http://localhost:8080
[2023-11-02 21:35:08 +0000] [32026] [INFO] mode: production, single worker
[2023-11-02 21:35:08 +0000] [32026] [INFO] server: sanic, HTTP/1.1
[2023-11-02 21:35:08 +0000] [32026] [INFO] python: 3.11.6
[2023-11-02 21:35:08 +0000] [32026] [INFO] platform: Linux-3.10.0-1160.99.1.el7.x86_64-x86_64-with-glibc2.17
[2023-11-02 21:35:08 +0000] [32026] [INFO] packages: sanic-routing==23.6.0, sanic-testing==23.6.0, sanic-ext==23.6.0
[2023-11-02 21:35:08 +0000] [32026] [INFO] Sanic Extensions:
[2023-11-02 21:35:08 +0000] [32026] [INFO]   > injection [0 dependencies; 0 constants]
[2023-11-02 21:35:08 +0000] [32026] [INFO]   > openapi [http://localhost:8080/docs]
[2023-11-02 21:35:08 +0000] [32026] [INFO]   > http 
[2023-11-02 21:35:08 +0000] [32026] [INFO] Starting worker [32026]
None
test <class 'str'>
1 <class 'int'>
None
test <class 'str'>
1 <class 'int'>

Test Logs

/with-func
	get     <Response [200 OK]>
	post    <Response [405 Method Not Allowed]>
/with-func/str/test
	get     <Response [200 OK]>
	post    <Response [404 Not Found]>
/with-func/int/1
	get     <Response [200 OK]>
	post    <Response [404 Not Found]>
/with-view
	get     <Response [200 OK]>
	post    <Response [405 Method Not Allowed]>
/with-view/str/test
	get     <Response [200 OK]>
	post    <Response [404 Not Found]>
/with-view/int/1
	get     <Response [200 OK]>
	post    <Response [404 Not Found]>

Multiple colons in segment

A route definition might legitimately have two : in it:

/path/to/<file_uuid:[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}(?:\.[A-z]{1,4})?>
Traceback (most recent call last):
  File "/app/run_sanic.py", line 63, in <module>
    run_sanic_forever()
  File "/app/run_sanic.py", line 28, in run_sanic_forever
    server = loop.run_until_complete(serv_task)
  File "uvloop/loop.pyx", line 1494, in uvloop.loop.Loop.run_until_complete
  File "/usr/local/lib/python3.9/site-packages/sanic/app.py", line 1017, in create_server
    await self.trigger_events(
  File "/usr/local/lib/python3.9/site-packages/sanic/app.py", line 1041, in trigger_events
    await result
  File "/usr/local/lib/python3.9/site-packages/sanic/app.py", line 1280, in finalize
    app.router.finalize()
  File "/usr/local/lib/python3.9/site-packages/sanic/router.py", line 179, in finalize
    super().finalize(*args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/sanic_routing/router.py", line 216, in finalize
    self._generate_tree()
  File "/usr/local/lib/python3.9/site-packages/sanic_routing/router.py", line 238, in _generate_tree
    self.tree.finalize()
  File "/usr/local/lib/python3.9/site-packages/sanic_routing/tree.py", line 365, in finalize
    self.root.finalize_children()
  File "/usr/local/lib/python3.9/site-packages/sanic_routing/tree.py", line 47, in finalize_children
    child.finalize_children()
  File "/usr/local/lib/python3.9/site-packages/sanic_routing/tree.py", line 47, in finalize_children
    child.finalize_children()
  File "/usr/local/lib/python3.9/site-packages/sanic_routing/tree.py", line 47, in finalize_children
    child.finalize_children()
  File "/usr/local/lib/python3.9/site-packages/sanic_routing/tree.py", line 39, in finalize_children
    k: v for k, v in sorted(self._children.items(), key=self._sorting)
  File "/usr/local/lib/python3.9/site-packages/sanic_routing/tree.py", line 320, in _sorting
    key, param_type = key.split(":")
ValueError: too many values to unpack (expected 2)

Pattern registration auto-compile

app.router.register_pattern(
    "ipv4",
    ipaddress.ip_address,
    IP_ADDRESS_PATTERN,
)

If IP_ADDRESS_PATTERN is a str there is a mypy error that it is looking for a compiled expression. Should add support for accepting both re.compile(...) and strings, and auto compile them at registration.

Add route.uri

This should be:

@property
def uri(self):
    return f"/{self.path}"

Some Sanic Routes fail with 404 Not Found

Some of our routes are not resolved. Return 404 Not Found.
Last tested on sanic-routing v0.6.2 , sanic v21.3.4
Unittest to reproduce:

Line to add to /tests/test_routing.py

def test_failing():

    def handler1():
        return "handler1"

    def handler2():
        return "handler2"

    router = Router()
    router.add("/v1/c", handler1, methods=["GET"])
    router.add("/v1/c/<c_id:int>", handler2, methods=["GET"])
    router.add("/v1/c/<c_id:int>/e", handler1, methods=["GET"])
    router.add("/v1/c/<c_id:int>/e/<e_id:int>", handler2, methods=["GET"])
    router.add("/v1/c/<c_id:int>/f", handler1, methods=["GET"])
    router.add("/v1/c/<c_id:int>/f/<f_id:int>", handler2, methods=["GET"])
    # Comment next line to pass the test
    router.add("/v1/c/<c_id:int>/d", handler1, methods=["GET"])
    router.add("/v1/c/<c_id:int>/d/<d_id:int>", handler2, methods=["GET"])
    router.finalize()


    _, handler, params = router.get("/v1/c", "GET")
    assert handler() == "handler1"
    assert params == {}

    _, handler, params = router.get("/v1/c/123", "GET")
    assert handler() == "handler2"
    assert params == {"c_id": 123}

    _, handler, params = router.get("/v1/c/123/e", "GET")
    assert handler() == "handler1"
    assert params == {"c_id": 123}

    _, handler, params = router.get("/v1/c/123/e/456", "GET")
    assert handler() == "handler2"
    assert params == {"c_id": 123, "e_id": 456}

    _, handler, params = router.get("/v1/c/123/f", "GET")
    assert handler() == "handler1"
    assert params == {"c_id": 123}

    _, handler, params = router.get("/v1/c/123/f/890", "GET")
    assert handler() == "handler2"
    assert params == {"c_id": 123, "f_id": 890}

Seems to have something to do with the auto generated src for compile.

Dynamic node with regex subnode can result in 404

In this example, having a subnode with regex needs to make sure the node level return does not include the num>x.

"/<foo>"
r"/<foo>/<invoice:(?P<invoice>[0-9]+)\.pdf>"
def find_route(path, method, router, basket, extra):
    parts = tuple(path[1:].split(router.delimiter))
    num = len(parts)
    if parts[0]:
        if parts[0] == "v2":
            if num > 1:
                basket[1] = parts[1]
                if num > 3:
                    ...
                elif num == 3:
                    basket[2] = parts[2]
                    ...
                if num > 2:
                    raise NotFound
                try:
                    basket['__params__']['foo'] = str(basket[1])
                except (ValueError, KeyError):
                    pass
                else:
                    if method in frozenset({'OPTIONS'}):
                        route_idx = 0
                    elif method in frozenset({'GET'}):
                        route_idx = 1
                    else:
                        raise NoMethod
                    return router.dynamic_routes[('v2', '<foo>')][route_idx], basket
    match = router.matchers[0].match(path)
    if match:
        basket['__params__'] = match.groupdict()
        if method in frozenset({'OPTIONS'}):
            route_idx = 0
        elif method in frozenset({'GET'}):
            route_idx = 1
        else:
            raise NoMethod
        return router.regex_routes[('v2', '<foo>', '<invoice:(?P<invoice>[0-9]+)\\.pdf>')][route_idx], basket
    raise NotFound
matchers = [
    re.compile(r'^/v2/<foo>/(?P<invoice>[0-9]+)\.pdf$'),
]

When route part zero has type coersion, subroutes match when they should be NotFound.

Test case fails:

def test_use_route_test_bug2():
    router = Router()
    def h1(foo):
        return "first"
    router.add("/<foo:int>", h1)
    def h2(foo):
        return "second"
    router.add("/<foo:int>/bar", h2)

    router.finalize()
    with pytest.raises(NotFound):
        router.get("/foo/aaaa", "BASE")  #<-- This raises NotFound as expected
    with pytest.raises(NotFound):
        router.get("/0/aaaa", "BASE")  #<-- This returns route h1, should raise NotFound.

Generated source:

def find_route(path, router, basket, extra):
    parts = tuple(path[1:].split(router.delimiter))
    num = len(parts)
    if num > 0:
        basket[0] = parts[0]
        if num == 2:
            if parts[1] == "bar":
                try:
                    basket['__params__']['foo'] = int(basket[0])
                except ValueError:
                    ...
                else:
                    basket['__raw_path__'] = '<foo:int>/bar'
                    return router.dynamic_routes[('<foo:int>', 'bar')], basket
        try:
            basket['__params__']['foo'] = int(basket[0])
        except ValueError:
            ...
        else:
            basket['__raw_path__'] = '<foo:int>'
            return router.dynamic_routes[('<foo:int>',)], basket
    raise NotFound

When there are two parts to the route, and parts[1] != "bar", it falls through to the try/except clause for int(basket[0]), succeeds in that, so returns the route match for h1, should raise NotFound.

I don't think this is an indentation issue like #13, perhaps a logic problem?

Routing error after updating to 0.4.3

Hi

We have route in code defined as:

@app.get('/relations/<relation_id>/keys')

Code above worked with sanic 21.3.2 and sanic-router 0.4.2, but with 0.4.3 ends up with error:

Experienced exception while trying to serve
Traceback (most recent call last):
  File "/service/venv/lib/python3.7/site-packages/sanic/app.py", line 918, in run
    serve_single(server_settings)
  File "/service/venv/lib/python3.7/site-packages/sanic/server.py", line 725, in serve_single
    serve(**server_settings)
  File "/service/venv/lib/python3.7/site-packages/sanic/server.py", line 554, in serve
    trigger_events(before_start, loop)
  File "/service/venv/lib/python3.7/site-packages/sanic/server.py", line 354, in trigger_events
    loop.run_until_complete(result)
  File "uvloop/loop.pyx", line 1494, in uvloop.loop.Loop.run_until_complete
  File "/service/venv/lib/python3.7/site-packages/sanic/app.py", line 1280, in finalize
    app.router.finalize()
  File "/service/venv/lib/python3.7/site-packages/sanic/router.py", line 179, in finalize
    super().finalize(*args, **kwargs)
  File "/service/venv/lib/python3.7/site-packages/sanic_routing/router.py", line 185, in finalize
    self._render(do_compile)
  File "/service/venv/lib/python3.7/site-packages/sanic_routing/router.py", line 262, in _render
    "exec",
  File "<string>", line 108
    elif parts[0] == "relations":

Not sure how to resolve this, does somebody have any ideas?

Parameter types are broken when using <something:path>

from sanic import Sanic
from sanic.response import text

app = Sanic("MyHelloWorldApp")

@app.get("/<id:int>/<subpath:path>")
async def hello_world(request, id, subpath):
    return text(str(type(id)))

Expected response is <class 'int'>, but actual response is <class 'str'>.

Same with <id:float> or <id:uuid>.

When using <id:ymd>, it throws an error:

[2023-06-20 20:15:55 +0300] [3355801] [ERROR] Invalid matching pattern ([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))
Traceback (most recent call last):
  File "lib/python3.8/site-packages/sanic/worker/serve.py", line 117, in worker_serve
    return _serve_http_1(
  File "lib/python3.8/site-packages/sanic/server/runners.py", line 224, in _serve_http_1
    loop.run_until_complete(app._startup())
  File "uvloop/loop.pyx", line 1517, in uvloop.loop.Loop.run_until_complete
  File "lib/python3.8/site-packages/sanic/app.py", line 1580, in _startup
    self.finalize()
  File "lib/python3.8/site-packages/sanic/app.py", line 1551, in finalize
    self.router.finalize()
  File "lib/python3.8/site-packages/sanic/router.py", line 202, in finalize
    super().finalize(*args, **kwargs)
  File "lib/python3.8/site-packages/sanic_routing/router.py", line 329, in finalize
    route.finalize()
  File "lib/python3.8/site-packages/sanic_routing/route.py", line 280, in finalize
    self._compile_regex()
  File "lib/python3.8/site-packages/sanic_routing/route.py", line 267, in _compile_regex
    raise InvalidUsage(f"Invalid matching pattern {pattern}")
sanic_routing.exceptions.InvalidUsage: Invalid matching pattern ([12]\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))

Having two or more <path> routes does not work with different methods

If you have two or more @app.route with a similar but different methods will return MethodNotSupported.

Example:

@app.route("/<path:path>", methods=["GET", "OPTIONS"])
async def get_stuff(request, path):
    return.text("This was a GET or OPTIONS request")

@app.route("/<path:path>", methods=["POST"])
async def post_stuff(request, path):
    return.text("This was a POST request")

Error when issuing a GET request: sanic_routing.exceptions.NoMethod: Method 'GET' not found on <Route: name=__main__.cors_204 path=<path:path>>

Dynamic route sorting

Currently it is this:

        return (
            child.dynamic,
            len(child._children),
            key,
            bool(child.group and child.group.regex),
            type_ * -1,
        )

It should be:

        return (
            child.dynamic,
            len(child._children),
            bool(child.group and child.group.regex),
            type_ * -1,
            key,
        )

... so that the variable name is the last tie breaker

[Feature] Fallback route when none match

I'm working on an app that has a pretty unique routing requirement.

I need something that looks like this:

app = Sanic()

@app.get("/login")
async def login(request):
    ...

@app.get("/logout")
async def logout(request):
    ...

@app.get("/metrics")
async def metrics(request):
    ...

@app.route("/<mypath:path>")
async def fallback(request, mypath: str):
    # all other routes go to this handler
    ...

Implementing it as-is does not work because the "/<mypath:path>" matches all routes and overrides the other route handlers.

One thought would be to have the ability to assign a priority weight on the route, like:

@app.get("/login", priority=1)
async def login(request):
    ...

@app.route("/<mypath:path>", priority=9)
async def fallback(request, mypath: str):
    # this handler is evaluated after those with higher priority (lower number)
    ...

That might break some backwards compatibility (not sure), another solution would be to have a new match type, that is always evaluated last in the router (and this kind would never return a NotFound from the router).

@app.route("/<mypath:fallback>")
async def fallback(request, mypath: str):
    # this handler is evaluated after those with higher priority (lower number)
    ...

Does that seem useful? Or have I missed an obvious way to achieve this?

Route.methods nesting artifact

The Route.methods property is generally nested as follows:

{
    "/some/path": set("GET", "POST")
}

However, this level of nesting is not only a breaking change, it is not necessary. It is the result of an earlier iteration of the router that was since abandoned. It seems this artifact has remained, and is no longer necessary.


Can be solved:

    def _finalize_methods(self):
        self.methods = set()
        for handlers in self.handlers.values():
            self.methods.update(set(key.upper() for key in handlers.keys()))

Regex params not type cast

We need to type case param_basket["__params__"] since it is not done inline in the finder like param_basket["__matches__"].

Colon in uri generate response 404

Sanic can't find the route if there's a colon in the uri.

Code snippet

from sanic import Sanic, Request, HTTPResponse

app = Sanic('zzz')


@app.get(f'/abc/x:y')
async def main(_: Request):
    return HTTPResponse()


if __name__ == '__main__':
    app.run()

Expected behavior
GET http://127.0.0.1:8000/abc/x:y 200 0

Actual behavior
GET http://127.0.0.1:8000/abc/x:y 404 733

Environment

[2022-09-26 20:50:40 +0600] [13060] [INFO] Sanic v22.6.2
[2022-09-26 20:50:40 +0600] [13060] [INFO] Goin' Fast @ http://127.0.0.1:8000
[2022-09-26 20:50:40 +0600] [13060] [INFO] mode: production, single worker
[2022-09-26 20:50:40 +0600] [13060] [INFO] server: sanic, HTTP/1.1
[2022-09-26 20:50:40 +0600] [13060] [INFO] python: 3.10.5
[2022-09-26 20:50:40 +0600] [13060] [INFO] platform: Windows-10-10.0.19044-SP0
[2022-09-26 20:50:40 +0600] [13060] [INFO] packages: sanic-routing==22.3.0
[2022-09-26 20:50:40 +0600] [13060] [INFO] Starting worker [13060]

Additional context
tiangolo/fastapi#4892
encode/starlette#1657

Context managed code generation

Re code generation: maybe take that a step forward and use actual inline Python code instead of string literals. Code that is syntactically valid can be loaded even if the variables don't exist, provided that the particular path never gets executed. Annotations or some other hacks could be used to inject parts into it. Well, you get the idea but this is becoming some true black magic by now.

# This is router code
if need_foo:
    with codegen:
        # This is generated code
        if route == "foo":
            return handler_foo

Originally posted by @Tronic in #37 (comment)

`404`s with parameter type `path`

Hi y'all,

we have just upgraded from sanic==20.3.0 to sanic==21.9.1 with sanic-routing==0.7.1.
Our test suite now shows 404s and 405s where we didn't expect them.

We run into this with Python 3.7, 3.8 and 3.9 on Windows and Ubuntu.

Minimal Example

from sanic import Sanic
from sanic.response import text

app = Sanic("MyHelloWorldApp")


@app.get("/conversation/<conversation_id:path>/story")
async def story(request, conversation_id):
    return text("story")

@app.put("/conversation/<conversation_id:path>/tracker/events")
async def put_events(request, conversation_id):
    return text("put events")

@app.post("/conversation/<conversation_id:path>/tracker/events")
async def post_events(request, conversation_id):
    return text("post events")

How to reproduce
If you do a curl http://127.0.0.1:8000/conversation/dasda-dasd/story, this will result in a 404 (instead of the expected "story") If you comment out put_events or post_events it works fine.

I believe this is somewhat related to sanic-org/sanic#2130

Appreciate any help as this is currently blocking us to upgrade 🙌🏻

Add slug type

Example:

/path/to/<slug:slug>

Perhaps something like this:

"slug": (str, re.compile(r"^[\w-]+")),

Incorrect handling of `pattern` argument for `register_pattern`

The type annotation on register_pattern gives Pattern as the acceptable type.

However, an exception is thrown if you do provide a pattern with re.compile, complaining that it must be a str.

So:

app.router.register_pattern(
    "nestr",
    nonempty_str,
    re.compile(r"^[^/]+$")
)

passes the type checker, but fails with an exception, while

app.router.register_pattern(
    "nestr",
    nonempty_str,
    r"^[^/]+$"
)

does not raise an exception, but highlights the pattern argument as a mismatching type.

The relevant lines are:

https://github.com/sanic-org/sanic-routing/blob/main/sanic_routing/router.py#L241-L243

https://github.com/sanic-org/sanic-routing/blob/main/sanic_routing/router.py#L272-L276

parts_to_path, raises exception for part with multiple parameters without type hint

import sanic_routing.utils as sru
sru.path_to_parts('/asd/<int1>,<int2>')
('asd', '<int1>,<int2>')
sru.parts_to_path(sru.path_to_parts('/asd/<int1>,<int2>'))
Traceback (most recent call last):
  File "/srv/git/sensors-server/venv/lib/python3.9/site-packages/sanic_routing/utils.py", line 75, in parts_to_path
    if match.group(2):
AttributeError: 'NoneType' object has no attribute 'group'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "/srv/git/sensors-server/venv/lib/python3.9/site-packages/sanic_routing/utils.py", line 83, in parts_to_path
    if match.group(2):
AttributeError: 'NoneType' object has no attribute 'group'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "/usr/lib/python3.9/code.py", line 90, in runcode
    exec(code, self.locals)
  File "<input>", line 1, in <module>
  File "/srv/git/sensors-server/venv/lib/python3.9/site-packages/sanic_routing/utils.py", line 93, in parts_to_path
    raise InvalidUsage(f"Invalid declaration: {part}")
sanic_routing.exceptions.InvalidUsage: Invalid declaration: <int1>,<int2>

Introduce RouteGroup

Currently, a route is defined as defined path to reach the endpoint. Multiple methods whether in a single definition, or multiple will result in a single route.

We should add a RouteGroup that has a near identical API to the Route.

In practical example, it means that this would yield a single route:

router.add("/path", methods=("foo", "bar"))

But this would yield two:

router.add("/path", methods=("foo",))
router.add("/path", methods=("bar"))

However, they should be joined into a single RouteGroup that the router does not distinguish between when generating the source or resolving a path.

[Bug] Routes which are overlapping and have different depth are not found

Describe the bug
Routes which are overlapping and have different depth are not found.

For example, if you describe your paths as :
/foo/<foo_id>/bars_ids
/foo/<foo_id>/bars_ids/<bar_id>/settings

you will get not found error while requesting /foo/123/bars_ids.

This cause is hidden in optimize() function, when Line("...", 0, offset=-1, render=False)) are rendered,
offset is not applied to structures which have same indentation but are situated below.

Code snippet

def find_route(path, router, basket, extra):
    *omitted_part*
                    if parts[2] == "bars_ids":
                        if num > 3:
                            basket[3] = parts[3]
                            if num == 5:
                                if parts[4] == "settings":
                                    .....
                              **this block should have same identation as if num>3**           
                            try:
                                basket['__params__']['foo_id'] = str(basket[1])

Expected behavior
Both routes should be accessed as expected

Environment :

  • OS: MacOS 10.15.2 (19C57)
  • python: Python v3.7.1:260ec2c36a
  • sanic-router== 0.4.1

PR: #13

Missing LICENSE file

Hi,

I'm adding sanic-routing to conda-forge in order to get sanic to build again:

conda-forge/sanic-feedstock#34

However neither the repository nor the package contain a LICENSE file, which is kind of a soft-requirement to build packages on conda-forge.

It would be great to add it to the repository and to the package in order to make the package on conda-forge more compliant. 👍

conda-forge/staged-recipes#14753

Compiler SyntaxError

It would be helpful to add a catch for SyntaxError in the compiler to output the source. Something like this, except abstracted so that line numbers and source can be displayed in other uses as well:

            try:
                compiled_src = compile(
                    self.find_route_src,
                    "",
                    "exec",
                )
            except SyntaxError as e:
                lines = self.find_route_src.split("\n")
                pad = len(str(len(lines)))
                logger.error(
                    "\n".join(
                        f"{str(idx+1).rjust(pad)}: {line}"
                        for idx, line in enumerate(lines)
                    )
                )
                raise e

Deprecate string

The string parameter type is somewhat confusing since we also have int. Often people new to Sanic will think it should be str.

I propose that it becomes deprecated, we add it as an alias to str for some period of time, and then remove it prior to the 21.12 Sanic LTS.

@sanic-org/framework thoughts?

Websocket conflicts with http GET

I find it a bit surprising that a websocket request to Sanic ends up in an app.get handler (at least with the http1 server - didn't try asgi), as to my knowledge Sanic does not provide means to handle such requests in any meaningful way (you could respond 101 perhaps but then what?), and if you simply ignore the header, the connection will fail but your handler runs for nothing.

Also, it is not possible to serve GET and websocket (GET+Upgrade) at the same path like so:

@app.get("/")
def index(req):
    ...

@app.websocket("/")
def websocket(req, ws):
    ...

Would it be possible to change the routing such that upgrade: websocket requests are considered a different method than GET, or alternatively make the scheme also a part of routing (ws, wss, http and https each being different routing-wise)?

On the front side the same path as the document is convenient:

// Same path (easy peasy)
const ws = new WebSocket(location.href.replace(/^http/, 'ws'))

// Different path
const ws_path = '/ws'  // sometimes hard to know in front code
const ws = new WebSocket(new URL(ws_path, location.href.replace(/^http/, 'ws')))

Requirements based on equality

Hello,

Currently, requirements are based entirely on matching the hash of the extra.
Is there any way to actually have this run an equality check, dict to dict, to do more advanced checks ?
It would most certainly impact performances, i'm just wondering if it's doable with the current code, somehow ?

thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.