GIT-1591 Strict Slashes behavior fix (#1594)
* fix: GIT-1591: fix strict_slashes option inheriting behavior Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * doc: GIT-1591: add documentation exlaining the strict_slashes behavior Signed-off-by: Harsha Narayana <harsha2k4@gmail.com> * fix: GIT-1591: fix deprecated for test_client Signed-off-by: Harsha Narayana <harsha2k4@gmail.com>
This commit is contained in:
parent
c15158224b
commit
13079c6e30
|
@ -241,6 +241,45 @@ def handler(request):
|
||||||
app.blueprint(bp)
|
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
|
## User defined route name
|
||||||
|
|
||||||
A custom route name can be used by passing a `name` argument while registering the route which will
|
A custom route name can be used by passing a `name` argument while registering the route which will
|
||||||
|
|
|
@ -37,7 +37,7 @@ class Blueprint:
|
||||||
url_prefix=None,
|
url_prefix=None,
|
||||||
host=None,
|
host=None,
|
||||||
version=None,
|
version=None,
|
||||||
strict_slashes=False,
|
strict_slashes=None,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
In *Sanic* terminology, a **Blueprint** is a logical collection of
|
In *Sanic* terminology, a **Blueprint** is a logical collection of
|
||||||
|
|
|
@ -687,3 +687,49 @@ def test_register_blueprint(app, debug):
|
||||||
"version 1.0. Please use the blueprint method"
|
"version 1.0. Please use the blueprint method"
|
||||||
" instead"
|
" 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
|
||||||
|
|
|
@ -24,7 +24,9 @@ old_conn = None
|
||||||
class ReusableSanicConnectionPool(httpcore.ConnectionPool):
|
class ReusableSanicConnectionPool(httpcore.ConnectionPool):
|
||||||
async def acquire_connection(self, origin):
|
async def acquire_connection(self, origin):
|
||||||
global old_conn
|
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:
|
if connection is None:
|
||||||
connection = self.keepalive_connections.pop_by_origin(origin)
|
connection = self.keepalive_connections.pop_by_origin(origin)
|
||||||
|
|
||||||
|
@ -187,11 +189,7 @@ class ReuseableSanicTestClient(SanicTestClient):
|
||||||
self._session = ResusableSanicSession()
|
self._session = ResusableSanicSession()
|
||||||
try:
|
try:
|
||||||
response = await getattr(self._session, method.lower())(
|
response = await getattr(self._session, method.lower())(
|
||||||
url,
|
url, verify=False, timeout=request_keepalive, *args, **kwargs
|
||||||
verify=False,
|
|
||||||
timeout=request_keepalive,
|
|
||||||
*args,
|
|
||||||
**kwargs,
|
|
||||||
)
|
)
|
||||||
except NameError:
|
except NameError:
|
||||||
raise Exception(response.status_code)
|
raise Exception(response.status_code)
|
||||||
|
|
|
@ -110,7 +110,7 @@ def test_redirect_with_header_injection(redirect_app):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("test_str", ["sanic-test", "sanictest", "sanic test"])
|
@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>/")
|
@app.route("/api/v1/test/<test>/")
|
||||||
async def init_handler(request, test):
|
async def init_handler(request, test):
|
||||||
assert test == test_str
|
assert test == test_str
|
||||||
|
@ -121,7 +121,7 @@ async def test_redirect_with_params(app, test_client, test_str):
|
||||||
assert test == test_str
|
assert test == test_str
|
||||||
return text("OK")
|
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)))
|
response = await test_cli.get("/api/v1/test/{}/".format(quote(test_str)))
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
|
@ -4,7 +4,7 @@ import contextlib
|
||||||
from sanic.response import stream, text
|
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.still_serving_cancelled_request = False
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
|
@ -14,7 +14,7 @@ async def test_request_cancel_when_connection_lost(loop, app, test_client):
|
||||||
app.still_serving_cancelled_request = True
|
app.still_serving_cancelled_request = True
|
||||||
return text("OK")
|
return text("OK")
|
||||||
|
|
||||||
test_cli = await test_client(app)
|
test_cli = await sanic_client(app)
|
||||||
|
|
||||||
# schedule client call
|
# schedule client call
|
||||||
task = loop.create_task(test_cli.get("/"))
|
task = loop.create_task(test_cli.get("/"))
|
||||||
|
@ -33,7 +33,7 @@ async def test_request_cancel_when_connection_lost(loop, app, test_client):
|
||||||
assert app.still_serving_cancelled_request is False
|
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.still_serving_cancelled_request = False
|
||||||
|
|
||||||
@app.post("/post/<id>", stream=True)
|
@app.post("/post/<id>", stream=True)
|
||||||
|
@ -53,7 +53,7 @@ async def test_stream_request_cancel_when_conn_lost(loop, app, test_client):
|
||||||
|
|
||||||
return stream(streaming)
|
return stream(streaming)
|
||||||
|
|
||||||
test_cli = await test_client(app)
|
test_cli = await sanic_client(app)
|
||||||
|
|
||||||
# schedule client call
|
# schedule client call
|
||||||
task = loop.create_task(test_cli.post("/post/1"))
|
task = loop.create_task(test_cli.post("/post/1"))
|
||||||
|
|
|
@ -111,7 +111,6 @@ def test_request_stream_app(app):
|
||||||
result += body.decode("utf-8")
|
result += body.decode("utf-8")
|
||||||
return text(result)
|
return text(result)
|
||||||
|
|
||||||
|
|
||||||
assert app.is_request_stream is True
|
assert app.is_request_stream is True
|
||||||
|
|
||||||
request, response = app.test_client.get("/get")
|
request, response = app.test_client.get("/get")
|
||||||
|
|
|
@ -13,15 +13,12 @@ class DelayableSanicConnectionPool(httpcore.ConnectionPool):
|
||||||
self._request_delay = request_delay
|
self._request_delay = request_delay
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
async def send(
|
async def send(self, request, stream=False, ssl=None, timeout=None):
|
||||||
self,
|
|
||||||
request,
|
|
||||||
stream=False,
|
|
||||||
ssl=None,
|
|
||||||
timeout=None,
|
|
||||||
):
|
|
||||||
connection = await self.acquire_connection(request.url.origin)
|
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)
|
await connection.connect(ssl=ssl, timeout=timeout)
|
||||||
if self._request_delay:
|
if self._request_delay:
|
||||||
await asyncio.sleep(self._request_delay)
|
await asyncio.sleep(self._request_delay)
|
||||||
|
|
|
@ -231,9 +231,7 @@ def test_chunked_streaming_returns_correct_content(streaming_app):
|
||||||
assert response.text == "foo,bar"
|
assert response.text == "foo,bar"
|
||||||
|
|
||||||
|
|
||||||
def test_non_chunked_streaming_adds_correct_headers(
|
def test_non_chunked_streaming_adds_correct_headers(non_chunked_streaming_app):
|
||||||
non_chunked_streaming_app
|
|
||||||
):
|
|
||||||
request, response = non_chunked_streaming_app.test_client.get("/")
|
request, response = non_chunked_streaming_app.test_client.get("/")
|
||||||
assert "Transfer-Encoding" not in response.headers
|
assert "Transfer-Encoding" not in response.headers
|
||||||
assert response.headers["Content-Type"] == "text/csv"
|
assert response.headers["Content-Type"] == "text/csv"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user