Merge branch 'main' into zhiwei/bp-copy
This commit is contained in:
		
							
								
								
									
										13
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -30,6 +30,7 @@ from typing import ( | ||||
|     List, | ||||
|     Optional, | ||||
|     Set, | ||||
|     Tuple, | ||||
|     Type, | ||||
|     Union, | ||||
| ) | ||||
| @@ -411,7 +412,13 @@ class Sanic(BaseSanic): | ||||
|  | ||||
|         self.websocket_enabled = enable | ||||
|  | ||||
|     def blueprint(self, blueprint, **options): | ||||
|     def blueprint( | ||||
|         self, | ||||
|         blueprint: Union[ | ||||
|             Blueprint, List[Blueprint], Tuple[Blueprint], BlueprintGroup | ||||
|         ], | ||||
|         **options: Any, | ||||
|     ): | ||||
|         """Register a blueprint on the application. | ||||
|  | ||||
|         :param blueprint: Blueprint object or (list, tuple) thereof | ||||
| @@ -869,7 +876,7 @@ class Sanic(BaseSanic): | ||||
|         *, | ||||
|         debug: bool = False, | ||||
|         auto_reload: Optional[bool] = None, | ||||
|         ssl: Union[dict, SSLContext, None] = None, | ||||
|         ssl: Union[Dict[str, str], SSLContext, None] = None, | ||||
|         sock: Optional[socket] = None, | ||||
|         workers: int = 1, | ||||
|         protocol: Optional[Type[Protocol]] = None, | ||||
| @@ -999,7 +1006,7 @@ class Sanic(BaseSanic): | ||||
|         port: Optional[int] = None, | ||||
|         *, | ||||
|         debug: bool = False, | ||||
|         ssl: Union[dict, SSLContext, None] = None, | ||||
|         ssl: Union[Dict[str, str], SSLContext, None] = None, | ||||
|         sock: Optional[socket] = None, | ||||
|         protocol: Type[Protocol] = None, | ||||
|         backlog: int = 100, | ||||
|   | ||||
| @@ -181,10 +181,10 @@ class Blueprint(BaseSanic): | ||||
|  | ||||
|     @staticmethod | ||||
|     def group( | ||||
|         *blueprints, | ||||
|         url_prefix="", | ||||
|         version=None, | ||||
|         strict_slashes=None, | ||||
|         *blueprints: Union[Blueprint, BlueprintGroup], | ||||
|         url_prefix: Optional[str] = None, | ||||
|         version: Optional[Union[int, str, float]] = None, | ||||
|         strict_slashes: Optional[bool] = None, | ||||
|         version_prefix: str = "/v", | ||||
|     ): | ||||
|         """ | ||||
|   | ||||
| @@ -31,6 +31,7 @@ class NotFound(SanicException): | ||||
|     """ | ||||
|  | ||||
|     status_code = 404 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class InvalidUsage(SanicException): | ||||
| @@ -39,6 +40,7 @@ class InvalidUsage(SanicException): | ||||
|     """ | ||||
|  | ||||
|     status_code = 400 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class MethodNotSupported(SanicException): | ||||
| @@ -47,6 +49,7 @@ class MethodNotSupported(SanicException): | ||||
|     """ | ||||
|  | ||||
|     status_code = 405 | ||||
|     quiet = True | ||||
|  | ||||
|     def __init__(self, message, method, allowed_methods): | ||||
|         super().__init__(message) | ||||
| @@ -70,6 +73,7 @@ class ServiceUnavailable(SanicException): | ||||
|     """ | ||||
|  | ||||
|     status_code = 503 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class URLBuildError(ServerError): | ||||
| @@ -101,6 +105,7 @@ class RequestTimeout(SanicException): | ||||
|     """ | ||||
|  | ||||
|     status_code = 408 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class PayloadTooLarge(SanicException): | ||||
| @@ -109,6 +114,7 @@ class PayloadTooLarge(SanicException): | ||||
|     """ | ||||
|  | ||||
|     status_code = 413 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class HeaderNotFound(InvalidUsage): | ||||
| @@ -117,6 +123,7 @@ class HeaderNotFound(InvalidUsage): | ||||
|     """ | ||||
|  | ||||
|     status_code = 400 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class ContentRangeError(SanicException): | ||||
| @@ -125,6 +132,7 @@ class ContentRangeError(SanicException): | ||||
|     """ | ||||
|  | ||||
|     status_code = 416 | ||||
|     quiet = True | ||||
|  | ||||
|     def __init__(self, message, content_range): | ||||
|         super().__init__(message) | ||||
| @@ -137,6 +145,7 @@ class HeaderExpectationFailed(SanicException): | ||||
|     """ | ||||
|  | ||||
|     status_code = 417 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class Forbidden(SanicException): | ||||
| @@ -145,6 +154,7 @@ class Forbidden(SanicException): | ||||
|     """ | ||||
|  | ||||
|     status_code = 403 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class InvalidRangeType(ContentRangeError): | ||||
| @@ -153,6 +163,7 @@ class InvalidRangeType(ContentRangeError): | ||||
|     """ | ||||
|  | ||||
|     status_code = 416 | ||||
|     quiet = True | ||||
|  | ||||
|  | ||||
| class PyFileError(Exception): | ||||
| @@ -196,6 +207,7 @@ class Unauthorized(SanicException): | ||||
|     """ | ||||
|  | ||||
|     status_code = 401 | ||||
|     quiet = True | ||||
|  | ||||
|     def __init__(self, message, status_code=None, scheme=None, **kwargs): | ||||
|         super().__init__(message, status_code) | ||||
|   | ||||
| @@ -486,20 +486,24 @@ class Http: | ||||
|                 self.keep_alive = False | ||||
|                 raise InvalidUsage("Bad chunked encoding") | ||||
|  | ||||
|             del buf[: pos + 2] | ||||
|  | ||||
|             if size <= 0: | ||||
|                 self.request_body = None | ||||
|                 # Because we are leaving one CRLF in the buffer, we manually | ||||
|                 # reset the buffer here | ||||
|                 self.recv_buffer = bytearray() | ||||
|  | ||||
|                 if size < 0: | ||||
|                     self.keep_alive = False | ||||
|                     raise InvalidUsage("Bad chunked encoding") | ||||
|  | ||||
|                 # Consume CRLF, chunk size 0 and the two CRLF that follow | ||||
|                 pos += 4 | ||||
|                 # Might need to wait for the final CRLF | ||||
|                 while len(buf) < pos: | ||||
|                     await self._receive_more() | ||||
|                 del buf[:pos] | ||||
|                 return None | ||||
|  | ||||
|             # Remove CRLF, chunk size and the CRLF that follows | ||||
|             del buf[: pos + 2] | ||||
|  | ||||
|             self.request_bytes_left = size | ||||
|             self.request_bytes += size | ||||
|  | ||||
|   | ||||
							
								
								
									
										4
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								setup.py
									
									
									
									
									
								
							| @@ -83,7 +83,7 @@ ujson = "ujson>=1.35" + env_dependency | ||||
| uvloop = "uvloop>=0.5.3" + env_dependency | ||||
| types_ujson = "types-ujson" + env_dependency | ||||
| requirements = [ | ||||
|     "sanic-routing==0.7.0", | ||||
|     "sanic-routing~=0.7", | ||||
|     "httptools>=0.0.10", | ||||
|     uvloop, | ||||
|     ujson, | ||||
| @@ -93,7 +93,7 @@ requirements = [ | ||||
| ] | ||||
|  | ||||
| tests_require = [ | ||||
|     "sanic-testing>=0.6.0", | ||||
|     "sanic-testing>=0.7.0b1", | ||||
|     "pytest==5.2.1", | ||||
|     "coverage==5.3", | ||||
|     "gunicorn==20.0.4", | ||||
|   | ||||
							
								
								
									
										82
									
								
								tests/test_pipelining.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								tests/test_pipelining.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| from httpx import AsyncByteStream | ||||
| from sanic_testing.reusable import ReusableClient | ||||
|  | ||||
| from sanic.response import json | ||||
|  | ||||
|  | ||||
| def test_no_body_requests(app): | ||||
|     @app.get("/") | ||||
|     async def handler(request): | ||||
|         return json( | ||||
|             { | ||||
|                 "request_id": str(request.id), | ||||
|                 "connection_id": id(request.conn_info), | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     client = ReusableClient(app, port=1234) | ||||
|  | ||||
|     with client: | ||||
|         _, response1 = client.get("/") | ||||
|         _, response2 = client.get("/") | ||||
|  | ||||
|     assert response1.status == response2.status == 200 | ||||
|     assert response1.json["request_id"] != response2.json["request_id"] | ||||
|     assert response1.json["connection_id"] == response2.json["connection_id"] | ||||
|  | ||||
|  | ||||
| def test_json_body_requests(app): | ||||
|     @app.post("/") | ||||
|     async def handler(request): | ||||
|         return json( | ||||
|             { | ||||
|                 "request_id": str(request.id), | ||||
|                 "connection_id": id(request.conn_info), | ||||
|                 "foo": request.json.get("foo"), | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     client = ReusableClient(app, port=1234) | ||||
|  | ||||
|     with client: | ||||
|         _, response1 = client.post("/", json={"foo": True}) | ||||
|         _, response2 = client.post("/", json={"foo": True}) | ||||
|  | ||||
|     assert response1.status == response2.status == 200 | ||||
|     assert response1.json["foo"] is response2.json["foo"] is True | ||||
|     assert response1.json["request_id"] != response2.json["request_id"] | ||||
|     assert response1.json["connection_id"] == response2.json["connection_id"] | ||||
|  | ||||
|  | ||||
| def test_streaming_body_requests(app): | ||||
|     @app.post("/", stream=True) | ||||
|     async def handler(request): | ||||
|         data = [part.decode("utf-8") async for part in request.stream] | ||||
|         return json( | ||||
|             { | ||||
|                 "request_id": str(request.id), | ||||
|                 "connection_id": id(request.conn_info), | ||||
|                 "data": data, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     data = ["hello", "world"] | ||||
|  | ||||
|     class Data(AsyncByteStream): | ||||
|         def __init__(self, data): | ||||
|             self.data = data | ||||
|  | ||||
|         async def __aiter__(self): | ||||
|             for value in self.data: | ||||
|                 yield value.encode("utf-8") | ||||
|  | ||||
|     client = ReusableClient(app, port=1234) | ||||
|  | ||||
|     with client: | ||||
|         _, response1 = client.post("/", data=Data(data)) | ||||
|         _, response2 = client.post("/", data=Data(data)) | ||||
|  | ||||
|     assert response1.status == response2.status == 200 | ||||
|     assert response1.json["data"] == response2.json["data"] == data | ||||
|     assert response1.json["request_id"] != response2.json["request_id"] | ||||
|     assert response1.json["connection_id"] == response2.json["connection_id"] | ||||
| @@ -471,7 +471,7 @@ def test_stack_trace_on_not_found(app, static_file_directory, caplog): | ||||
|  | ||||
|     assert response.status == 404 | ||||
|     assert counter[logging.INFO] == 5 | ||||
|     assert counter[logging.ERROR] == 1 | ||||
|     assert counter[logging.ERROR] == 0 | ||||
|  | ||||
|  | ||||
| def test_no_stack_trace_on_not_found(app, static_file_directory, caplog): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Zhiwei
					Zhiwei