diff --git a/.coveragerc b/.coveragerc index f0624d2e..724b2872 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,7 @@ [run] branch = True source = sanic -omit = site-packages, sanic/utils.py +omit = site-packages, sanic/utils.py, sanic/__main__.py [html] directory = coverage diff --git a/.travis.yml b/.travis.yml index c18e895b..83f13caf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,7 @@ deploy: provider: pypi user: channelcat password: - secure: OgADRQH3+dTL5swGzXkeRJDNbLpFzwqYnXB4iLD0Npvzj9QnKyQVvkbaeq6VmV9dpEFb5ULaAKYQq19CrXYDm28yanUSn6jdJ4SukaHusi7xt07U6H7pmoX/uZ2WZYqCSLM8cSp8TXY/3oV3rY5Jfj/AibE5XTbim5/lrhsvW6NR+ALzxc0URRPAHDZEPpojTCjSTjpY0aDsaKWg4mXVRMFfY3O68j6KaIoukIZLuoHfePLKrbZxaPG5VxNhMHEaICdxVxE/dO+7pQmQxXuIsEOHK1QiVJ9YrSGcNqgEqhN36kYP8dqMeVB07sv8Xa6o/Uax2/wXS2HEJvuwP1YD6WkoZuo9ZB85bcMdg7BV9jJDbVFVPJwc75BnTLHrMa3Q1KrRlKRDBUXBUsQivPuWhFNwUgvEayq2qSI3aRQR4Z0O+DfboEhXYojSoD64/EWBTZ7vhgbvOTGEdukUQSYrKj9P8jc1s8exomTsAiqdFxTUpzfiammUSL+M93lP4urtahl1jjXFX7gd3DzdEEb0NsGkx5lm/qdsty8/TeAvKUmC+RVU6T856W6MqN0P+yGbpWUARcSE7fwztC3SPxwAuxvIN3BHmRhOUHoORPNG2VpfbnscIzBKJR4v0JKzbpi0IDa66K+tCGsCEvQuL4cxVOtoUySPWNSUAyUWWUrGM2k= + secure: h7oNDjA/ObDBGK7xt55SV0INHOclMJW/byxMrNxvCZ0JxiRk7WBNtWYt0WJjyf5lO/L0/sfgiAk0GIdFon57S24njSLPAq/a4ptkWZ68s2A+TaF6ezJSZvE9V8khivjoeub90TzfX6P5aukRja1CSxXKJm+v0V8hGE4CZGyCgEDvK3JqIakpXllSDl19DhVftCS/lQZD7AXrZlg1kZnPCMtB5IbCVR4L2bfrSJVNptBi2CqqxacY2MOLu+jv5FzJ2BGVIJ2zoIJS2T+JmGJzpiamF6y8Amv0667i9lg2DXWCtI3PsQzCmwa3F/ZsI+ohUAvJC5yvzP7SyTJyXifRBdJ9O137QkNAHFoJOOY3B4GSnTo8/boajKXEqGiV4h2EgwNjBaR0WJl0pB7HHUCBMkNRWqo6ACB8eCr04tXWXPvkGIc+wPjq960hsUZea1O31MuktYc9Ot6eiFqm7OKoItdi7LxCen1eTj93ePgkiEnVZ+p/04Hh1U7CX31UJMNu5kCvZPIANnAuDsS2SK7Qkr88OAuWL0wmrBcXKOcnVkJtZ5mzx8T54bI1RrSYtFDBLFfOPb0GucSziMBtQpE76qPEauVwIXBk3RnR8N57xBR/lvTaIk758tf+haO0llEO5rVls1zLNZ+VlTzXy7hX0OZbdopIAcCFBFWqWMAdXQc= on: tags: true distributions: "sdist bdist_wheel" diff --git a/CHANGELOG.md b/CHANGELOG.md index 84e7be78..a5d3b7f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,80 @@ +Version 0.8 +----------- +0.8.3 + - Changes: + - Ownership changed to org 'huge-success' + +0.8.0 + - Changes: + - Add Server-Sent Events extension (Innokenty Lebedev) + - Graceful handling of request_handler_task cancellation (Ashley Sommer) + - Sanitize URL before redirection (aveao) + - Add url_bytes to request (johndoe46) + - py37 support for travisci (yunstanford) + - Auto reloader support for OSX (garyo) + - Add UUID route support (Volodymyr Maksymiv) + - Add pausable response streams (Ashley Sommer) + - Add weakref to request slots (vopankov) + - remove ubuntu 12.04 from test fixture due to deprecation (yunstanford) + - Allow streaming handlers in add_route (kinware) + - use travis_retry for tox (Raphael Deem) + - update aiohttp version for test client (yunstanford) + - add redirect import for clarity (yingshaoxo) + - Update HTTP Entity headers (Arnulfo Solís) + - Add register_listener method (Stephan Fitzpatrick) + - Remove uvloop/ujson dependencies for Windows (abuckenheimer) + - Content-length header on 204/304 responses (Arnulfo Solís) + - Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford) + - Update development status from pre-alpha to beta (Maksim Anisenkov) + - KeepAlive Timout log level changed to debug (Arnulfo Solís) + - Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov) + - Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad) + - Add support for blueprint groups and nesting (Elias Tarhini) + - Remove uvloop for windows setup (Aleksandr Kurlov) + - Auto Reload (Yaser Amari) + - Documentation updates/fixups (multiple contributors) + + - Fixes: + - Fix: auto_reload in Linux (Ashley Sommer) + - Fix: broken tests for aiohttp >= 3.3.0 (Ashley Sommer) + - Fix: disable auto_reload by default on windows (abuckenheimer) + - Fix (1143): Turn off access log with gunicorn (hqy) + - Fix (1268): Support status code for file response (Cosmo Borsky) + - Fix (1266): Add content_type flag to Sanic.static (Cosmo Borsky) + - Fix: subprotocols parameter missing from add_websocket_route (ciscorn) + - Fix (1242): Responses for CI header (yunstanford) + - Fix (1237): add version constraint for websockets (yunstanford) + - Fix (1231): memory leak - always release resource (Phillip Xu) + - Fix (1221): make request truthy if transport exists (Raphael Deem) + - Fix failing tests for aiohttp>=3.1.0 (Ashley Sommer) + - Fix try_everything examples (PyManiacGR, kot83) + - Fix (1158): default to auto_reload in debug mode (Raphael Deem) + - Fix (1136): ErrorHandler.response handler call too restrictive (Julien Castiaux) + - Fix: raw requires bytes-like object (cloudship) + - Fix (1120): passing a list in to a route decorator's host arg (Timothy Ebiuwhe) + - Fix: Bug in multipart/form-data parser (DirkGuijt) + - Fix: Exception for missing parameter when value is null (NyanKiyoshi) + - Fix: Parameter check (Howie Hu) + - Fix (1089): Routing issue with named parameters and different methods (yunstanford) + - Fix (1085): Signal handling in multi-worker mode (yunstanford) + - Fix: single quote in readme.rst (Cosven) + - Fix: method typos (Dmitry Dygalo) + - Fix: log_response correct output for ip and port (Wibowo Arindrarto) + - Fix (1042): Exception Handling (Raphael Deem) + - Fix: Chinese URIs (Howie Hu) + - Fix (1079): timeout bug when self.transport is None (Raphael Deem) + - Fix (1074): fix strict_slashes when route has slash (Raphael Deem) + - Fix (1050): add samesite cookie to cookie keys (Raphael Deem) + - Fix (1065): allow add_task after server starts (Raphael Deem) + - Fix (1061): double quotes in unauthorized exception (Raphael Deem) + - Fix (1062): inject the app in add_task method (Raphael Deem) + - Fix: update environment.yml for readthedocs (Eli Uriegas) + - Fix: Cancel request task when response timeout is triggered (Jeong YunWon) + - Fix (1052): Method not allowed response for RFC7231 compliance (Raphael Deem) + - Fix: IPv6 Address and Socket Data Format (Dan Palmer) + +Note: Changelog was unmaintained between 0.1 and 0.7 + Version 0.1 ----------- - 0.1.7 @@ -5,18 +82,18 @@ Version 0.1 - 0.1.6 - Static files - Lazy Cookie Loading - - 0.1.5 + - 0.1.5 - Cookies - Blueprint listeners and ordering - Faster Router - Fix: Incomplete file reads on medium+ sized post requests - Breaking: after_start and before_stop now pass sanic as their first argument - - 0.1.4 + - 0.1.4 - Multiprocessing - 0.1.3 - Blueprint support - Faster Response processing - - 0.1.1 - 0.1.2 + - 0.1.1 - 0.1.2 - Struggling to update pypi via CI - - 0.1.0 - - Released to public \ No newline at end of file + - 0.1.0 + - Released to public diff --git a/README.rst b/README.rst index 01801ddd..2b3f1606 100644 --- a/README.rst +++ b/README.rst @@ -7,9 +7,9 @@ Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's ba On top of being Flask-like, Sanic supports async request handlers. This means you can use the new shiny async/await syntax from Python 3.5, making your code non-blocking and speedy. -Sanic is developed `on GitHub `_. Contributions are welcome! +Sanic is developed `on GitHub `_. We also have `a community discussion board `_. Contributions are welcome! -If you have a project that utilizes Sanic make sure to comment on the `issue `_ that we use to track those projects! +If you have a project that utilizes Sanic make sure to comment on the `issue `_ that we use to track those projects! Hello World Example ------------------- @@ -47,8 +47,8 @@ Documentation .. |Join the chat at https://gitter.im/sanic-python/Lobby| image:: https://badges.gitter.im/sanic-python/Lobby.svg :target: https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge -.. |Build Status| image:: https://travis-ci.org/channelcat/sanic.svg?branch=master - :target: https://travis-ci.org/channelcat/sanic +.. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master + :target: https://travis-ci.org/huge-success/sanic .. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest :target: http://sanic.readthedocs.io/en/latest/?badge=latest .. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg @@ -56,24 +56,22 @@ Documentation .. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg :target: https://pypi.python.org/pypi/sanic/ + +Questions and Discussion +------------------------ + +`Ask a question or join the conversation `_. + Examples -------- -`Non-Core examples `_. Examples of plugins and Sanic that are outside the scope of Sanic core. +`Non-Core examples `_. Examples of plugins and Sanic that are outside the scope of Sanic core. -`Extensions `_. Sanic extensions created by the community. +`Extensions `_. Sanic extensions created by the community. -`Projects `_. Sanic in production use. +`Projects `_. Sanic in production use. -TODO ----- - * http2 - -Limitations ------------ -* No wheels for uvloop and httptools on Windows :( - Final Thoughts -------------- diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index 01c89f95..c0728627 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -31,3 +31,4 @@ A list of Sanic extensions created by the community. - [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic. - [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask. - [Sanic-WTF](https://github.com/pyx/sanic-wtf): Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier. +- [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic. diff --git a/docs/sanic/streaming.md b/docs/sanic/streaming.md index a785322a..bf3ca664 100644 --- a/docs/sanic/streaming.md +++ b/docs/sanic/streaming.md @@ -37,7 +37,7 @@ async def handler(request): if body is None: break body = body.decode('utf-8').replace('1', 'A') - response.write(body) + await response.write(body) return stream(streaming) @@ -85,8 +85,8 @@ app = Sanic(__name__) @app.route("/") async def test(request): async def sample_streaming_fn(response): - response.write('foo,') - response.write('bar') + await response.write('foo,') + await response.write('bar') return stream(sample_streaming_fn, content_type='text/csv') ``` @@ -100,7 +100,7 @@ async def index(request): conn = await asyncpg.connect(database='test') async with conn.transaction(): async for record in conn.cursor('SELECT generate_series(0, 10)'): - response.write(record[0]) + await response.write(record[0]) return stream(stream_from_db) ``` diff --git a/environment.yml b/environment.yml index 1c1dd82f..78eddfd0 100644 --- a/environment.yml +++ b/environment.yml @@ -12,9 +12,10 @@ dependencies: - zlib=1.2.8=0 - pip: - uvloop>=0.5.3 - - httptools>=0.0.9 + - httptools>=0.0.10 - ujson>=1.35 - aiofiles>=0.3.0 - - websockets>=3.2 + - websockets>=6.0 - sphinxcontrib-asyncio>=0.2.0 + - multidict>=4.0<5.0 - https://github.com/channelcat/docutils-fork/zipball/master diff --git a/examples/blueprints.py b/examples/blueprints.py index 03154049..29144c4e 100644 --- a/examples/blueprints.py +++ b/examples/blueprints.py @@ -1,7 +1,5 @@ -from sanic import Sanic -from sanic import Blueprint -from sanic.response import json - +from sanic import Blueprint, Sanic +from sanic.response import file, json app = Sanic(__name__) blueprint = Blueprint('name', url_prefix='/my_blueprint') @@ -19,7 +17,12 @@ async def foo2(request): return json({'msg': 'hi from blueprint2'}) -@blueprint3.websocket('/foo') +@blueprint3.route('/foo') +async def index(request): + return await file('websocket.html') + + +@app.websocket('/feed') async def foo3(request, ws): while True: data = 'hello!' diff --git a/examples/request_stream/server.py b/examples/request_stream/server.py index e53a224c..d3d35aef 100644 --- a/examples/request_stream/server.py +++ b/examples/request_stream/server.py @@ -30,7 +30,7 @@ async def handler(request): if body is None: break body = body.decode('utf-8').replace('1', 'A') - response.write(body) + await response.write(body) return stream(streaming) diff --git a/examples/simple_async_view.py b/examples/simple_async_view.py new file mode 100644 index 00000000..990aa21a --- /dev/null +++ b/examples/simple_async_view.py @@ -0,0 +1,42 @@ +from sanic import Sanic +from sanic.views import HTTPMethodView +from sanic.response import text + +app = Sanic('some_name') + + +class SimpleView(HTTPMethodView): + + def get(self, request): + return text('I am get method') + + def post(self, request): + return text('I am post method') + + def put(self, request): + return text('I am put method') + + def patch(self, request): + return text('I am patch method') + + def delete(self, request): + return text('I am delete method') + + +class SimpleAsyncView(HTTPMethodView): + + async def get(self, request): + return text('I am async get method') + + async def post(self, request): + return text('I am async post method') + + async def put(self, request): + return text('I am async put method') + + +app.add_route(SimpleView.as_view(), '/') +app.add_route(SimpleAsyncView.as_view(), '/async') + +if __name__ == '__main__': + app.run(host="0.0.0.0", port=8000, debug=True) diff --git a/requirements-dev.txt b/requirements-dev.txt index 674ef91d..12b29a2b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,12 +1,13 @@ aiofiles -aiohttp>=2.3.0 +aiohttp>=2.3.0,<=3.2.1 chardet<=2.3.0 beautifulsoup4 coverage -httptools +httptools>=0.0.10 flake8 pytest==3.3.2 tox ujson; sys_platform != "win32" and implementation_name == "cpython" uvloop; sys_platform != "win32" and implementation_name == "cpython" gunicorn +multidict>=4.0,<5.0 diff --git a/requirements.txt b/requirements.txt index 05968bd8..3e577b2b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ aiofiles -httptools +httptools>=0.0.10 ujson; sys_platform != "win32" and implementation_name == "cpython" uvloop; sys_platform != "win32" and implementation_name == "cpython" -websockets>=5.0,<6.0 +websockets>=6.0,<7.0 +multidict>=4.0,<5.0 diff --git a/sanic/__init__.py b/sanic/__init__.py index 78bc7bd9..51c8268e 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from sanic.app import Sanic from sanic.blueprints import Blueprint -__version__ = '0.7.0' +__version__ = '0.8.3' __all__ = ['Sanic', 'Blueprint'] diff --git a/sanic/app.py b/sanic/app.py index ca50edc1..5af21751 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -170,7 +170,7 @@ class Sanic: return handler else: raise ValueError( - 'Required parameter `request` missing' + 'Required parameter `request` missing ' 'in the {0}() route?'.format( handler.__name__)) @@ -315,13 +315,13 @@ class Sanic: return response def add_websocket_route(self, handler, uri, host=None, - strict_slashes=None, name=None): + strict_slashes=None, subprotocols=None, name=None): """A helper method to register a function as a websocket route.""" if strict_slashes is None: strict_slashes = self.strict_slashes return self.websocket(uri, host=host, strict_slashes=strict_slashes, - name=name)(handler) + subprotocols=subprotocols, name=name)(handler) def enable_websocket(self, enable=True): """Enable or disable the support for websocket. @@ -386,13 +386,14 @@ class Sanic: def static(self, uri, file_or_directory, pattern=r'/?.+', use_modified_since=True, use_content_range=False, stream_large_files=False, name='static', host=None, - strict_slashes=None): + strict_slashes=None, content_type=None): """Register a root to serve files from. The input can either be a file or a directory. See """ static_register(self, uri, file_or_directory, pattern, use_modified_since, use_content_range, - stream_large_files, name, host, strict_slashes) + stream_large_files, name, host, strict_slashes, + content_type) def blueprint(self, blueprint, **options): """Register a blueprint on the application. @@ -570,6 +571,10 @@ class Sanic: :return: Nothing """ + # Define `response` var here to remove warnings about + # allocation before assignment below. + response = None + cancelled = False try: # -------------------------------------------- # # Request Middleware @@ -596,6 +601,13 @@ class Sanic: response = handler(request, *args, **kwargs) if isawaitable(response): response = await response + except CancelledError: + # If response handler times out, the server handles the error + # and cancels the handle_request job. + # In this case, the transport is already closed and we cannot + # issue a response. + response = None + cancelled = True except Exception as e: # -------------------------------------------- # # Response Generation Failed @@ -621,13 +633,22 @@ class Sanic: # -------------------------------------------- # # Response Middleware # -------------------------------------------- # - try: - response = await self._run_response_middleware(request, - response) - except BaseException: - error_logger.exception( - 'Exception occurred in one of response middleware handlers' - ) + # Don't run response middleware if response is None + if response is not None: + try: + response = await self._run_response_middleware(request, + response) + except CancelledError: + # Response middleware can timeout too, as above. + response = None + cancelled = True + except BaseException: + error_logger.exception( + 'Exception occurred in one of response ' + 'middleware handlers' + ) + if cancelled: + raise CancelledError() # pass the response to the correct callback if isinstance(response, StreamingHTTPResponse): @@ -670,8 +691,8 @@ class Sanic: """ # Default auto_reload to false auto_reload = False - # If debug is set, default it to true - if debug: + # If debug is set, default it to true (unless on windows) + if debug and os.name == 'posix': auto_reload = True # Allow for overriding either of the defaults auto_reload = kwargs.get("auto_reload", auto_reload) @@ -687,11 +708,12 @@ class Sanic: warnings.simplefilter('default') warnings.warn("stop_event will be removed from future versions.", DeprecationWarning) + # compatibility old access_log params + self.config.ACCESS_LOG = access_log server_settings = self._helper( host=host, port=port, debug=debug, ssl=ssl, sock=sock, workers=workers, protocol=protocol, backlog=backlog, - register_sys_signals=register_sys_signals, - access_log=access_log, auto_reload=auto_reload) + register_sys_signals=register_sys_signals, auto_reload=auto_reload) try: self.is_running = True @@ -745,12 +767,12 @@ class Sanic: warnings.simplefilter('default') warnings.warn("stop_event will be removed from future versions.", DeprecationWarning) - + # compatibility old access_log params + self.config.ACCESS_LOG = access_log server_settings = self._helper( host=host, port=port, debug=debug, ssl=ssl, sock=sock, loop=get_event_loop(), protocol=protocol, - backlog=backlog, run_async=True, - access_log=access_log) + backlog=backlog, run_async=True) # Trigger before_start events await self.trigger_events( @@ -795,8 +817,7 @@ class Sanic: def _helper(self, host=None, port=None, debug=False, ssl=None, sock=None, workers=1, loop=None, protocol=HttpProtocol, backlog=100, stop_event=None, - register_sys_signals=True, run_async=False, access_log=True, - auto_reload=False): + register_sys_signals=True, run_async=False, auto_reload=False): """Helper function used by `run` and `create_server`.""" if isinstance(ssl, dict): # try common aliaseses @@ -837,7 +858,7 @@ class Sanic: 'loop': loop, 'register_sys_signals': register_sys_signals, 'backlog': backlog, - 'access_log': access_log, + 'access_log': self.config.ACCESS_LOG, 'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE, 'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE, 'websocket_read_limit': self.config.WEBSOCKET_READ_LIMIT, diff --git a/sanic/config.py b/sanic/config.py index 8e1f383c..c5e42de5 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -39,6 +39,7 @@ class Config(dict): self.WEBSOCKET_READ_LIMIT = 2 ** 16 self.WEBSOCKET_WRITE_LIMIT = 2 ** 16 self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec + self.ACCESS_LOG = True if load_env: prefix = SANIC_PREFIX if load_env is True else load_env diff --git a/sanic/cookies.py b/sanic/cookies.py index f4cbf6a3..19daac11 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -47,16 +47,15 @@ class CookieJar(dict): super().__init__() self.headers = headers self.cookie_headers = {} + self.header_key = "Set-Cookie" def __setitem__(self, key, value): # If this cookie doesn't exist, add it to the header keys - cookie_header = self.cookie_headers.get(key) - if not cookie_header: + if not self.cookie_headers.get(key): cookie = Cookie(key, value) cookie['path'] = '/' - cookie_header = MultiHeader("Set-Cookie") - self.cookie_headers[key] = cookie_header - self.headers[cookie_header] = cookie + self.cookie_headers[key] = self.header_key + self.headers.add(self.header_key, cookie) return super().__setitem__(key, cookie) else: self[key].value = value @@ -67,7 +66,11 @@ class CookieJar(dict): self[key]['max-age'] = 0 else: cookie_header = self.cookie_headers[key] - del self.headers[cookie_header] + # remove it from header + cookies = self.headers.popall(cookie_header) + for cookie in cookies: + if cookie.key != key: + self.headers.add(cookie_header, cookie) del self.cookie_headers[key] return super().__delitem__(key) @@ -124,18 +127,3 @@ class Cookie(dict): output.append('%s=%s' % (self._keys[key], value)) return "; ".join(output).encode(encoding) - -# ------------------------------------------------------------ # -# Header Trickery -# ------------------------------------------------------------ # - - -class MultiHeader: - """String-holding object which allow us to set a header within response - that has a unique key, but may contain duplicate header names - """ - def __init__(self, name): - self.name = name - - def encode(self): - return self.name.encode() diff --git a/sanic/reloader_helpers.py b/sanic/reloader_helpers.py index 73759124..e0cb42e0 100644 --- a/sanic/reloader_helpers.py +++ b/sanic/reloader_helpers.py @@ -74,7 +74,14 @@ def kill_process_children_unix(pid): with open(children_proc_path) as children_list_file_2: children_list_pid_2 = children_list_file_2.read().split() for _pid in children_list_pid_2: - os.kill(int(_pid), signal.SIGTERM) + try: + os.kill(int(_pid), signal.SIGTERM) + except ProcessLookupError: + continue + try: + os.kill(int(child_pid), signal.SIGTERM) + except ProcessLookupError: + continue def kill_process_children_osx(pid): @@ -94,7 +101,7 @@ def kill_process_children(pid): """ if sys.platform == 'darwin': kill_process_children_osx(pid) - elif sys.platform == 'posix': + elif sys.platform == 'linux': kill_process_children_unix(pid) else: pass # should signal error here @@ -136,8 +143,8 @@ def watchdog(sleep_interval): continue elif mtime > old_time: kill_process_children(worker_process.pid) + worker_process.terminate() worker_process = restart_with_reloader() - mtimes[filename] = mtime break diff --git a/sanic/request.py b/sanic/request.py index 7ce7620d..c8b470d4 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -48,10 +48,11 @@ class Request(dict): 'app', 'headers', 'version', 'method', '_cookies', 'transport', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', '_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr', - '_socket', '_port', '__weakref__' + '_socket', '_port', '__weakref__', 'raw_url' ) def __init__(self, url_bytes, headers, version, method, transport): + self.raw_url = url_bytes # TODO: Content-Encoding detection self._parsed_url = parse_url(url_bytes) self.app = None diff --git a/sanic/response.py b/sanic/response.py index b32e9daf..f169b4f2 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,5 +1,6 @@ from mimetypes import guess_type from os import path +from urllib.parse import quote_plus try: from ujson import dumps as json_dumps @@ -7,6 +8,7 @@ except BaseException: from json import dumps as json_dumps from aiofiles import open as open_async +from multidict import CIMultiDict from sanic import http from sanic.cookies import CookieJar @@ -44,7 +46,7 @@ class BaseHTTPResponse: class StreamingHTTPResponse(BaseHTTPResponse): __slots__ = ( - 'transport', 'streaming_fn', 'status', + 'protocol', 'streaming_fn', 'status', 'content_type', 'headers', '_cookies' ) @@ -53,10 +55,10 @@ class StreamingHTTPResponse(BaseHTTPResponse): self.content_type = content_type self.streaming_fn = streaming_fn self.status = status - self.headers = headers or {} + self.headers = CIMultiDict(headers or {}) self._cookies = None - def write(self, data): + async def write(self, data): """Writes a chunk of data to the streaming response. :param data: bytes-ish data to be written. @@ -64,8 +66,9 @@ class StreamingHTTPResponse(BaseHTTPResponse): if type(data) != bytes: data = self._encode_body(data) - self.transport.write( + self.protocol.push_data( b"%x\r\n%b\r\n" % (len(data), data)) + await self.protocol.drain() async def stream( self, version="1.1", keep_alive=False, keep_alive_timeout=None): @@ -75,10 +78,12 @@ class StreamingHTTPResponse(BaseHTTPResponse): headers = self.get_headers( version, keep_alive=keep_alive, keep_alive_timeout=keep_alive_timeout) - self.transport.write(headers) - + self.protocol.push_data(headers) + await self.protocol.drain() await self.streaming_fn(self) - self.transport.write(b'0\r\n\r\n') + self.protocol.push_data(b'0\r\n\r\n') + # no need to await drain here after this write, because it is the + # very last thing we write and nothing needs to wait for it. def get_headers( self, version="1.1", keep_alive=False, keep_alive_timeout=None): @@ -124,7 +129,7 @@ class HTTPResponse(BaseHTTPResponse): self.body = body_bytes self.status = status - self.headers = headers or {} + self.headers = CIMultiDict(headers or {}) self._cookies = None def output( @@ -231,8 +236,8 @@ def html(body, status=200, headers=None): content_type="text/html; charset=utf-8") -async def file( - location, mime_type=None, headers=None, filename=None, _range=None): +async def file(location, status=200, mime_type=None, headers=None, + filename=None, _range=None): """Return a response object with file data. :param location: Location of file on system. @@ -258,15 +263,14 @@ async def file( out_stream = await _file.read() mime_type = mime_type or guess_type(filename)[0] or 'text/plain' - return HTTPResponse(status=200, + return HTTPResponse(status=status, headers=headers, content_type=mime_type, body_bytes=out_stream) -async def file_stream( - location, chunk_size=4096, mime_type=None, headers=None, - filename=None, _range=None): +async def file_stream(location, status=200, chunk_size=4096, mime_type=None, + headers=None, filename=None, _range=None): """Return a streaming response object with file data. :param location: Location of file on system. @@ -297,13 +301,13 @@ async def file_stream( if len(content) < 1: break to_send -= len(content) - response.write(content) + await response.write(content) else: while True: content = await _file.read(chunk_size) if len(content) < 1: break - response.write(content) + await response.write(content) finally: await _file.close() return # Returning from this fn closes the stream @@ -313,7 +317,7 @@ async def file_stream( headers['Content-Range'] = 'bytes %s-%s/%s' % ( _range.start, _range.end, _range.total) return StreamingHTTPResponse(streaming_fn=_streaming_fn, - status=200, + status=status, headers=headers, content_type=mime_type) @@ -359,8 +363,11 @@ def redirect(to, headers=None, status=302, """ headers = headers or {} + # URL Quote the URL before redirecting + safe_to = quote_plus(to, safe=":/#?&=@[]!$&'()*+,;") + # According to RFC 7231, a relative URI is now permitted. - headers['Location'] = to + headers['Location'] = safe_to return HTTPResponse( status=status, diff --git a/sanic/server.py b/sanic/server.py index fc8291b5..17529a32 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -18,6 +18,7 @@ from time import time from httptools import HttpRequestParser from httptools.parser.errors import HttpParserError +from multidict import CIMultiDict try: import uvloop @@ -39,25 +40,6 @@ class Signal: stopped = False -class CIDict(dict): - """Case Insensitive dict where all keys are converted to lowercase - This does not maintain the inputted case when calling items() or keys() - in favor of speed, since headers are case insensitive - """ - - def get(self, key, default=None): - return super().get(key.casefold(), default) - - def __getitem__(self, key): - return super().__getitem__(key.casefold()) - - def __setitem__(self, key, value): - return super().__setitem__(key.casefold(), value) - - def __contains__(self, key): - return super().__contains__(key.casefold()) - - class HttpProtocol(asyncio.Protocol): __slots__ = ( # event loop, connection @@ -73,7 +55,8 @@ class HttpProtocol(asyncio.Protocol): # connection management '_total_request_size', '_request_timeout_handler', '_response_timeout_handler', '_keep_alive_timeout_handler', - '_last_request_time', '_last_response_time', '_is_stream_handler') + '_last_request_time', '_last_response_time', '_is_stream_handler', + '_not_paused') def __init__(self, *, loop, request_handler, error_handler, signal=Signal(), connections=set(), request_timeout=60, @@ -100,6 +83,7 @@ class HttpProtocol(asyncio.Protocol): self.request_class = request_class or Request self.is_request_stream = is_request_stream self._is_stream_handler = False + self._not_paused = asyncio.Event(loop=loop) self._total_request_size = 0 self._request_timeout_handler = None self._response_timeout_handler = None @@ -114,6 +98,7 @@ class HttpProtocol(asyncio.Protocol): if 'requests_count' not in self.state: self.state['requests_count'] = 0 self._debug = debug + self._not_paused.set() @property def keep_alive(self): @@ -142,6 +127,12 @@ class HttpProtocol(asyncio.Protocol): if self._keep_alive_timeout_handler: self._keep_alive_timeout_handler.cancel() + def pause_writing(self): + self._not_paused.clear() + + def resume_writing(self): + self._not_paused.set() + def request_timeout_callback(self): # See the docstring in the RequestTimeout exception, to see # exactly what this timeout is checking for. @@ -159,10 +150,7 @@ class HttpProtocol(asyncio.Protocol): self._request_stream_task.cancel() if self._request_handler_task: self._request_handler_task.cancel() - try: - raise RequestTimeout('Request Timeout') - except RequestTimeout as exception: - self.write_error(exception) + self.write_error(RequestTimeout('Request Timeout')) def response_timeout_callback(self): # Check if elapsed time since response was initiated exceeds our @@ -179,10 +167,7 @@ class HttpProtocol(asyncio.Protocol): self._request_stream_task.cancel() if self._request_handler_task: self._request_handler_task.cancel() - try: - raise ServiceUnavailable('Response Timeout') - except ServiceUnavailable as exception: - self.write_error(exception) + self.write_error(ServiceUnavailable('Response Timeout')) def keep_alive_timeout_callback(self): # Check if elapsed time since last response exceeds our configured @@ -208,8 +193,7 @@ class HttpProtocol(asyncio.Protocol): # memory limits self._total_request_size += len(data) if self._total_request_size > self.request_max_size: - exception = PayloadTooLarge('Payload Too Large') - self.write_error(exception) + self.write_error(PayloadTooLarge('Payload Too Large')) # Create parser if this is the first time we're receiving data if self.parser is None: @@ -227,8 +211,7 @@ class HttpProtocol(asyncio.Protocol): message = 'Bad Request' if self._debug: message += '\n' + traceback.format_exc() - exception = InvalidUsage(message) - self.write_error(exception) + self.write_error(InvalidUsage(message)) def on_url(self, url): if not self.url: @@ -242,8 +225,7 @@ class HttpProtocol(asyncio.Protocol): if value is not None: if self._header_fragment == b'Content-Length' \ and int(value) > self.request_max_size: - exception = PayloadTooLarge('Payload Too Large') - self.write_error(exception) + self.write_error(PayloadTooLarge('Payload Too Large')) try: value = value.decode() except UnicodeDecodeError: @@ -256,7 +238,7 @@ class HttpProtocol(asyncio.Protocol): def on_headers_complete(self): self.request = self.request_class( url_bytes=self.url, - headers=CIDict(self.headers), + headers=CIMultiDict(self.headers), version=self.parser.get_http_version(), method=self.parser.get_method().decode(), transport=self.transport @@ -369,6 +351,12 @@ class HttpProtocol(asyncio.Protocol): self._last_response_time = current_time self.cleanup() + async def drain(self): + await self._not_paused.wait() + + def push_data(self, data): + self.transport.write(data) + async def stream_response(self, response): """ Streams a response to the client asynchronously. Attaches @@ -378,9 +366,10 @@ class HttpProtocol(asyncio.Protocol): if self._response_timeout_handler: self._response_timeout_handler.cancel() self._response_timeout_handler = None + try: keep_alive = self.keep_alive - response.transport = self.transport + response.protocol = self await response.stream( self.request.version, keep_alive, self.keep_alive_timeout) self.log_response(response) @@ -435,7 +424,7 @@ class HttpProtocol(asyncio.Protocol): self.log_response(response) try: self.transport.close() - except AttributeError as e: + except AttributeError: logger.debug('Connection lost before server could close it.') def bail_out(self, message, from_error=False): @@ -445,8 +434,7 @@ class HttpProtocol(asyncio.Protocol): self.transport.get_extra_info('peername')) logger.debug('Exception:\n%s', traceback.format_exc()) else: - exception = ServerError(message) - self.write_error(exception) + self.write_error(ServerError(message)) logger.error(message) def cleanup(self): diff --git a/sanic/static.py b/sanic/static.py index f2d02ab0..07831390 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -19,7 +19,7 @@ from sanic.response import file, file_stream, HTTPResponse def register(app, uri, file_or_directory, pattern, use_modified_since, use_content_range, stream_large_files, name='static', host=None, - strict_slashes=None): + strict_slashes=None, content_type=None): # TODO: Though sanic is not a file server, I feel like we should at least # make a good effort here. Modified-since is nice, but we could # also look into etags, expires, and caching @@ -41,6 +41,7 @@ def register(app, uri, file_or_directory, pattern, If this is an integer, this represents the threshold size to switch to file_stream() :param name: user defined name used for url_for + :param content_type: user defined content type for header """ # If we're not trying to match a file directly, # serve from the folder @@ -95,10 +96,10 @@ def register(app, uri, file_or_directory, pattern, del headers['Content-Length'] for key, value in _range.headers.items(): headers[key] = value + headers['Content-Type'] = content_type \ + or guess_type(file_path)[0] or 'text/plain' if request.method == 'HEAD': - return HTTPResponse( - headers=headers, - content_type=guess_type(file_path)[0] or 'text/plain') + return HTTPResponse(headers=headers) else: if stream_large_files: if isinstance(stream_large_files, int): diff --git a/sanic/websocket.py b/sanic/websocket.py index 99408af5..9ccf9fdf 100644 --- a/sanic/websocket.py +++ b/sanic/websocket.py @@ -57,17 +57,11 @@ class WebSocketProtocol(HttpProtocol): async def websocket_handshake(self, request, subprotocols=None): # let the websockets package do the handshake with the client - headers = [] - - def get_header(k): - return request.headers.get(k, '') - - def set_header(k, v): - headers.append((k, v)) + headers = {} try: - key = handshake.check_request(get_header) - handshake.build_response(set_header, key) + key = handshake.check_request(request.headers) + handshake.build_response(headers, key) except InvalidHandshake: raise InvalidUsage('Invalid websocket request') @@ -79,12 +73,12 @@ class WebSocketProtocol(HttpProtocol): for p in client_subprotocols: if p in subprotocols: subprotocol = p - set_header('Sec-Websocket-Protocol', subprotocol) + headers['Sec-Websocket-Protocol'] = subprotocol break # write the 101 response back to the client rv = b'HTTP/1.1 101 Switching Protocols\r\n' - for k, v in headers: + for k, v in headers.items(): rv += k.encode('utf-8') + b': ' + v.encode('utf-8') + b'\r\n' rv += b'\r\n' request.transport.write(rv) diff --git a/setup.py b/setup.py index 73cb559f..027918bc 100644 --- a/setup.py +++ b/setup.py @@ -56,11 +56,12 @@ ujson = 'ujson>=1.35' + env_dependency uvloop = 'uvloop>=0.5.3' + env_dependency requirements = [ - 'httptools>=0.0.9', + 'httptools>=0.0.10', uvloop, ujson, 'aiofiles>=0.3.0', - 'websockets>=5.0,<6.0', + 'websockets>=6.0,<7.0', + 'multidict>=4.0,<5.0', ] if strtobool(os.environ.get("SANIC_NO_UJSON", "no")): print("Installing without uJSON") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..5844e3a1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +import pytest + +from sanic import Sanic + + +@pytest.fixture +def app(request): + return Sanic(request.node.name) diff --git a/tests/static/test.html b/tests/static/test.html new file mode 100644 index 00000000..4ba71873 --- /dev/null +++ b/tests/static/test.html @@ -0,0 +1,26 @@ + + +
+                 ▄▄▄▄▄
+        ▀▀▀██████▄▄▄       _______________
+      ▄▄▄▄▄  █████████▄  /                 \
+     ▀▀▀▀█████▌ ▀▐▄ ▀▐█ |   Gotta go fast!  |
+   ▀▀█████▄▄ ▀██████▄██ | _________________/
+   ▀▄▄▄▄▄  ▀▀█▄▀█════█▀ |/
+        ▀▀▀▄  ▀▀███ ▀       ▄▄
+     ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
+   ██▀▄▄▄██▀▄███▀ ▀▀████      ▄██
+▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███     ▌▄▄▀
+▌    ▐▀████▐███▒▒▒▒▒▐██▌
+▀▄▄▄▄▀   ▀▀████▒▒▒▒▄██▀
+          ▀▀█████████▀
+        ▄▄██▀██████▀█
+      ▄██▀     ▀▀▀  █
+     ▄█             ▐▌
+ ▄▄▄▄█▌              ▀█▄▄▄▄▀▀▄
+▌     ▐                ▀▀▄▄▄▀
+ ▀▀▄▄▀
+
+
+ + diff --git a/tests/test_bad_request.py b/tests/test_bad_request.py index bf595085..eed4e83a 100644 --- a/tests/test_bad_request.py +++ b/tests/test_bad_request.py @@ -1,9 +1,7 @@ import asyncio -from sanic import Sanic -def test_bad_request_response(): - app = Sanic('test_bad_request_response') +def test_bad_request_response(app): lines = [] @app.listener('after_server_start') async def _request(sanic, loop): diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 4c321646..4b821e91 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -1,10 +1,10 @@ import asyncio import inspect +import os import pytest -from sanic import Sanic from sanic.blueprints import Blueprint -from sanic.response import json, text +from sanic.response import text from sanic.exceptions import NotFound, ServerError, InvalidUsage from sanic.constants import HTTP_METHODS @@ -13,9 +13,16 @@ from sanic.constants import HTTP_METHODS # GET # ------------------------------------------------------------ # +def get_file_path(static_file_directory, file_name): + return os.path.join(static_file_directory, file_name) + +def get_file_content(static_file_directory, file_name): + """The content of the static file to check""" + with open(get_file_path(static_file_directory, file_name), 'rb') as file: + return file.read() + @pytest.mark.parametrize('method', HTTP_METHODS) -def test_versioned_routes_get(method): - app = Sanic('test_shorhand_routes_get') +def test_versioned_routes_get(app, method): bp = Blueprint('test_text') method = method.lower() @@ -37,8 +44,7 @@ def test_versioned_routes_get(method): assert response.status == 200 -def test_bp(): - app = Sanic('test_text') +def test_bp(app): bp = Blueprint('test_text') @bp.route('/') @@ -51,8 +57,7 @@ def test_bp(): assert response.text == 'Hello' -def test_bp_strict_slash(): - app = Sanic('test_route_strict_slash') +def test_bp_strict_slash(app): bp = Blueprint('test_text') @bp.get('/get', strict_slashes=True) @@ -78,8 +83,7 @@ def test_bp_strict_slash(): request, response = app.test_client.post('/post') assert response.status == 404 -def test_bp_strict_slash_default_value(): - app = Sanic('test_route_strict_slash') +def test_bp_strict_slash_default_value(app): bp = Blueprint('test_text', strict_slashes=True) @bp.get('/get') @@ -98,8 +102,7 @@ def test_bp_strict_slash_default_value(): request, response = app.test_client.post('/post') assert response.status == 404 -def test_bp_strict_slash_without_passing_default_value(): - app = Sanic('test_route_strict_slash') +def test_bp_strict_slash_without_passing_default_value(app): bp = Blueprint('test_text') @bp.get('/get') @@ -118,8 +121,7 @@ def test_bp_strict_slash_without_passing_default_value(): request, response = app.test_client.post('/post') assert response.text == 'OK' -def test_bp_strict_slash_default_value_can_be_overwritten(): - app = Sanic('test_route_strict_slash') +def test_bp_strict_slash_default_value_can_be_overwritten(app): bp = Blueprint('test_text', strict_slashes=True) @bp.get('/get', strict_slashes=False) @@ -138,8 +140,7 @@ def test_bp_strict_slash_default_value_can_be_overwritten(): request, response = app.test_client.post('/post') assert response.text == 'OK' -def test_bp_with_url_prefix(): - app = Sanic('test_text') +def test_bp_with_url_prefix(app): bp = Blueprint('test_text', url_prefix='/test1') @bp.route('/') @@ -152,8 +153,7 @@ def test_bp_with_url_prefix(): assert response.text == 'Hello' -def test_several_bp_with_url_prefix(): - app = Sanic('test_text') +def test_several_bp_with_url_prefix(app): bp = Blueprint('test_text', url_prefix='/test1') bp2 = Blueprint('test_text2', url_prefix='/test2') @@ -173,8 +173,7 @@ def test_several_bp_with_url_prefix(): request, response = app.test_client.get('/test2/') assert response.text == 'Hello2' -def test_bp_with_host(): - app = Sanic('test_bp_host') +def test_bp_with_host(app): bp = Blueprint('test_bp_host', url_prefix='/test1', host="example.com") @bp.route('/') @@ -200,8 +199,7 @@ def test_bp_with_host(): assert response.text == 'Hello subdomain!' -def test_several_bp_with_host(): - app = Sanic('test_text') +def test_several_bp_with_host(app): bp = Blueprint('test_text', url_prefix='/test', host="example.com") @@ -244,8 +242,7 @@ def test_several_bp_with_host(): headers=headers) assert response.text == 'Hello3' -def test_bp_middleware(): - app = Sanic('test_middleware') +def test_bp_middleware(app): blueprint = Blueprint('test_middleware') @blueprint.middleware('response') @@ -263,8 +260,7 @@ def test_bp_middleware(): assert response.status == 200 assert response.text == 'OK' -def test_bp_exception_handler(): - app = Sanic('test_middleware') +def test_bp_exception_handler(app): blueprint = Blueprint('test_middleware') @blueprint.route('/1') @@ -296,8 +292,7 @@ def test_bp_exception_handler(): request, response = app.test_client.get('/3') assert response.status == 200 -def test_bp_listeners(): - app = Sanic('test_middleware') +def test_bp_listeners(app): blueprint = Blueprint('test_middleware') order = [] @@ -332,12 +327,11 @@ def test_bp_listeners(): assert order == [1,2,3,4,5,6] -def test_bp_static(): +def test_bp_static(app): current_file = inspect.getfile(inspect.currentframe()) with open(current_file, 'rb') as file: current_file_contents = file.read() - app = Sanic('test_static') blueprint = Blueprint('test_static') blueprint.static('/testing.file', current_file) @@ -348,8 +342,28 @@ def test_bp_static(): assert response.status == 200 assert response.body == current_file_contents -def test_bp_shorthand(): - app = Sanic('test_shorhand_routes') +@pytest.mark.parametrize('file_name', ['test.html']) +def test_bp_static_content_type(app, file_name): + # This is done here, since no other test loads a file here + current_file = inspect.getfile(inspect.currentframe()) + current_directory = os.path.dirname(os.path.abspath(current_file)) + static_directory = os.path.join(current_directory, 'static') + + blueprint = Blueprint('test_static') + blueprint.static( + '/testing.file', + get_file_path(static_directory, file_name), + content_type='text/html; charset=utf-8' + ) + + app.blueprint(blueprint) + + request, response = app.test_client.get('/testing.file') + assert response.status == 200 + assert response.body == get_file_content(static_directory, file_name) + assert response.headers['Content-Type'] == 'text/html; charset=utf-8' + +def test_bp_shorthand(app): blueprint = Blueprint('test_shorhand_routes') ev = asyncio.Event() @@ -447,43 +461,41 @@ def test_bp_shorthand(): assert response.status == 101 assert ev.is_set() -def test_bp_group(): - app = Sanic('test_nested_bp_groups') - +def test_bp_group(app): deep_0 = Blueprint('deep_0', url_prefix='/deep') deep_1 = Blueprint('deep_1', url_prefix = '/deep1') @deep_0.route('/') def handler(request): return text('D0_OK') - + @deep_1.route('/bottom') def handler(request): return text('D1B_OK') mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid') mid_1 = Blueprint('mid_tier', url_prefix='/mid1') - + @mid_1.route('/') def handler(request): return text('M1_OK') top = Blueprint.group(mid_0, mid_1) - + app.blueprint(top) - + @app.route('/') def handler(request): return text('TOP_OK') - + request, response = app.test_client.get('/') assert response.text == 'TOP_OK' - + request, response = app.test_client.get('/mid1') assert response.text == 'M1_OK' - + request, response = app.test_client.get('/mid/deep') assert response.text == 'D0_OK' - + request, response = app.test_client.get('/mid/deep1/bottom') assert response.text == 'D1B_OK' diff --git a/tests/test_config.py b/tests/test_config.py index e393d02b..14b28d46 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,8 +5,7 @@ from tempfile import NamedTemporaryFile from sanic import Sanic -def test_load_from_object(): - app = Sanic('test_load_from_object') +def test_load_from_object(app): class Config: not_for_config = 'should not be used' CONFIG_VALUE = 'should be used' @@ -16,26 +15,29 @@ def test_load_from_object(): assert app.config.CONFIG_VALUE == 'should be used' assert 'not_for_config' not in app.config + def test_auto_load_env(): environ["SANIC_TEST_ANSWER"] = "42" app = Sanic() assert app.config.TEST_ANSWER == 42 del environ["SANIC_TEST_ANSWER"] + def test_dont_load_env(): environ["SANIC_TEST_ANSWER"] = "42" app = Sanic(load_env=False) - assert getattr(app.config, 'TEST_ANSWER', None) == None + assert getattr(app.config, 'TEST_ANSWER', None) is None del environ["SANIC_TEST_ANSWER"] + def test_load_env_prefix(): environ["MYAPP_TEST_ANSWER"] = "42" app = Sanic(load_env='MYAPP_') assert app.config.TEST_ANSWER == 42 del environ["MYAPP_TEST_ANSWER"] -def test_load_from_file(): - app = Sanic('test_load_from_file') + +def test_load_from_file(app): config = b""" VALUE = 'some value' condition = 1 == 1 @@ -53,14 +55,12 @@ if condition: assert 'condition' not in app.config -def test_load_from_missing_file(): - app = Sanic('test_load_from_missing_file') +def test_load_from_missing_file(app): with pytest.raises(IOError): app.config.from_pyfile('non-existent file') -def test_load_from_envvar(): - app = Sanic('test_load_from_envvar') +def test_load_from_envvar(app): config = b"VALUE = 'some value'" with NamedTemporaryFile() as config_file: config_file.write(config) @@ -71,15 +71,17 @@ def test_load_from_envvar(): assert app.config.VALUE == 'some value' -def test_load_from_missing_envvar(): - app = Sanic('test_load_from_missing_envvar') - with pytest.raises(RuntimeError): +def test_load_from_missing_envvar(app): + with pytest.raises(RuntimeError) as e: app.config.from_envvar('non-existent variable') + assert str(e.value) == ("The environment variable 'non-existent " + "variable' is not set and thus configuration " + "could not be loaded.") -def test_overwrite_exisiting_config(): - app = Sanic('test_overwrite_exisiting_config') +def test_overwrite_exisiting_config(app): app.config.DEFAULT = 1 + class Config: DEFAULT = 2 @@ -87,7 +89,7 @@ def test_overwrite_exisiting_config(): assert app.config.DEFAULT == 2 -def test_missing_config(): - app = Sanic('test_missing_config') - with pytest.raises(AttributeError): +def test_missing_config(app): + with pytest.raises(AttributeError) as e: app.config.NON_EXISTENT + assert str(e.value) == ("Config has no 'NON_EXISTENT'") diff --git a/tests/test_cookies.py b/tests/test_cookies.py index 84b493cb..87d7dcad 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -9,8 +9,7 @@ import pytest # GET # ------------------------------------------------------------ # -def test_cookies(): - app = Sanic('test_text') +def test_cookies(app): @app.route('/') def handler(request): @@ -25,12 +24,12 @@ def test_cookies(): assert response.text == 'Cookies are: working!' assert response_cookies['right_back'].value == 'at you' + @pytest.mark.parametrize("httponly,expected", [ (False, False), (True, True), ]) -def test_false_cookies_encoded(httponly, expected): - app = Sanic('test_text') +def test_false_cookies_encoded(app, httponly, expected): @app.route('/') def handler(request): @@ -48,8 +47,7 @@ def test_false_cookies_encoded(httponly, expected): (False, False), (True, True), ]) -def test_false_cookies(httponly, expected): - app = Sanic('test_text') +def test_false_cookies(app, httponly, expected): @app.route('/') def handler(request): @@ -64,8 +62,7 @@ def test_false_cookies(httponly, expected): assert ('HttpOnly' in response_cookies['right_back'].output()) == expected -def test_http2_cookies(): - app = Sanic('test_http2_cookies') +def test_http2_cookies(app): @app.route('/') async def handler(request): @@ -77,8 +74,7 @@ def test_http2_cookies(): assert response.text == 'Cookies are: working!' -def test_cookie_options(): - app = Sanic('test_text') +def test_cookie_options(app): @app.route('/') def handler(request): @@ -95,8 +91,7 @@ def test_cookie_options(): assert response_cookies['test'].value == 'at you' assert response_cookies['test']['httponly'] == True -def test_cookie_deletion(): - app = Sanic('test_text') +def test_cookie_deletion(app): @app.route('/') def handler(request): diff --git a/tests/test_create_task.py b/tests/test_create_task.py index 1517ca8c..3a94884d 100644 --- a/tests/test_create_task.py +++ b/tests/test_create_task.py @@ -1,18 +1,16 @@ -from sanic import Sanic from sanic.response import text from threading import Event import asyncio from queue import Queue -def test_create_task(): +def test_create_task(app): e = Event() async def coro(): await asyncio.sleep(0.05) e.set() - app = Sanic('test_create_task') app.add_task(coro) @app.route('/early') @@ -30,8 +28,7 @@ def test_create_task(): request, response = app.test_client.get('/late') assert response.body == b'True' -def test_create_task_with_app_arg(): - app = Sanic('test_add_task') +def test_create_task_with_app_arg(app): q = Queue() @app.route('/') @@ -44,4 +41,4 @@ def test_create_task_with_app_arg(): app.add_task(coro) request, response = app.test_client.get('/') - assert q.get() == 'test_add_task' + assert q.get() == 'test_create_task_with_app_arg' diff --git a/tests/test_custom_protocol.py b/tests/test_custom_protocol.py index 74564012..236eb831 100644 --- a/tests/test_custom_protocol.py +++ b/tests/test_custom_protocol.py @@ -1,9 +1,6 @@ -from sanic import Sanic from sanic.server import HttpProtocol from sanic.response import text -app = Sanic('test_custom_porotocol') - class CustomHttpProtocol(HttpProtocol): @@ -16,12 +13,12 @@ class CustomHttpProtocol(HttpProtocol): self.transport.close() -@app.route('/1') -async def handler_1(request): - return 'OK' +def test_use_custom_protocol(app): + @app.route('/1') + async def handler_1(request): + return 'OK' -def test_use_custom_protocol(): server_kwargs = { 'protocol': CustomHttpProtocol } diff --git a/tests/test_dynamic_routes.py b/tests/test_dynamic_routes.py index 950584a8..a37861e3 100644 --- a/tests/test_dynamic_routes.py +++ b/tests/test_dynamic_routes.py @@ -10,8 +10,7 @@ import pytest ("put", "text", "OK2 test"), ("delete", "status", 405), ]) -def test_overload_dynamic_routes(method, attr, expected): - app = Sanic('test_dynamic_route') +def test_overload_dynamic_routes(app, method, attr, expected): @app.route('/overload/', methods=['GET']) async def handler1(request, param): @@ -25,8 +24,7 @@ def test_overload_dynamic_routes(method, attr, expected): assert getattr(response, attr) == expected -def test_overload_dynamic_routes_exist(): - app = Sanic('test_dynamic_route') +def test_overload_dynamic_routes_exist(app): @app.route('/overload/', methods=['GET']) async def handler1(request, param): diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index b4e2c6ea..cf41982d 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -81,8 +81,7 @@ def exception_app(): return app -def test_catch_exception_list(): - app = Sanic('exception_list') +def test_catch_exception_list(app): @app.exception([SanicExceptionTestException, NotFound]) def exception_list(request, exception): diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 2a9e93a2..53a2872e 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -9,14 +9,39 @@ import aiohttp from aiohttp import TCPConnector from sanic.testing import SanicTestClient, HOST, PORT +try: + try: + import packaging # direct use + except ImportError: + # setuptools v39.0 and above. + try: + from setuptools.extern import packaging + except ImportError: + # Before setuptools v39.0 + from pkg_resources.extern import packaging + version = packaging.version +except ImportError: + raise RuntimeError("The 'packaging' library is missing.") + +aiohttp_version = version.parse(aiohttp.__version__) class ReuseableTCPConnector(TCPConnector): def __init__(self, *args, **kwargs): super(ReuseableTCPConnector, self).__init__(*args, **kwargs) self.old_proto = None - if aiohttp.__version__ >= '3.0': - + if aiohttp_version >= version.parse('3.3.0'): + async def connect(self, req, traces, timeout): + new_conn = await super(ReuseableTCPConnector, self)\ + .connect(req, traces, timeout) + if self.old_proto is not None: + if self.old_proto != new_conn._protocol: + raise RuntimeError( + "We got a new connection, wanted the same one!") + print(new_conn.__dict__) + self.old_proto = new_conn._protocol + return new_conn + elif aiohttp_version >= version.parse('3.0.0'): async def connect(self, req, traces=None): new_conn = await super(ReuseableTCPConnector, self)\ .connect(req, traces=traces) @@ -28,7 +53,6 @@ class ReuseableTCPConnector(TCPConnector): self.old_proto = new_conn._protocol return new_conn else: - async def connect(self, req): new_conn = await super(ReuseableTCPConnector, self)\ .connect(req) diff --git a/tests/test_logging.py b/tests/test_logging.py index 1a040a5c..3af3f122 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -23,7 +23,7 @@ def reset_logging(): reload(logging) -def test_log(): +def test_log(app): log_stream = StringIO() for handler in logging.root.handlers[:]: logging.root.removeHandler(handler) @@ -33,7 +33,6 @@ def test_log(): stream=log_stream ) log = logging.getLogger() - app = Sanic('test_logging') rand_string = str(uuid.uuid4()) @app.route('/') @@ -80,9 +79,8 @@ def test_logging_pass_customer_logconfig(): @pytest.mark.parametrize('debug', (True, False, )) -def test_log_connection_lost(debug, monkeypatch): +def test_log_connection_lost(app, debug, monkeypatch): """ Should not log Connection lost exception on non debug """ - app = Sanic('connection_lost') stream = StringIO() root = logging.getLogger('root') root.addHandler(logging.StreamHandler(stream)) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 4d4d6901..d2098cd1 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,7 +1,5 @@ -from json import loads as json_loads, dumps as json_dumps -from sanic import Sanic from sanic.request import Request -from sanic.response import json, text, HTTPResponse +from sanic.response import text, HTTPResponse from sanic.exceptions import NotFound @@ -9,9 +7,7 @@ from sanic.exceptions import NotFound # GET # ------------------------------------------------------------ # -def test_middleware_request(): - app = Sanic('test_middleware_request') - +def test_middleware_request(app): results = [] @app.middleware @@ -28,9 +24,7 @@ def test_middleware_request(): assert type(results[0]) is Request -def test_middleware_response(): - app = Sanic('test_middleware_response') - +def test_middleware_response(app): results = [] @app.middleware('request') @@ -54,8 +48,7 @@ def test_middleware_response(): assert isinstance(results[2], HTTPResponse) -def test_middleware_response_exception(): - app = Sanic('test_middleware_response_exception') +def test_middleware_response_exception(app): result = {'status_code': None} @app.middleware('response') @@ -75,8 +68,7 @@ def test_middleware_response_exception(): assert response.text == 'OK' assert result['status_code'] == 404 -def test_middleware_override_request(): - app = Sanic('test_middleware_override_request') +def test_middleware_override_request(app): @app.middleware async def halt_request(request): @@ -92,8 +84,7 @@ def test_middleware_override_request(): assert response.text == 'OK' -def test_middleware_override_response(): - app = Sanic('test_middleware_override_response') +def test_middleware_override_response(app): @app.middleware('response') async def process_response(request, response): @@ -109,10 +100,7 @@ def test_middleware_override_response(): assert response.text == 'OK' - -def test_middleware_order(): - app = Sanic('test_middleware_order') - +def test_middleware_order(app): order = [] @app.middleware('request') diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 7d94a972..c78e19fd 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -2,15 +2,13 @@ import multiprocessing import random import signal -from sanic import Sanic from sanic.testing import HOST, PORT -def test_multiprocessing(): +def test_multiprocessing(app): """Tests that the number of children we produce is correct""" # Selects a number at random so we can spot check num_workers = random.choice(range(2, multiprocessing.cpu_count() * 2 + 1)) - app = Sanic('test_multiprocessing') process_list = set() def stop_on_alarm(*args): diff --git a/tests/test_named_routes.py b/tests/test_named_routes.py index ca377e8d..65d5955a 100644 --- a/tests/test_named_routes.py +++ b/tests/test_named_routes.py @@ -4,7 +4,6 @@ import asyncio import pytest -from sanic import Sanic from sanic.blueprints import Blueprint from sanic.response import text from sanic.exceptions import URLBuildError @@ -16,8 +15,7 @@ from sanic.constants import HTTP_METHODS # ------------------------------------------------------------ # @pytest.mark.parametrize('method', HTTP_METHODS) -def test_versioned_named_routes_get(method): - app = Sanic('test_shorhand_routes_get') +def test_versioned_named_routes_get(app, method): bp = Blueprint('test_bp', url_prefix='/bp') method = method.lower() @@ -57,8 +55,7 @@ def test_versioned_named_routes_get(method): app.url_for('handler') -def test_shorthand_default_routes_get(): - app = Sanic('test_shorhand_routes_get') +def test_shorthand_default_routes_get(app): @app.get('/get') def handler(request): @@ -68,8 +65,7 @@ def test_shorthand_default_routes_get(): assert app.url_for('handler') == '/get' -def test_shorthand_named_routes_get(): - app = Sanic('test_shorhand_routes_get') +def test_shorthand_named_routes_get(app): bp = Blueprint('test_bp', url_prefix='/bp') @app.get('/get', name='route_get') @@ -93,8 +89,7 @@ def test_shorthand_named_routes_get(): app.url_for('test_bp.handler2') -def test_shorthand_named_routes_post(): - app = Sanic('test_shorhand_routes_post') +def test_shorthand_named_routes_post(app): @app.post('/post', name='route_name') def handler(request): @@ -106,8 +101,7 @@ def test_shorthand_named_routes_post(): app.url_for('handler') -def test_shorthand_named_routes_put(): - app = Sanic('test_shorhand_routes_put') +def test_shorthand_named_routes_put(app): @app.put('/put', name='route_put') def handler(request): @@ -121,8 +115,7 @@ def test_shorthand_named_routes_put(): app.url_for('handler') -def test_shorthand_named_routes_delete(): - app = Sanic('test_shorhand_routes_delete') +def test_shorthand_named_routes_delete(app): @app.delete('/delete', name='route_delete') def handler(request): @@ -136,8 +129,7 @@ def test_shorthand_named_routes_delete(): app.url_for('handler') -def test_shorthand_named_routes_patch(): - app = Sanic('test_shorhand_routes_patch') +def test_shorthand_named_routes_patch(app): @app.patch('/patch', name='route_patch') def handler(request): @@ -151,8 +143,7 @@ def test_shorthand_named_routes_patch(): app.url_for('handler') -def test_shorthand_named_routes_head(): - app = Sanic('test_shorhand_routes_head') +def test_shorthand_named_routes_head(app): @app.head('/head', name='route_head') def handler(request): @@ -166,8 +157,7 @@ def test_shorthand_named_routes_head(): app.url_for('handler') -def test_shorthand_named_routes_options(): - app = Sanic('test_shorhand_routes_options') +def test_shorthand_named_routes_options(app): @app.options('/options', name='route_options') def handler(request): @@ -181,8 +171,7 @@ def test_shorthand_named_routes_options(): app.url_for('handler') -def test_named_static_routes(): - app = Sanic('test_dynamic_route') +def test_named_static_routes(app): @app.route('/test', name='route_test') async def handler1(request): @@ -205,9 +194,7 @@ def test_named_static_routes(): app.url_for('handler2') -def test_named_dynamic_route(): - app = Sanic('test_dynamic_route') - +def test_named_dynamic_route(app): results = [] @app.route('/folder/', name='route_dynamic') @@ -221,8 +208,7 @@ def test_named_dynamic_route(): app.url_for('handler') -def test_dynamic_named_route_regex(): - app = Sanic('test_dynamic_route_regex') +def test_dynamic_named_route_regex(app): @app.route('/folder/', name='route_re') async def handler(request, folder_id): @@ -235,8 +221,7 @@ def test_dynamic_named_route_regex(): app.url_for('handler') -def test_dynamic_named_route_path(): - app = Sanic('test_dynamic_route_path') +def test_dynamic_named_route_path(app): @app.route('//info', name='route_dynamic_path') async def handler(request, path): @@ -249,8 +234,7 @@ def test_dynamic_named_route_path(): app.url_for('handler') -def test_dynamic_named_route_unhashable(): - app = Sanic('test_dynamic_route_unhashable') +def test_dynamic_named_route_unhashable(app): @app.route('/folder//end/', name='route_unhashable') @@ -265,8 +249,7 @@ def test_dynamic_named_route_unhashable(): app.url_for('handler') -def test_websocket_named_route(): - app = Sanic('test_websocket_route') +def test_websocket_named_route(app): ev = asyncio.Event() @app.websocket('/ws', name='route_ws') @@ -280,8 +263,7 @@ def test_websocket_named_route(): app.url_for('handler') -def test_websocket_named_route_with_subprotocols(): - app = Sanic('test_websocket_route') +def test_websocket_named_route_with_subprotocols(app): results = [] @app.websocket('/ws', subprotocols=['foo', 'bar'], name='route_ws') @@ -294,8 +276,7 @@ def test_websocket_named_route_with_subprotocols(): app.url_for('handler') -def test_static_add_named_route(): - app = Sanic('test_static_add_route') +def test_static_add_named_route(app): async def handler1(request): return text('OK1') @@ -319,9 +300,7 @@ def test_static_add_named_route(): app.url_for('handler2') -def test_dynamic_add_named_route(): - app = Sanic('test_dynamic_add_route') - +def test_dynamic_add_named_route(app): results = [] async def handler(request, name): @@ -335,8 +314,7 @@ def test_dynamic_add_named_route(): app.url_for('handler') -def test_dynamic_add_named_route_unhashable(): - app = Sanic('test_dynamic_add_route_unhashable') +def test_dynamic_add_named_route_unhashable(app): async def handler(request, unhashable): return text('OK') @@ -351,8 +329,7 @@ def test_dynamic_add_named_route_unhashable(): app.url_for('handler') -def test_overload_routes(): - app = Sanic('test_dynamic_route') +def test_overload_routes(app): @app.route('/overload', methods=['GET'], name='route_first') async def handler1(request): diff --git a/tests/test_payload_too_large.py b/tests/test_payload_too_large.py index ecac605c..49ad5ab7 100644 --- a/tests/test_payload_too_large.py +++ b/tests/test_payload_too_large.py @@ -1,49 +1,45 @@ -from sanic import Sanic from sanic.exceptions import PayloadTooLarge from sanic.response import text -def test_payload_too_large_from_error_handler(): - data_received_app = Sanic('data_received') - data_received_app.config.REQUEST_MAX_SIZE = 1 +def test_payload_too_large_from_error_handler(app): + app.config.REQUEST_MAX_SIZE = 1 - @data_received_app.route('/1') + @app.route('/1') async def handler1(request): return text('OK') - @data_received_app.exception(PayloadTooLarge) + @app.exception(PayloadTooLarge) def handler_exception(request, exception): return text('Payload Too Large from error_handler.', 413) - response = data_received_app.test_client.get('/1', gather_request=False) + response = app.test_client.get('/1', gather_request=False) assert response.status == 413 assert response.text == 'Payload Too Large from error_handler.' -def test_payload_too_large_at_data_received_default(): - data_received_default_app = Sanic('data_received_default') - data_received_default_app.config.REQUEST_MAX_SIZE = 1 +def test_payload_too_large_at_data_received_default(app): + app.config.REQUEST_MAX_SIZE = 1 - @data_received_default_app.route('/1') + @app.route('/1') async def handler2(request): return text('OK') - response = data_received_default_app.test_client.get( + response = app.test_client.get( '/1', gather_request=False) assert response.status == 413 assert response.text == 'Error: Payload Too Large' -def test_payload_too_large_at_on_header_default(): - on_header_default_app = Sanic('on_header') - on_header_default_app.config.REQUEST_MAX_SIZE = 500 +def test_payload_too_large_at_on_header_default(app): + app.config.REQUEST_MAX_SIZE = 500 - @on_header_default_app.post('/1') + @app.post('/1') async def handler3(request): return text('OK') data = 'a' * 1000 - response = on_header_default_app.test_client.post( + response = app.test_client.post( '/1', gather_request=False, data=data) assert response.status == 413 assert response.text == 'Error: Payload Too Large' diff --git a/tests/test_redirect.py b/tests/test_redirect.py index f5b734e3..f5efac60 100644 --- a/tests/test_redirect.py +++ b/tests/test_redirect.py @@ -1,12 +1,10 @@ import pytest -from sanic import Sanic from sanic.response import text, redirect @pytest.fixture -def redirect_app(): - app = Sanic('test_redirection') +def redirect_app(app): @app.route('/redirect_init') async def redirect_init(request): @@ -32,6 +30,10 @@ def redirect_app(): def handler(request): return text('OK') + @app.route('/redirect_with_header_injection') + async def redirect_with_header_injection(request): + return redirect("/unsafe\ntest-header: test-value\n\ntest-body") + return app @@ -92,3 +94,16 @@ def test_chained_redirect(redirect_app): assert response.url.endswith('/3') except AttributeError: assert response.url.path.endswith('/3') + + +def test_redirect_with_header_injection(redirect_app): + """ + Test redirection to a URL with header and body injections. + """ + request, response = redirect_app.test_client.get( + "/redirect_with_header_injection", + allow_redirects=False) + + assert response.status == 302 + assert "test-header" not in response.headers + assert not response.text.startswith('test-body') diff --git a/tests/test_request_data.py b/tests/test_request_data.py index f795ff1f..69935fc0 100644 --- a/tests/test_request_data.py +++ b/tests/test_request_data.py @@ -1,6 +1,5 @@ import random -from sanic import Sanic from sanic.response import json try: @@ -9,8 +8,7 @@ except ImportError: from json import loads -def test_storage(): - app = Sanic('test_text') +def test_storage(app): @app.middleware('request') def store(request): @@ -29,8 +27,7 @@ def test_storage(): assert response_json.get('sidekick') is None -def test_app_injection(): - app = Sanic('test_app_injection') +def test_app_injection(app): expected = random.choice(range(0, 100)) @app.listener('after_server_start') diff --git a/tests/test_request_stream.py b/tests/test_request_stream.py index 4ca4e44e..97cd5a4a 100644 --- a/tests/test_request_stream.py +++ b/tests/test_request_stream.py @@ -1,5 +1,4 @@ import asyncio -from sanic import Sanic from sanic.blueprints import Blueprint from sanic.views import CompositionView from sanic.views import HTTPMethodView @@ -9,11 +8,9 @@ from sanic.response import stream, text data = "abc" * 100000 -def test_request_stream_method_view(): +def test_request_stream_method_view(app): '''for self.is_request_stream = True''' - app = Sanic('test_request_stream_method_view') - class SimpleView(HTTPMethodView): def get(self, request): @@ -44,11 +41,9 @@ def test_request_stream_method_view(): assert response.text == data -def test_request_stream_app(): +def test_request_stream_app(app): '''for self.is_request_stream = True and decorators''' - app = Sanic('test_request_stream_app') - @app.get('/get') async def get(request): assert request.stream is None @@ -83,7 +78,7 @@ def test_request_stream_app(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) @app.put('/_put') @@ -100,7 +95,7 @@ def test_request_stream_app(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) @app.patch('/_patch') @@ -117,7 +112,7 @@ def test_request_stream_app(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) assert app.is_request_stream is True @@ -163,11 +158,9 @@ def test_request_stream_app(): assert response.text == data -def test_request_stream_handle_exception(): +def test_request_stream_handle_exception(app): '''for handling exceptions properly''' - app = Sanic('test_request_stream_exception') - @app.post('/post/', stream=True) async def post(request, id): assert isinstance(request.stream, asyncio.Queue) @@ -177,7 +170,7 @@ def test_request_stream_handle_exception(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) # 404 @@ -191,10 +184,8 @@ def test_request_stream_handle_exception(): assert response.text == 'Error: Method GET not allowed for URL /post/random_id' -def test_request_stream_blueprint(): +def test_request_stream_blueprint(app): '''for self.is_request_stream = True''' - - app = Sanic('test_request_stream_blueprint') bp = Blueprint('test_blueprint_request_stream_blueprint') @app.get('/get') @@ -231,7 +222,7 @@ def test_request_stream_blueprint(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) @bp.put('/_put') @@ -248,7 +239,7 @@ def test_request_stream_blueprint(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) @bp.patch('/_patch') @@ -265,7 +256,7 @@ def test_request_stream_blueprint(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) app.blueprint(bp) @@ -313,11 +304,9 @@ def test_request_stream_blueprint(): assert response.text == data -def test_request_stream_composition_view(): +def test_request_stream_composition_view(app): '''for self.is_request_stream = True''' - app = Sanic('test_request_stream__composition_view') - def get_handler(request): assert request.stream is None return text('OK') @@ -348,11 +337,9 @@ def test_request_stream_composition_view(): assert response.text == data -def test_request_stream(): +def test_request_stream(app): '''test for complex application''' - bp = Blueprint('test_blueprint_request_stream') - app = Sanic('test_request_stream') class SimpleView(HTTPMethodView): @@ -380,7 +367,7 @@ def test_request_stream(): body = await request.stream.get() if body is None: break - response.write(body.decode('utf-8')) + await response.write(body.decode('utf-8')) return stream(streaming) @app.get('/get') diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index b3eb78aa..672d0588 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -5,9 +5,24 @@ import asyncio from sanic.response import text from sanic.config import Config import aiohttp -from aiohttp import TCPConnector +from aiohttp import TCPConnector, ClientResponse from sanic.testing import SanicTestClient, HOST, PORT +try: + try: + import packaging # direct use + except ImportError: + # setuptools v39.0 and above. + try: + from setuptools.extern import packaging + except ImportError: + # Before setuptools v39.0 + from pkg_resources.extern import packaging + version = packaging.version +except ImportError: + raise RuntimeError("The 'packaging' library is missing.") + +aiohttp_version = version.parse(aiohttp.__version__) class DelayableTCPConnector(TCPConnector): @@ -38,8 +53,11 @@ class DelayableTCPConnector(TCPConnector): self.orig_start = getattr(resp, 'start') try: - ret = await self.orig_start(connection, - read_until_eof) + if aiohttp_version >= version.parse("3.3.0"): + ret = await self.orig_start(connection) + else: + ret = await self.orig_start(connection, + read_until_eof) except Exception as e: raise e return ret @@ -57,15 +75,31 @@ class DelayableTCPConnector(TCPConnector): await asyncio.sleep(self.delay) t = req.loop.time() print("sending at {}".format(t), flush=True) - conn = next(iter(args)) # first arg is connection - if aiohttp.__version__ >= "3.1.0": + conn = next(iter(args)) # first arg is connection + + if aiohttp_version >= version.parse("3.1.0"): try: delayed_resp = await self.orig_send(*args, **kwargs) except Exception as e: - return aiohttp.ClientResponse(req.method, req.url, - writer=None, continue100=None, timer=None, - request_info=None, auto_decompress=None, traces=[], - loop=req.loop, session=None) + if aiohttp_version >= version.parse("3.3.0"): + return aiohttp.ClientResponse(req.method, req.url, + writer=None, + continue100=None, + timer=None, + request_info=None, + traces=[], + loop=req.loop, + session=None) + else: + return aiohttp.ClientResponse(req.method, req.url, + writer=None, + continue100=None, + timer=None, + request_info=None, + auto_decompress=None, + traces=[], + loop=req.loop, + session=None) else: try: delayed_resp = self.orig_send(*args, **kwargs) @@ -73,7 +107,7 @@ class DelayableTCPConnector(TCPConnector): return aiohttp.ClientResponse(req.method, req.url) return delayed_resp - if aiohttp.__version__ >= "3.1.0": + if aiohttp_version >= version.parse("3.1.0"): # aiohttp changed the request.send method to async async def send(self, *args, **kwargs): gen = self.delayed_send(*args, **kwargs) @@ -96,12 +130,25 @@ class DelayableTCPConnector(TCPConnector): self._post_connect_delay = _post_connect_delay self._pre_request_delay = _pre_request_delay - if aiohttp.__version__ >= '3.0': - + if aiohttp_version >= version.parse("3.3.0"): + async def connect(self, req, traces, timeout): + d_req = DelayableTCPConnector.\ + RequestContextManager(req, self._pre_request_delay) + conn = await super(DelayableTCPConnector, self).\ + connect(req, traces, timeout) + if self._post_connect_delay and self._post_connect_delay > 0: + await asyncio.sleep(self._post_connect_delay, + loop=self._loop) + req.send = d_req.send + t = req.loop.time() + print("Connected at {}".format(t), flush=True) + return conn + elif aiohttp_version >= version.parse("3.0.0"): async def connect(self, req, traces=None): d_req = DelayableTCPConnector.\ RequestContextManager(req, self._pre_request_delay) - conn = await super(DelayableTCPConnector, self).connect(req, traces=traces) + conn = await super(DelayableTCPConnector, self).\ + connect(req, traces=traces) if self._post_connect_delay and self._post_connect_delay > 0: await asyncio.sleep(self._post_connect_delay, loop=self._loop) diff --git a/tests/test_requests.py b/tests/test_requests.py index 2a91fb9b..9617216e 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -5,7 +5,6 @@ import ssl import pytest -from sanic import Sanic from sanic.exceptions import ServerError from sanic.response import json, text from sanic.request import DEFAULT_HTTP_CONTENT_TYPE @@ -16,8 +15,7 @@ from sanic.testing import HOST, PORT # GET # ------------------------------------------------------------ # -def test_sync(): - app = Sanic('test_text') +def test_sync(app): @app.route('/') def handler(request): @@ -27,8 +25,8 @@ def test_sync(): assert response.text == 'Hello' -def test_remote_address(): - app = Sanic('test_text') + +def test_remote_address(app): @app.route('/') def handler(request): @@ -38,8 +36,8 @@ def test_remote_address(): assert response.text == '127.0.0.1' -def test_text(): - app = Sanic('test_text') + +def test_text(app): @app.route('/') async def handler(request): @@ -50,8 +48,7 @@ def test_text(): assert response.text == 'Hello' -def test_headers(): - app = Sanic('test_text') +def test_headers(app): @app.route('/') async def handler(request): @@ -63,8 +60,7 @@ def test_headers(): assert response.headers.get('spam') == 'great' -def test_non_str_headers(): - app = Sanic('test_text') +def test_non_str_headers(app): @app.route('/') async def handler(request): @@ -75,8 +71,8 @@ def test_non_str_headers(): assert response.headers.get('answer') == '42' -def test_invalid_response(): - app = Sanic('test_invalid_response') + +def test_invalid_response(app): @app.exception(ServerError) def handler_exception(request, exception): @@ -91,8 +87,7 @@ def test_invalid_response(): assert response.text == "Internal Server Error." -def test_json(): - app = Sanic('test_json') +def test_json(app): @app.route('/') async def handler(request): @@ -105,8 +100,7 @@ def test_json(): assert results.get('test') == True -def test_empty_json(): - app = Sanic('test_json') +def test_empty_json(app): @app.route('/') async def handler(request): @@ -118,8 +112,7 @@ def test_empty_json(): assert response.text == 'null' -def test_invalid_json(): - app = Sanic('test_json') +def test_invalid_json(app): @app.route('/') async def handler(request): @@ -131,8 +124,7 @@ def test_invalid_json(): assert response.status == 400 -def test_query_string(): - app = Sanic('test_query_string') +def test_query_string(app): @app.route('/') async def handler(request): @@ -145,8 +137,7 @@ def test_query_string(): assert request.args.get('test2') == 'false' -def test_uri_template(): - app = Sanic('test_uri_template') +def test_uri_template(app): @app.route('/foo//bar/') async def handler(request): @@ -156,8 +147,7 @@ def test_uri_template(): assert request.uri_template == '/foo//bar/' -def test_token(): - app = Sanic('test_post_token') +def test_token(app): @app.route('/') async def handler(request): @@ -204,8 +194,7 @@ def test_token(): assert request.token is None -def test_content_type(): - app = Sanic('test_content_type') +def test_content_type(app): @app.route('/') async def handler(request): @@ -223,8 +212,7 @@ def test_content_type(): assert response.text == 'application/json' -def test_remote_addr(): - app = Sanic('test_content_type') +def test_remote_addr(app): @app.route('/') async def handler(request): @@ -249,8 +237,7 @@ def test_remote_addr(): assert response.text == '127.0.0.1' -def test_match_info(): - app = Sanic('test_match_info') +def test_match_info(app): @app.route('/api/v1/user//') async def handler(request, user_id): @@ -266,8 +253,7 @@ def test_match_info(): # POST # ------------------------------------------------------------ # -def test_post_json(): - app = Sanic('test_post_json') +def test_post_json(app): @app.route('/', methods=['POST']) async def handler(request): @@ -283,8 +269,7 @@ def test_post_json(): assert response.text == 'OK' -def test_post_form_urlencoded(): - app = Sanic('test_post_form_urlencoded') +def test_post_form_urlencoded(app): @app.route('/', methods=['POST']) async def handler(request): @@ -311,8 +296,7 @@ def test_post_form_urlencoded(): 'OK\r\n' \ '------sanic--\r\n', ]) -def test_post_form_multipart_form_data(payload): - app = Sanic('test_post_form_multipart_form_data') +def test_post_form_multipart_form_data(app, payload): @app.route('/', methods=['POST']) async def handler(request): @@ -331,8 +315,7 @@ def test_post_form_multipart_form_data(payload): ('/bar/baz', '', 'http://{}:{}/bar/baz'), ('/moo/boo', 'arg1=val1', 'http://{}:{}/moo/boo?arg1=val1') ]) -def test_url_attributes_no_ssl(path, query, expected_url): - app = Sanic('test_url_attrs_no_ssl') +def test_url_attributes_no_ssl(app, path, query, expected_url): async def handler(request): return text('OK') @@ -356,9 +339,7 @@ def test_url_attributes_no_ssl(path, query, expected_url): ('/bar/baz', '', 'https://{}:{}/bar/baz'), ('/moo/boo', 'arg1=val1', 'https://{}:{}/moo/boo?arg1=val1') ]) -def test_url_attributes_with_ssl(path, query, expected_url): - app = Sanic('test_url_attrs_with_ssl') - +def test_url_attributes_with_ssl(app, path, query, expected_url): current_dir = os.path.dirname(os.path.realpath(__file__)) context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) context.load_cert_chain( diff --git a/tests/test_response.py b/tests/test_response.py index 12049460..78f9f103 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -8,17 +8,16 @@ from urllib.parse import unquote import pytest from random import choice -from sanic import Sanic from sanic.response import HTTPResponse, stream, StreamingHTTPResponse, file, file_stream, json +from sanic.server import HttpProtocol from sanic.testing import HOST, PORT from unittest.mock import MagicMock JSON_DATA = {'ok': True} -def test_response_body_not_a_string(): +def test_response_body_not_a_string(app): """Test when a response body sent from the application is not a string""" - app = Sanic('response_body_not_a_string') random_num = choice(range(1000)) @app.route('/hello') @@ -30,13 +29,12 @@ def test_response_body_not_a_string(): async def sample_streaming_fn(response): - response.write('foo,') + await response.write('foo,') await asyncio.sleep(.001) - response.write('bar') + await response.write('bar') -def test_method_not_allowed(): - app = Sanic('method_not_allowed') +def test_method_not_allowed(app): @app.get('/') async def test(request): @@ -64,9 +62,27 @@ def test_method_not_allowed(): assert response.headers['Content-Length'] == '0' +def test_response_header(app): + + @app.get('/') + async def test(request): + return json({ + "ok": True + }, headers={ + 'CONTENT-TYPE': 'application/json' + }) + + request, response = app.test_client.get('/') + assert dict(response.headers) == { + 'Connection': 'keep-alive', + 'Keep-Alive': '2', + 'Content-Length': '11', + 'Content-Type': 'application/json', + } + + @pytest.fixture -def json_app(): - app = Sanic('json') +def json_app(app): @app.route("/") async def test(request): @@ -124,8 +140,7 @@ def test_no_content(json_app): @pytest.fixture -def streaming_app(): - app = Sanic('streaming') +def streaming_app(app): @app.route("/") async def test(request): @@ -170,20 +185,30 @@ def test_stream_response_includes_chunked_header(): def test_stream_response_writes_correct_content_to_transport(streaming_app): response = StreamingHTTPResponse(sample_streaming_fn) - response.transport = MagicMock(asyncio.Transport) + response.protocol = MagicMock(HttpProtocol) + response.protocol.transport = MagicMock(asyncio.Transport) + + async def mock_drain(): + pass + + def mock_push_data(data): + response.protocol.transport.write(data) + + response.protocol.push_data = mock_push_data + response.protocol.drain = mock_drain @streaming_app.listener('after_server_start') async def run_stream(app, loop): await response.stream() - assert response.transport.write.call_args_list[1][0][0] == ( + assert response.protocol.transport.write.call_args_list[1][0][0] == ( b'4\r\nfoo,\r\n' ) - assert response.transport.write.call_args_list[2][0][0] == ( + assert response.protocol.transport.write.call_args_list[2][0][0] == ( b'3\r\nbar\r\n' ) - assert response.transport.write.call_args_list[3][0][0] == ( + assert response.protocol.transport.write.call_args_list[3][0][0] == ( b'0\r\n\r\n' ) @@ -208,25 +233,25 @@ def get_file_content(static_file_directory, file_name): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) -def test_file_response(file_name, static_file_directory): - app = Sanic('test_file_helper') +@pytest.mark.parametrize('status', [200, 401]) +def test_file_response(app, file_name, static_file_directory, status): @app.route('/files/', methods=['GET']) def file_route(request, filename): file_path = os.path.join(static_file_directory, filename) file_path = os.path.abspath(unquote(file_path)) - return file(file_path, mime_type=guess_type(file_path)[0] or 'text/plain') + return file(file_path, status=status, + mime_type=guess_type(file_path)[0] or 'text/plain') request, response = app.test_client.get('/files/{}'.format(file_name)) - assert response.status == 200 + assert response.status == status assert response.body == get_file_content(static_file_directory, file_name) assert 'Content-Disposition' not in response.headers @pytest.mark.parametrize('source,dest', [ ('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')]) -def test_file_response_custom_filename(source, dest, static_file_directory): - app = Sanic('test_file_helper') +def test_file_response_custom_filename(app, source, dest, static_file_directory): @app.route('/files/', methods=['GET']) def file_route(request, filename): @@ -241,8 +266,7 @@ def test_file_response_custom_filename(source, dest, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_file_head_response(file_name, static_file_directory): - app = Sanic('test_file_helper') +def test_file_head_response(app, file_name, static_file_directory): @app.route('/files/', methods=['GET', 'HEAD']) async def file_route(request, filename): @@ -270,8 +294,7 @@ def test_file_head_response(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) -def test_file_stream_response(file_name, static_file_directory): - app = Sanic('test_file_helper') +def test_file_stream_response(app, file_name, static_file_directory): @app.route('/files/', methods=['GET']) def file_route(request, filename): @@ -288,8 +311,7 @@ def test_file_stream_response(file_name, static_file_directory): @pytest.mark.parametrize('source,dest', [ ('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')]) -def test_file_stream_response_custom_filename(source, dest, static_file_directory): - app = Sanic('test_file_helper') +def test_file_stream_response_custom_filename(app, source, dest, static_file_directory): @app.route('/files/', methods=['GET']) def file_route(request, filename): @@ -304,8 +326,7 @@ def test_file_stream_response_custom_filename(source, dest, static_file_director @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_file_stream_head_response(file_name, static_file_directory): - app = Sanic('test_file_helper') +def test_file_stream_head_response(app, file_name, static_file_directory): @app.route('/files/', methods=['GET', 'HEAD']) async def file_route(request, filename): diff --git a/tests/test_response_timeout.py b/tests/test_response_timeout.py index bf55a42e..44204b5e 100644 --- a/tests/test_response_timeout.py +++ b/tests/test_response_timeout.py @@ -7,6 +7,7 @@ from sanic.config import Config Config.RESPONSE_TIMEOUT = 1 response_timeout_app = Sanic('test_response_timeout') response_timeout_default_app = Sanic('test_response_timeout_default') +response_handler_cancelled_app = Sanic('test_response_handler_cancelled') @response_timeout_app.route('/1') @@ -36,3 +37,29 @@ def test_default_server_error_response_timeout(): request, response = response_timeout_default_app.test_client.get('/1') assert response.status == 503 assert response.text == 'Error: Response Timeout' + + +response_handler_cancelled_app.flag = False + + +@response_handler_cancelled_app.exception(asyncio.CancelledError) +def handler_cancelled(request, exception): + # If we get a CancelledError, it means sanic has already sent a response, + # we should not ever have to handle a CancelledError. + response_handler_cancelled_app.flag = True + return text("App received CancelledError!", 500) + # The client will never receive this response, because the socket + # is already closed when we get a CancelledError. + + +@response_handler_cancelled_app.route('/1') +async def handler_3(request): + await asyncio.sleep(2) + return text('OK') + + +def test_response_handler_cancelled(): + request, response = response_handler_cancelled_app.test_client.get('/1') + assert response.status == 503 + assert response.text == 'Error: Response Timeout' + assert response_handler_cancelled_app.flag is False diff --git a/tests/test_routes.py b/tests/test_routes.py index 146db97c..d70bf975 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -12,9 +12,7 @@ from sanic.constants import HTTP_METHODS # ------------------------------------------------------------ # @pytest.mark.parametrize('method', HTTP_METHODS) -def test_versioned_routes_get(method): - app = Sanic('test_shorhand_routes_get') - +def test_versioned_routes_get(app, method): method = method.lower() func = getattr(app, method) @@ -32,8 +30,7 @@ def test_versioned_routes_get(method): assert response.status == 200 -def test_shorthand_routes_get(): - app = Sanic('test_shorhand_routes_get') +def test_shorthand_routes_get(app): @app.get('/get') def handler(request): @@ -46,8 +43,7 @@ def test_shorthand_routes_get(): assert response.status == 405 -def test_shorthand_routes_multiple(): - app = Sanic('test_shorthand_routes_multiple') +def test_shorthand_routes_multiple(app): @app.get('/get') def get_handler(request): @@ -65,8 +61,7 @@ def test_shorthand_routes_multiple(): assert response.status == 200 -def test_route_strict_slash(): - app = Sanic('test_route_strict_slash') +def test_route_strict_slash(app): @app.get('/get', strict_slashes=True) def handler(request): @@ -93,9 +88,8 @@ def test_route_strict_slash(): assert response.status == 404 -def test_route_invalid_parameter_syntax(): +def test_route_invalid_parameter_syntax(app): with pytest.raises(ValueError): - app = Sanic('test_route_invalid_param_syntax') @app.get('/get/<:string>', strict_slashes=True) def handler(request): @@ -115,8 +109,7 @@ def test_route_strict_slash_default_value(): assert response.status == 404 -def test_route_strict_slash_without_passing_default_value(): - app = Sanic('test_route_strict_slash') +def test_route_strict_slash_without_passing_default_value(app): @app.get('/get') def handler(request): @@ -137,8 +130,7 @@ def test_route_strict_slash_default_value_can_be_overwritten(): assert response.text == 'OK' -def test_route_slashes_overload(): - app = Sanic('test_route_slashes_overload') +def test_route_slashes_overload(app): @app.get('/hello/') def handler(request): @@ -161,8 +153,7 @@ def test_route_slashes_overload(): assert response.text == 'OK' -def test_route_optional_slash(): - app = Sanic('test_route_optional_slash') +def test_route_optional_slash(app): @app.get('/get') def handler(request): @@ -174,9 +165,8 @@ def test_route_optional_slash(): request, response = app.test_client.get('/get/') assert response.text == 'OK' -def test_route_strict_slashes_set_to_false_and_host_is_a_list(): +def test_route_strict_slashes_set_to_false_and_host_is_a_list(app): #Part of regression test for issue #1120 - app = Sanic('test_route_strict_slashes_set_to_false_and_host_is_a_list') site1 = 'localhost:{}'.format(app.test_client.port) @@ -209,8 +199,7 @@ def test_route_strict_slashes_set_to_false_and_host_is_a_list(): request, response = app.test_client.delete('http://' + site1 +'/delete') assert response.text == 'OK' -def test_shorthand_routes_post(): - app = Sanic('test_shorhand_routes_post') +def test_shorthand_routes_post(app): @app.post('/post') def handler(request): @@ -223,8 +212,7 @@ def test_shorthand_routes_post(): assert response.status == 405 -def test_shorthand_routes_put(): - app = Sanic('test_shorhand_routes_put') +def test_shorthand_routes_put(app): @app.put('/put') def handler(request): @@ -240,8 +228,7 @@ def test_shorthand_routes_put(): assert response.status == 405 -def test_shorthand_routes_delete(): - app = Sanic('test_shorhand_routes_delete') +def test_shorthand_routes_delete(app): @app.delete('/delete') def handler(request): @@ -257,8 +244,7 @@ def test_shorthand_routes_delete(): assert response.status == 405 -def test_shorthand_routes_patch(): - app = Sanic('test_shorhand_routes_patch') +def test_shorthand_routes_patch(app): @app.patch('/patch') def handler(request): @@ -274,8 +260,7 @@ def test_shorthand_routes_patch(): assert response.status == 405 -def test_shorthand_routes_head(): - app = Sanic('test_shorhand_routes_head') +def test_shorthand_routes_head(app): @app.head('/head') def handler(request): @@ -291,8 +276,7 @@ def test_shorthand_routes_head(): assert response.status == 405 -def test_shorthand_routes_options(): - app = Sanic('test_shorhand_routes_options') +def test_shorthand_routes_options(app): @app.options('/options') def handler(request): @@ -308,8 +292,7 @@ def test_shorthand_routes_options(): assert response.status == 405 -def test_static_routes(): - app = Sanic('test_dynamic_route') +def test_static_routes(app): @app.route('/test') async def handler1(request): @@ -326,9 +309,7 @@ def test_static_routes(): assert response.text == 'OK2' -def test_dynamic_route(): - app = Sanic('test_dynamic_route') - +def test_dynamic_route(app): results = [] @app.route('/folder/') @@ -342,9 +323,7 @@ def test_dynamic_route(): assert results[0] == 'test123' -def test_dynamic_route_string(): - app = Sanic('test_dynamic_route_string') - +def test_dynamic_route_string(app): results = [] @app.route('/folder/') @@ -363,9 +342,7 @@ def test_dynamic_route_string(): assert results[1] == 'favicon.ico' -def test_dynamic_route_int(): - app = Sanic('test_dynamic_route_int') - +def test_dynamic_route_int(app): results = [] @app.route('/folder/') @@ -381,9 +358,7 @@ def test_dynamic_route_int(): assert response.status == 404 -def test_dynamic_route_number(): - app = Sanic('test_dynamic_route_number') - +def test_dynamic_route_number(app): results = [] @app.route('/weight/') @@ -402,8 +377,7 @@ def test_dynamic_route_number(): assert response.status == 404 -def test_dynamic_route_regex(): - app = Sanic('test_dynamic_route_regex') +def test_dynamic_route_regex(app): @app.route('/folder/') async def handler(request, folder_id): @@ -422,9 +396,8 @@ def test_dynamic_route_regex(): assert response.status == 200 -def test_dynamic_route_uuid(): +def test_dynamic_route_uuid(app): import uuid - app = Sanic('test_dynamic_route_uuid') results = [] @@ -444,8 +417,7 @@ def test_dynamic_route_uuid(): assert response.status == 404 -def test_dynamic_route_path(): - app = Sanic('test_dynamic_route_path') +def test_dynamic_route_path(app): @app.route('//info') async def handler(request, path): @@ -468,8 +440,7 @@ def test_dynamic_route_path(): assert response.status == 200 -def test_dynamic_route_unhashable(): - app = Sanic('test_dynamic_route_unhashable') +def test_dynamic_route_unhashable(app): @app.route('/folder//end/') async def handler(request, unhashable): @@ -488,8 +459,7 @@ def test_dynamic_route_unhashable(): assert response.status == 404 -def test_websocket_route(): - app = Sanic('test_websocket_route') +def test_websocket_route(app): ev = asyncio.Event() @app.websocket('/ws') @@ -506,8 +476,7 @@ def test_websocket_route(): assert ev.is_set() -def test_websocket_route_with_subprotocols(): - app = Sanic('test_websocket_route') +def test_websocket_route_with_subprotocols(app): results = [] @app.websocket('/ws', subprotocols=['foo', 'bar']) @@ -548,8 +517,7 @@ def test_websocket_route_with_subprotocols(): assert results == ['bar', 'bar', None, None] -def test_route_duplicate(): - app = Sanic('test_route_duplicate') +def test_route_duplicate(app): with pytest.raises(RouteExists): @app.route('/test') @@ -570,8 +538,7 @@ def test_route_duplicate(): pass -def test_method_not_allowed(): - app = Sanic('test_method_not_allowed') +def test_method_not_allowed(app): @app.route('/test', methods=['GET']) async def handler(request): @@ -584,8 +551,7 @@ def test_method_not_allowed(): assert response.status == 405 -def test_static_add_route(): - app = Sanic('test_static_add_route') +def test_static_add_route(app): async def handler1(request): return text('OK1') @@ -603,8 +569,7 @@ def test_static_add_route(): assert response.text == 'OK2' -def test_dynamic_add_route(): - app = Sanic('test_dynamic_add_route') +def test_dynamic_add_route(app): results = [] @@ -619,8 +584,7 @@ def test_dynamic_add_route(): assert results[0] == 'test123' -def test_dynamic_add_route_string(): - app = Sanic('test_dynamic_add_route_string') +def test_dynamic_add_route_string(app): results = [] @@ -640,9 +604,7 @@ def test_dynamic_add_route_string(): assert results[1] == 'favicon.ico' -def test_dynamic_add_route_int(): - app = Sanic('test_dynamic_add_route_int') - +def test_dynamic_add_route_int(app): results = [] async def handler(request, folder_id): @@ -659,9 +621,7 @@ def test_dynamic_add_route_int(): assert response.status == 404 -def test_dynamic_add_route_number(): - app = Sanic('test_dynamic_add_route_number') - +def test_dynamic_add_route_number(app): results = [] async def handler(request, weight): @@ -681,8 +641,7 @@ def test_dynamic_add_route_number(): assert response.status == 404 -def test_dynamic_add_route_regex(): - app = Sanic('test_dynamic_route_int') +def test_dynamic_add_route_regex(app): async def handler(request, folder_id): return text('OK') @@ -702,8 +661,7 @@ def test_dynamic_add_route_regex(): assert response.status == 200 -def test_dynamic_add_route_unhashable(): - app = Sanic('test_dynamic_add_route_unhashable') +def test_dynamic_add_route_unhashable(app): async def handler(request, unhashable): return text('OK') @@ -723,8 +681,7 @@ def test_dynamic_add_route_unhashable(): assert response.status == 404 -def test_add_route_duplicate(): - app = Sanic('test_add_route_duplicate') +def test_add_route_duplicate(app): with pytest.raises(RouteExists): async def handler1(request): @@ -747,8 +704,7 @@ def test_add_route_duplicate(): app.add_route(handler2, '/test//') -def test_add_route_method_not_allowed(): - app = Sanic('test_add_route_method_not_allowed') +def test_add_route_method_not_allowed(app): async def handler(request): return text('OK') @@ -762,8 +718,7 @@ def test_add_route_method_not_allowed(): assert response.status == 405 -def test_remove_static_route(): - app = Sanic('test_remove_static_route') +def test_remove_static_route(app): async def handler1(request): return text('OK1') @@ -790,8 +745,7 @@ def test_remove_static_route(): assert response.status == 404 -def test_remove_dynamic_route(): - app = Sanic('test_remove_dynamic_route') +def test_remove_dynamic_route(app): async def handler(request, name): return text('OK') @@ -806,15 +760,13 @@ def test_remove_dynamic_route(): assert response.status == 404 -def test_remove_inexistent_route(): - app = Sanic('test_remove_inexistent_route') +def test_remove_inexistent_route(app): with pytest.raises(RouteDoesNotExist): app.remove_route('/test') -def test_removing_slash(): - app = Sanic(__name__) +def test_removing_slash(app): @app.get('/rest/') def get(_): @@ -827,8 +779,7 @@ def test_removing_slash(): assert len(app.router.routes_all.keys()) == 2 -def test_remove_unhashable_route(): - app = Sanic('test_remove_unhashable_route') +def test_remove_unhashable_route(app): async def handler(request, unhashable): return text('OK') @@ -856,8 +807,7 @@ def test_remove_unhashable_route(): assert response.status == 404 -def test_remove_route_without_clean_cache(): - app = Sanic('test_remove_static_route') +def test_remove_route_without_clean_cache(app): async def handler(request): return text('OK') @@ -884,8 +834,7 @@ def test_remove_route_without_clean_cache(): assert response.status == 200 -def test_overload_routes(): - app = Sanic('test_dynamic_route') +def test_overload_routes(app): @app.route('/overload', methods=['GET']) async def handler1(request): @@ -913,8 +862,7 @@ def test_overload_routes(): return text('Duplicated') -def test_unmergeable_overload_routes(): - app = Sanic('test_dynamic_route') +def test_unmergeable_overload_routes(app): @app.route('/overload_whole', methods=None) async def handler1(request): @@ -947,8 +895,7 @@ def test_unmergeable_overload_routes(): assert response.status == 405 -def test_unicode_routes(): - app = Sanic('test_unicode_routes') +def test_unicode_routes(app): @app.get('/你好') def handler1(request): @@ -965,8 +912,7 @@ def test_unicode_routes(): assert response.text == 'OK2 你好' -def test_uri_with_different_method_and_different_params(): - app = Sanic('test_uri') +def test_uri_with_different_method_and_different_params(app): @app.route('/ads/', methods=['GET']) async def ad_get(request, ad_id): diff --git a/tests/test_server_events.py b/tests/test_server_events.py index c2ccc7dc..68e097eb 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -1,11 +1,7 @@ -from io import StringIO -from random import choice -from string import ascii_letters import signal import pytest -from sanic import Sanic from sanic.testing import HOST, PORT AVAILABLE_LISTENERS = [ @@ -37,54 +33,46 @@ def start_stop_app(random_name_app, **run_kwargs): @pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS) -def test_single_listener(listener_name): +def test_single_listener(app, listener_name): """Test that listeners on their own work""" - random_name_app = Sanic(''.join( - [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) - output = list() + output = [] # Register listener - random_name_app.listener(listener_name)( + app.listener(listener_name)( create_listener(listener_name, output)) - start_stop_app(random_name_app) - assert random_name_app.name + listener_name == output.pop() + start_stop_app(app) + assert app.name + listener_name == output.pop() @pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS) -def test_register_listener(listener_name): +def test_register_listener(app, listener_name): """ Test that listeners on their own work with app.register_listener method """ - random_name_app = Sanic(''.join( - [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) - output = list() + output = [] # Register listener listener = create_listener(listener_name, output) - random_name_app.register_listener(listener, + app.register_listener(listener, event=listener_name) - start_stop_app(random_name_app) - assert random_name_app.name + listener_name == output.pop() + start_stop_app(app) + assert app.name + listener_name == output.pop() -def test_all_listeners(): - random_name_app = Sanic(''.join( - [choice(ascii_letters) for _ in range(choice(range(5, 10)))])) - output = list() +def test_all_listeners(app): + output = [] for listener_name in AVAILABLE_LISTENERS: listener = create_listener(listener_name, output) - random_name_app.listener(listener_name)(listener) - start_stop_app(random_name_app) + app.listener(listener_name)(listener) + start_stop_app(app) for listener_name in AVAILABLE_LISTENERS: - assert random_name_app.name + listener_name == output.pop() + assert app.name + listener_name == output.pop() -async def test_trigger_before_events_create_server(): +async def test_trigger_before_events_create_server(app): class MySanicDb: pass - app = Sanic("test_sanic_app") - @app.listener('before_server_start') async def init_db(app, loop): app.db = MySanicDb() diff --git a/tests/test_signal_handlers.py b/tests/test_signal_handlers.py index bee4f8e7..7c1327c2 100644 --- a/tests/test_signal_handlers.py +++ b/tests/test_signal_handlers.py @@ -1,4 +1,3 @@ -from sanic import Sanic from sanic.response import HTTPResponse from sanic.testing import HOST, PORT from unittest.mock import MagicMock @@ -18,9 +17,8 @@ def set_loop(app, loop): def after(app, loop): calledq.put(loop.add_signal_handler.called) -def test_register_system_signals(): +def test_register_system_signals(app): """Test if sanic register system signals""" - app = Sanic('test_register_system_signals') @app.route('/hello') async def hello_route(request): @@ -34,9 +32,8 @@ def test_register_system_signals(): assert calledq.get() == True -def test_dont_register_system_signals(): +def test_dont_register_system_signals(app): """Test if sanic don't register system signals""" - app = Sanic('test_register_system_signals') @app.route('/hello') async def hello_route(request): diff --git a/tests/test_static.py b/tests/test_static.py index 276001cc..e436aa74 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -3,8 +3,6 @@ import os import pytest -from sanic import Sanic - @pytest.fixture(scope='module') def static_file_directory(): @@ -26,8 +24,7 @@ def get_file_content(static_file_directory, file_name): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) -def test_static_file(static_file_directory, file_name): - app = Sanic('test_static') +def test_static_file(app, static_file_directory, file_name): app.static( '/testing.file', get_file_path(static_file_directory, file_name)) @@ -36,11 +33,23 @@ def test_static_file(static_file_directory, file_name): assert response.body == get_file_content(static_file_directory, file_name) +@pytest.mark.parametrize('file_name', ['test.html']) +def test_static_file_content_type(app, static_file_directory, file_name): + app.static( + '/testing.file', + get_file_path(static_file_directory, file_name), + content_type='text/html; charset=utf-8' + ) + + request, response = app.test_client.get('/testing.file') + assert response.status == 200 + assert response.body == get_file_content(static_file_directory, file_name) + assert response.headers['Content-Type'] == 'text/html; charset=utf-8' + + @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) @pytest.mark.parametrize('base_uri', ['/static', '', '/dir']) -def test_static_directory(file_name, base_uri, static_file_directory): - - app = Sanic('test_static') +def test_static_directory(app, file_name, base_uri, static_file_directory): app.static(base_uri, static_file_directory) request, response = app.test_client.get( @@ -50,8 +59,7 @@ def test_static_directory(file_name, base_uri, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_head_request(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_head_request(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -66,8 +74,7 @@ def test_static_head_request(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_correct(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_correct(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -87,8 +94,7 @@ def test_static_content_range_correct(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_front(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_front(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -108,8 +114,7 @@ def test_static_content_range_front(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_back(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_back(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -129,8 +134,7 @@ def test_static_content_range_back(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_empty(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_empty(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -146,8 +150,7 @@ def test_static_content_range_empty(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_error(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_error(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -164,8 +167,7 @@ def test_static_content_range_error(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) -def test_static_file_specified_host(static_file_directory, file_name): - app = Sanic('test_static') +def test_static_file_specified_host(app, static_file_directory, file_name): app.static( '/testing.file', get_file_path(static_file_directory, file_name), diff --git a/tests/test_url_building.py b/tests/test_url_building.py index 98bbc20a..1768f78f 100644 --- a/tests/test_url_building.py +++ b/tests/test_url_building.py @@ -1,7 +1,6 @@ import pytest as pytest from urllib.parse import urlsplit, parse_qsl -from sanic import Sanic from sanic.response import text from sanic.views import HTTPMethodView from sanic.blueprints import Blueprint @@ -30,8 +29,7 @@ def _generate_handlers_from_names(app, l): @pytest.fixture -def simple_app(): - app = Sanic('simple_app') +def simple_app(app): handler_names = list(string.ascii_letters) _generate_handlers_from_names(app, handler_names) @@ -54,8 +52,7 @@ def test_simple_url_for_getting(simple_app): (URL_FOR_ARGS2, URL_FOR_VALUE2), (URL_FOR_ARGS3, URL_FOR_VALUE3), (URL_FOR_ARGS4, URL_FOR_VALUE4)]) -def test_simple_url_for_getting_with_more_params(args, url): - app = Sanic('more_url_build') +def test_simple_url_for_getting_with_more_params(app, args, url): @app.route('/myurl') def passes(request): @@ -67,8 +64,7 @@ def test_simple_url_for_getting_with_more_params(args, url): assert response.text == 'this should pass' -def test_fails_if_endpoint_not_found(): - app = Sanic('fail_url_build') +def test_fails_if_endpoint_not_found(app): @app.route('/fail') def fail(request): @@ -80,14 +76,12 @@ def test_fails_if_endpoint_not_found(): assert str(e.value) == 'Endpoint with name `passes` was not found' -def test_fails_url_build_if_param_not_passed(): +def test_fails_url_build_if_param_not_passed(app): url = '/' for letter in string.ascii_letters: url += '<{}>/'.format(letter) - app = Sanic('fail_url_build') - @app.route(url) def fail(request): return text('this should fail') @@ -103,8 +97,7 @@ def test_fails_url_build_if_param_not_passed(): assert 'Required parameter `Z` was not passed to url_for' in str(e.value) -def test_fails_url_build_if_params_not_passed(): - app = Sanic('fail_url_build') +def test_fails_url_build_if_params_not_passed(app): @app.route('/fail') def fail(request): @@ -126,8 +119,7 @@ PASSING_KWARGS = { EXPECTED_BUILT_URL = '/4/woof/ba/normal/1.001' -def test_fails_with_int_message(): - app = Sanic('fail_url_build') +def test_fails_with_int_message(app): @app.route(COMPLEX_PARAM_URL) def fail(request): @@ -145,8 +137,7 @@ def test_fails_with_int_message(): assert str(e.value) == expected_error -def test_fails_with_two_letter_string_message(): - app = Sanic('fail_url_build') +def test_fails_with_two_letter_string_message(app): @app.route(COMPLEX_PARAM_URL) def fail(request): @@ -165,8 +156,7 @@ def test_fails_with_two_letter_string_message(): assert str(e.value) == expected_error -def test_fails_with_number_message(): - app = Sanic('fail_url_build') +def test_fails_with_number_message(app): @app.route(COMPLEX_PARAM_URL) def fail(request): @@ -185,8 +175,7 @@ def test_fails_with_number_message(): assert str(e.value) == expected_error -def test_adds_other_supplied_values_as_query_string(): - app = Sanic('passes') +def test_adds_other_supplied_values_as_query_string(app): @app.route(COMPLEX_PARAM_URL) def passes(request): @@ -205,8 +194,7 @@ def test_adds_other_supplied_values_as_query_string(): @pytest.fixture -def blueprint_app(): - app = Sanic('blueprints') +def blueprint_app(app): first_print = Blueprint('first', url_prefix='/first') second_print = Blueprint('second', url_prefix='/second') @@ -252,8 +240,7 @@ def test_blueprints_work_with_params(blueprint_app): @pytest.fixture -def methodview_app(): - app = Sanic('methodview') +def methodview_app(app): class ViewOne(HTTPMethodView): def get(self, request): diff --git a/tests/test_url_for_static.py b/tests/test_url_for_static.py index d1d8fc9b..2e802700 100644 --- a/tests/test_url_for_static.py +++ b/tests/test_url_for_static.py @@ -3,7 +3,6 @@ import os import pytest -from sanic import Sanic from sanic.blueprints import Blueprint @@ -27,8 +26,7 @@ def get_file_content(static_file_directory, file_name): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) -def test_static_file(static_file_directory, file_name): - app = Sanic('test_static') +def test_static_file(app, static_file_directory, file_name): app.static( '/testing.file', get_file_path(static_file_directory, file_name)) app.static( @@ -102,9 +100,7 @@ def test_static_file(static_file_directory, file_name): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) @pytest.mark.parametrize('base_uri', ['/static', '', '/dir']) -def test_static_directory(file_name, base_uri, static_file_directory): - - app = Sanic('test_static') +def test_static_directory(app, file_name, base_uri, static_file_directory): app.static(base_uri, static_file_directory) base_uri2 = base_uri + '/2' app.static(base_uri2, static_file_directory, name='uploads') @@ -156,10 +152,8 @@ def test_static_directory(file_name, base_uri, static_file_directory): assert response.body == get_file_content(static_file_directory, file_name) - @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_head_request(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_head_request(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -198,8 +192,7 @@ def test_static_head_request(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_correct(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_correct(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -250,8 +243,7 @@ def test_static_content_range_correct(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_front(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_front(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -302,8 +294,7 @@ def test_static_content_range_front(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_back(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_back(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -354,8 +345,7 @@ def test_static_content_range_back(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_empty(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_empty(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) @@ -401,8 +391,7 @@ def test_static_content_range_empty(file_name, static_file_directory): @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) -def test_static_content_range_error(file_name, static_file_directory): - app = Sanic('test_static') +def test_static_content_range_error(app, file_name, static_file_directory): app.static( '/testing.file', get_file_path(static_file_directory, file_name), use_content_range=True) diff --git a/tests/test_utf8.py b/tests/test_utf8.py index e1602958..4a8b9e73 100644 --- a/tests/test_utf8.py +++ b/tests/test_utf8.py @@ -1,14 +1,12 @@ -from json import loads as json_loads, dumps as json_dumps -from sanic import Sanic -from sanic.response import json, text +from json import dumps as json_dumps +from sanic.response import text # ------------------------------------------------------------ # # UTF-8 # ------------------------------------------------------------ # -def test_utf8_query_string(): - app = Sanic('test_utf8_query_string') +def test_utf8_query_string(app): @app.route('/') async def handler(request): @@ -18,8 +16,7 @@ def test_utf8_query_string(): assert request.args.get('utf8') == '✓' -def test_utf8_response(): - app = Sanic('test_utf8_response') +def test_utf8_response(app): @app.route('/') async def handler(request): @@ -29,8 +26,7 @@ def test_utf8_response(): assert response.text == '✓' -def skip_test_utf8_route(): - app = Sanic('skip_test_utf8_route') +def skip_test_utf8_route(app): @app.route('/') async def handler(request): @@ -41,8 +37,7 @@ def skip_test_utf8_route(): assert response.text == 'OK' -def test_utf8_post_json(): - app = Sanic('test_utf8_post_json') +def test_utf8_post_json(app): @app.route('/') async def handler(request): diff --git a/tests/test_vhosts.py b/tests/test_vhosts.py index 2b88bff3..0be537e6 100644 --- a/tests/test_vhosts.py +++ b/tests/test_vhosts.py @@ -1,9 +1,7 @@ -from sanic import Sanic -from sanic.response import json, text +from sanic.response import text -def test_vhosts(): - app = Sanic('test_vhosts') +def test_vhosts(app): @app.route('/', host="example.com") async def handler(request): @@ -22,8 +20,7 @@ def test_vhosts(): assert response.text == "You're at subdomain.example.com!" -def test_vhosts_with_list(): - app = Sanic('test_vhosts') +def test_vhosts_with_list(app): @app.route('/', host=["hello.com", "world.com"]) async def handler(request): @@ -37,8 +34,8 @@ def test_vhosts_with_list(): request, response = app.test_client.get('/', headers=headers) assert response.text == "Hello, world!" -def test_vhosts_with_defaults(): - app = Sanic('test_vhosts') + +def test_vhosts_with_defaults(app): @app.route('/', host="hello.com") async def handler(request): diff --git a/tests/test_views.py b/tests/test_views.py index 71d32a7f..5d339f55 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,6 +1,5 @@ import pytest as pytest -from sanic import Sanic from sanic.exceptions import InvalidUsage from sanic.response import text, HTTPResponse from sanic.views import HTTPMethodView, CompositionView @@ -10,8 +9,7 @@ from sanic.constants import HTTP_METHODS @pytest.mark.parametrize('method', HTTP_METHODS) -def test_methods(method): - app = Sanic('test_methods') +def test_methods(app, method): class DummyView(HTTPMethodView): @@ -44,8 +42,7 @@ def test_methods(method): assert response.headers['method'] == method -def test_unexisting_methods(): - app = Sanic('test_unexisting_methods') +def test_unexisting_methods(app): class DummyView(HTTPMethodView): @@ -59,8 +56,7 @@ def test_unexisting_methods(): assert response.text == 'Error: Method POST not allowed for URL /' -def test_argument_methods(): - app = Sanic('test_argument_methods') +def test_argument_methods(app): class DummyView(HTTPMethodView): @@ -74,8 +70,7 @@ def test_argument_methods(): assert response.text == 'I am get method with test123' -def test_with_bp(): - app = Sanic('test_with_bp') +def test_with_bp(app): bp = Blueprint('test_text') class DummyView(HTTPMethodView): @@ -93,8 +88,7 @@ def test_with_bp(): assert response.text == 'I am get method' -def test_with_bp_with_url_prefix(): - app = Sanic('test_with_bp_with_url_prefix') +def test_with_bp_with_url_prefix(app): bp = Blueprint('test_text', url_prefix='/test1') class DummyView(HTTPMethodView): @@ -110,8 +104,7 @@ def test_with_bp_with_url_prefix(): assert response.text == 'I am get method' -def test_with_middleware(): - app = Sanic('test_with_middleware') +def test_with_middleware(app): class DummyView(HTTPMethodView): @@ -132,9 +125,7 @@ def test_with_middleware(): assert type(results[0]) is Request -def test_with_middleware_response(): - app = Sanic('test_with_middleware_response') - +def test_with_middleware_response(app): results = [] @app.middleware('request') @@ -161,8 +152,7 @@ def test_with_middleware_response(): assert isinstance(results[2], HTTPResponse) -def test_with_custom_class_methods(): - app = Sanic('test_with_custom_class_methods') +def test_with_custom_class_methods(app): class DummyView(HTTPMethodView): global_var = 0 @@ -179,9 +169,7 @@ def test_with_custom_class_methods(): assert response.text == 'I am get method and global var is 10' -def test_with_decorator(): - app = Sanic('test_with_decorator') - +def test_with_decorator(app): results = [] def stupid_decorator(view): @@ -227,9 +215,7 @@ def test_composition_view_rejects_duplicate_methods(): @pytest.mark.parametrize('method', HTTP_METHODS) -def test_composition_view_runs_methods_as_expected(method): - app = Sanic('test_composition_view') - +def test_composition_view_runs_methods_as_expected(app, method): view = CompositionView() def first(request): @@ -251,9 +237,7 @@ def test_composition_view_runs_methods_as_expected(method): @pytest.mark.parametrize('method', HTTP_METHODS) -def test_composition_view_rejects_invalid_methods(method): - app = Sanic('test_composition_view') - +def test_composition_view_rejects_invalid_methods(app, method): view = CompositionView() view.add(['GET', 'POST', 'PUT'], lambda x: text('first method'))