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-1591 Strict Slashes behavior fix #1594

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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/sanic/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,45 @@ def handler(request):
app.blueprint(bp)
```

The behavior of how the `strict_slashes` flag follows a defined hierarchy which decides if a specific route
falls under the `strict_slashes` behavior.

```bash
|___ Route
|___ Blueprint
|___ Application
```

Above hierarchy defines how the `strict_slashes` flag will behave. The first non `None` value of the `strict_slashes`
found in the above order will be applied to the route in question.

```python
from sanic import Sanic, Blueprint
from sanic.response import text

app = Sanic("sample_strict_slashes", strict_slashes=True)

@app.get("/r1")
def r1(request):
return text("strict_slashes is applicable from App level")

@app.get("/r2", strict_slashes=False)
def r2(request):
return text("strict_slashes is not applicable due to False value set in route level")

bp = Blueprint("bp", strict_slashes=False)

@bp.get("/r3", strict_slashes=True)
def r3(request):
return text("strict_slashes applicable from blueprint route level")

bp1 = Blueprint("bp1", strict_slashes=True)

@bp.get("/r4")
def r3(request):
return text("strict_slashes applicable from blueprint level")
```

## User defined route name

A custom route name can be used by passing a `name` argument while registering the route which will
Expand Down
2 changes: 1 addition & 1 deletion sanic/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def __init__(
url_prefix=None,
host=None,
version=None,
strict_slashes=False,
strict_slashes=None,
):
"""
In *Sanic* terminology, a **Blueprint** is a logical collection of
Expand Down
46 changes: 46 additions & 0 deletions tests/test_blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,3 +687,49 @@ def test_register_blueprint(app, debug):
"version 1.0. Please use the blueprint method"
" instead"
)


def test_strict_slashes_behavior_adoption(app):
app.strict_slashes = True

@app.get("/test")
def handler_test(request):
return text("Test")

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

bp = Blueprint("bp")

@bp.get("/one", strict_slashes=False)
def one(request):
return text("one")

@bp.get("/second")
def second(request):
return text("second")

app.blueprint(bp)

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

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

bp2 = Blueprint("bp2", strict_slashes=False)

@bp2.get("/third")
def third(request):
return text("third")

app.blueprint(bp2)
assert app.test_client.get("/third")[1].status == 200
assert app.test_client.get("/third/")[1].status == 200

@app.get("/f1", strict_slashes=False)
def f1(request):
return text("f1")

assert app.test_client.get("/f1")[1].status == 200
assert app.test_client.get("/f1/")[1].status == 200
10 changes: 4 additions & 6 deletions tests/test_keep_alive_timeout.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
class ReusableSanicConnectionPool(httpcore.ConnectionPool):
async def acquire_connection(self, origin):
global old_conn
connection = self.active_connections.pop_by_origin(origin, http2_only=True)
connection = self.active_connections.pop_by_origin(
origin, http2_only=True
)
if connection is None:
connection = self.keepalive_connections.pop_by_origin(origin)

Expand Down Expand Up @@ -187,11 +189,7 @@ async def _local_request(self, method, url, *args, **kwargs):
self._session = ResusableSanicSession()
try:
response = await getattr(self._session, method.lower())(
url,
verify=False,
timeout=request_keepalive,
*args,
**kwargs,
url, verify=False, timeout=request_keepalive, *args, **kwargs
)
except NameError:
raise Exception(response.status_code)
Expand Down
4 changes: 2 additions & 2 deletions tests/test_redirect.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def test_redirect_with_header_injection(redirect_app):


@pytest.mark.parametrize("test_str", ["sanic-test", "sanictest", "sanic test"])
async def test_redirect_with_params(app, test_client, test_str):
async def test_redirect_with_params(app, sanic_client, test_str):
@app.route("/api/v1/test/<test>/")
async def init_handler(request, test):
assert test == test_str
Expand All @@ -121,7 +121,7 @@ async def target_handler(request, test):
assert test == test_str
return text("OK")

test_cli = await test_client(app)
test_cli = await sanic_client(app)

response = await test_cli.get("/api/v1/test/{}/".format(quote(test_str)))
assert response.status == 200
Expand Down
8 changes: 4 additions & 4 deletions tests/test_request_cancel.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from sanic.response import stream, text


async def test_request_cancel_when_connection_lost(loop, app, test_client):
async def test_request_cancel_when_connection_lost(loop, app, sanic_client):
app.still_serving_cancelled_request = False

@app.get("/")
Expand All @@ -14,7 +14,7 @@ async def handler(request):
app.still_serving_cancelled_request = True
return text("OK")

test_cli = await test_client(app)
test_cli = await sanic_client(app)

# schedule client call
task = loop.create_task(test_cli.get("/"))
Expand All @@ -33,7 +33,7 @@ async def handler(request):
assert app.still_serving_cancelled_request is False


async def test_stream_request_cancel_when_conn_lost(loop, app, test_client):
async def test_stream_request_cancel_when_conn_lost(loop, app, sanic_client):
app.still_serving_cancelled_request = False

@app.post("/post/<id>", stream=True)
Expand All @@ -53,7 +53,7 @@ async def streaming(response):

return stream(streaming)

test_cli = await test_client(app)
test_cli = await sanic_client(app)

# schedule client call
task = loop.create_task(test_cli.post("/post/1"))
Expand Down
1 change: 0 additions & 1 deletion tests/test_request_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ async def patch(request):
result += body.decode("utf-8")
return text(result)


assert app.is_request_stream is True

request, response = app.test_client.get("/get")
Expand Down
13 changes: 5 additions & 8 deletions tests/test_request_timeout.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,12 @@ def __init__(self, request_delay=None, *args, **kwargs):
self._request_delay = request_delay
super().__init__(*args, **kwargs)

async def send(
self,
request,
stream=False,
ssl=None,
timeout=None,
):
async def send(self, request, stream=False, ssl=None, timeout=None):
connection = await self.acquire_connection(request.url.origin)
if connection.h11_connection is None and connection.h2_connection is None:
if (
connection.h11_connection is None
and connection.h2_connection is None
):
await connection.connect(ssl=ssl, timeout=timeout)
if self._request_delay:
await asyncio.sleep(self._request_delay)
Expand Down
4 changes: 1 addition & 3 deletions tests/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,7 @@ def test_chunked_streaming_returns_correct_content(streaming_app):
assert response.text == "foo,bar"


def test_non_chunked_streaming_adds_correct_headers(
non_chunked_streaming_app
):
def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
request, response = non_chunked_streaming_app.test_client.get("/")
assert "Transfer-Encoding" not in response.headers
assert response.headers["Content-Type"] == "text/csv"
Expand Down