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

GIT-2045: enable versioning and strict slash on BlueprintGroup #2047

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
52 changes: 47 additions & 5 deletions sanic/blueprint_group.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from collections.abc import MutableSequence
from typing import List
from typing import List, Union

import sanic


class BlueprintGroup(MutableSequence):
Expand All @@ -16,6 +18,11 @@ class as a list/tuple inside the existing implementation.
bp1 = Blueprint('bp1', url_prefix='/bp1')
bp2 = Blueprint('bp2', url_prefix='/bp2')

bp3 = Blueprint('bp3', url_prefix='/bp4')
bp3 = Blueprint('bp3', url_prefix='/bp4')

bpg = BlueprintGroup(bp3, bp4, url_prefix="/api", version="v1")

@bp1.middleware('request')
async def bp1_only_middleware(request):
print('applied on Blueprint : bp1 Only')
Expand All @@ -28,6 +35,14 @@ async def bp1_route(request):
async def bp2_route(request, param):
return text(param)

@bp3.route('/')
async def bp1_route(request):
return text('bp1')

@bp4.route('/<param>')
async def bp2_route(request, param):
return text(param)

group = Blueprint.group(bp1, bp2)

@group.middleware('request')
Expand All @@ -36,18 +51,23 @@ async def group_middleware(request):

# Register Blueprint group under the app
app.blueprint(group)
app.blueprint(bpg)
"""

__slots__ = ("_blueprints", "_url_prefix")
__slots__ = ("_blueprints", "_url_prefix", "_version", "_strict_slashes")

def __init__(self, url_prefix=None):
def __init__(self, url_prefix=None, version=None, strict_slashes=None):
"""
Create a new Blueprint Group

:param url_prefix: URL: to be prefixed before all the Blueprint Prefix
:param version: API Version for the blueprint group. This will be inherited by each of the Blueprint
:param strict_slashes: URL Strict slash behavior indicator
"""
self._blueprints = []
self._url_prefix = url_prefix
self._version = version
self._strict_slashes = strict_slashes

@property
def url_prefix(self) -> str:
Expand All @@ -59,14 +79,33 @@ def url_prefix(self) -> str:
return self._url_prefix

@property
def blueprints(self) -> List:
def blueprints(self) -> List["sanic.Blueprint"]:
"""
Retrieve a list of all the available blueprints under this group.

:return: List of Blueprint instance
"""
return self._blueprints

@property
def version(self) -> Union[None, str, int]:
harshanarayana marked this conversation as resolved.
Show resolved Hide resolved
"""
API Version for the Blueprint Group. This will be applied only in case if the Blueprint doesn't already have
a version specified

:return: Version information
"""
return self._version

@property
def strict_slashes(self) -> Union[None, bool]:
"""
URL Slash termination behavior configuration

:return: bool
"""
return self._strict_slashes

def __iter__(self):
"""
Tun the class Blueprint Group into an Iterable item
Expand Down Expand Up @@ -121,7 +160,7 @@ def __len__(self) -> int:
"""
return len(self._blueprints)

def insert(self, index: int, item: object) -> None:
def insert(self, index: int, item: "sanic.Blueprint") -> None:
"""
The Abstract class `MutableSequence` leverages this insert method to
perform the `BlueprintGroup.append` operation.
Expand All @@ -130,6 +169,9 @@ def insert(self, index: int, item: object) -> None:
:param item: New `Blueprint` object.
:return: None
"""
for _attr in ["version", "strict_slashes"]:
if getattr(item, _attr) is None:
setattr(item, _attr, getattr(self, _attr))
self._blueprints.insert(index, item)

def middleware(self, *args, **kwargs):
Expand Down
18 changes: 14 additions & 4 deletions sanic/blueprints.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from collections import defaultdict
from typing import Dict, List, Optional
from typing import Dict, List, Optional, Iterable

from sanic_routing.route import Route # type: ignore

Expand Down Expand Up @@ -87,16 +87,18 @@ def exception(self, *args, **kwargs):
return super().exception(*args, **kwargs)

@staticmethod
def group(*blueprints, url_prefix=""):
def group(*blueprints, url_prefix="", version=None, strict_slashes=None):
"""
Create a list of blueprints, optionally grouping them under a
general URL prefix.

:param blueprints: blueprints to be registered as a group
:param url_prefix: URL route to be prepended to all sub-prefixes
:param version: API Version to be used for Blueprint group
:param strict_slashes: Indicate strict slash termination behavior for URL
"""

def chain(nested):
def chain(nested) -> Iterable[Blueprint]:
"""itertools.chain() but leaves strings untouched"""
for i in nested:
if isinstance(i, (list, tuple)):
Expand All @@ -106,10 +108,18 @@ def chain(nested):
else:
yield i

bps = BlueprintGroup(url_prefix=url_prefix)
bps = BlueprintGroup(
url_prefix=url_prefix,
version=version,
strict_slashes=strict_slashes,
)
for bp in chain(blueprints):
if bp.url_prefix is None:
bp.url_prefix = ""
if bp.version is None and version:
bp.version = version
if bp.strict_slashes is None:
bp.strict_slashes = strict_slashes
harshanarayana marked this conversation as resolved.
Show resolved Hide resolved
bp.url_prefix = url_prefix + bp.url_prefix
bps.append(bp)
return bps
Expand Down
85 changes: 46 additions & 39 deletions sanic/models/futures.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,46 @@
from collections import namedtuple


FutureRoute = namedtuple(
"FutureRoute",
[
"handler",
"uri",
"methods",
"host",
"strict_slashes",
"stream",
"version",
"name",
"ignore_body",
"websocket",
"subprotocols",
"unquote",
"static",
],
)
FutureListener = namedtuple("FutureListener", ["listener", "event"])
FutureMiddleware = namedtuple("FutureMiddleware", ["middleware", "attach_to"])
FutureException = namedtuple("FutureException", ["handler", "exceptions"])
FutureStatic = namedtuple(
"FutureStatic",
[
"uri",
"file_or_directory",
"pattern",
"use_modified_since",
"use_content_range",
"stream_large_files",
"name",
"host",
"strict_slashes",
"content_type",
],
)
from pathlib import PurePath
from typing import NamedTuple, List, Union, Callable

harshanarayana marked this conversation as resolved.
Show resolved Hide resolved

class FutureRoute(NamedTuple):
handler: str
uri: str
methods: Union[None, List[str]]
harshanarayana marked this conversation as resolved.
Show resolved Hide resolved
host: str
strict_slashes: bool
stream: bool
version: Union[None, int]
name: str
ignore_body: bool
websocket: bool
subprotocols: Union[None, List[str]]
unquote: bool
static: bool


class FutureListener(NamedTuple):
listener: Callable
harshanarayana marked this conversation as resolved.
Show resolved Hide resolved
event: str


class FutureMiddleware(NamedTuple):
middleware: Callable
harshanarayana marked this conversation as resolved.
Show resolved Hide resolved
attach_to: str


class FutureException(NamedTuple):
handler: Callable
harshanarayana marked this conversation as resolved.
Show resolved Hide resolved
exceptions: List[Exception]
harshanarayana marked this conversation as resolved.
Show resolved Hide resolved


class FutureStatic(NamedTuple):
uri: str
file_or_directory: Union[str, bytes, PurePath]
pattern: str
use_modified_since: bool
use_content_range: bool
stream_large_files: bool
name: str
host: Union[None, str]
strict_slashes: Union[None, bool]
content_type: Union[None, bool]
4 changes: 1 addition & 3 deletions sanic/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,7 @@ async def accept(self) -> None:
await self._send(
{
"type": "websocket.accept",
"subprotocol": ",".join(
list(self.subprotocols)
),
"subprotocol": ",".join(list(self.subprotocols)),
}
)

Expand Down
61 changes: 61 additions & 0 deletions tests/test_blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -893,3 +893,64 @@ def third(request):

assert app.test_client.get("/f1")[1].status == 200
assert app.test_client.get("/f1/")[1].status == 200


def test_blueprint_group_versioning_and_strict_slash():
harshanarayana marked this conversation as resolved.
Show resolved Hide resolved
app = Sanic(name="blueprint-group-test")

bp1 = Blueprint(name="bp1", url_prefix="/bp1")
bp2 = Blueprint(name="bp2", url_prefix="/bp2", version=2)

bp3 = Blueprint(name="bp3", url_prefix="/bp3")
bp4 = Blueprint(name="bp4", url_prefix=None)

bp5 = Blueprint(name="bp5", version=3, url_prefix="/bp5")

@bp5.get("/r1")
async def bp5_r1(request):
return json({"from": "bp5/r1"})

@bp4.post("/r1")
async def bp4_r1(request):
return json({"from": "bp4/r1"})

@bp3.get("/r1")
async def bp3_r1(request):
return json({"from": "bp3/r1"})

@bp1.get("/pre-group")
async def pre_group(request):
return json({"from": "bp1/pre-group"})

group = Blueprint.group([bp1, bp2], url_prefix="/group1", version=1)

group2 = Blueprint.group([bp3, bp4], strict_slashes=True)

@bp1.get("/r1")
async def r1(request):
return json({"from": "bp1/r1"})

@bp2.get("/r2")
async def r2(request):
return json({"from": "bp2/r2"})

@bp2.get("/r3", version=3)
async def r3(request):
return json({"from": "bp2/r3"})

group2.insert(0, bp5)
app.blueprint([group, group2])

print(f"{app.router.routes_all}")
harshanarayana marked this conversation as resolved.
Show resolved Hide resolved
assert app.test_client.get("/v1/group1/bp1/r1/")[1].status == 200
assert app.test_client.get("/v2/group1/bp2/r2")[1].status == 200
assert app.test_client.get("/v1/group1/bp1/pre-group")[1].status == 200
assert app.test_client.get("/v3/group1/bp2/r3")[1].status == 200
assert app.test_client.get("bp3/r1")[1].status == 200
harshanarayana marked this conversation as resolved.
Show resolved Hide resolved
assert app.test_client.post("/r1/")[1].status == 404
assert app.test_client.post("/r1")[1].status == 200
assert app.test_client.get("/v3/bp5/r1")[1].status == 200
assert app.test_client.get("/v3/bp5/r1/")[1].status == 404

assert group.version == 1
assert group2.strict_slashes is True