From e427e38da82372974c174843f91028c7b1053e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Thu, 29 Jun 2017 12:34:52 +0200 Subject: [PATCH 001/104] Simplified the `Unauthorized.__init__` signature. It doesn't really make sense to have a `realm` parameter in the method signature. Instead, one can simply set the realm in the `challenge` dict if necessary. Also fixed the tests accordingly (and added a new one for "Bearer" auth-scheme). --- sanic/exceptions.py | 31 +++++++++++++++++++++++++------ tests/test_exceptions.py | 13 +++++++++++-- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 95e41b4a..d05342fa 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -204,25 +204,44 @@ class Unauthorized(SanicException): Unauthorized exception (401 HTTP status code). :param scheme: Name of the authentication scheme to be used. - :param realm: Description of the protected area. (optional) :param challenge: A dict containing values to add to the WWW-Authenticate header that is generated. This is especially useful when dealing with the Digest scheme. (optional) + + Examples:: + + # With a Basic auth-scheme, realm MUST be present: + challenge = {"realm": "Restricted Area"} + raise Unauthorized("Auth required.", "Basic", challenge) + + # With a Digest auth-scheme, things are a bit more complicated: + challenge = { + "realm": "Restricted Area", + "qop": "auth, auth-int", + "algorithm": "MD5", + "nonce": "abcdef", + "opaque": "zyxwvu" + } + raise Unauthorized("Auth required.", "Digest", challenge) + + # With a Bearer auth-scheme, realm is optional: + challenge = {"realm": "Restricted Area"} + raise Unauthorized("Auth required.", "Bearer", challenge) + """ pass - def __init__(self, message, scheme, realm="", challenge=None): + def __init__(self, message, scheme, challenge=None): super().__init__(message) - adds = "" + chal = "" if challenge is not None: values = ["{!s}={!r}".format(k, v) for k, v in challenge.items()] - adds = ', '.join(values) - adds = ', {}'.format(adds) + chal = ', '.join(values) self.headers = { - "WWW-Authenticate": "{} realm='{}'{}".format(scheme, realm, adds) + "WWW-Authenticate": "{} {}".format(scheme, chal).rstrip() } diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index dcdecabd..db1fc246 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -29,17 +29,22 @@ def exception_app(): @app.route('/401/basic') def handler_401_basic(request): - raise Unauthorized("Unauthorized", "Basic", "Sanic") + raise Unauthorized("Unauthorized", "Basic", {"realm": "Sanic"}) @app.route('/401/digest') def handler_401_digest(request): challenge = { + "realm": "Sanic", "qop": "auth, auth-int", "algorithm": "MD5", "nonce": "abcdef", "opaque": "zyxwvu", } - raise Unauthorized("Unauthorized", "Digest", "Sanic", challenge) + raise Unauthorized("Unauthorized", "Digest", challenge) + + @app.route('/401/bearer') + def handler_401_bearer(request): + raise Unauthorized("Unauthorized", "Bearer") @app.route('/invalid') def handler_invalid(request): @@ -126,6 +131,10 @@ def test_unauthorized_exception(exception_app): assert "nonce='abcdef'" in auth_header assert "opaque='zyxwvu'" in auth_header + request, response = exception_app.test_client.get('/401/bearer') + assert response.status == 401 + assert response.headers.get('WWW-Authenticate') == "Bearer" + def test_handled_unhandled_exception(exception_app): """Test that an exception not built into sanic is handled""" From 3fff685c444672cfbc1f8d8d2d9432b905c67686 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sat, 1 Jul 2017 23:46:34 -0700 Subject: [PATCH 002/104] add auto-doc support --- docs/conf.py | 13 +++++++++++++ docs/sanic/request_data.md | 8 ++++++++ docs/sanic/response.md | 8 ++++++++ docs/sanic/routing.md | 1 - sanic/response.py | 4 ++++ sanic/router.py | 9 +++++++++ 6 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index c97f3c19..33f5097d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,9 @@ import sys # Add support for Markdown documentation using Recommonmark from recommonmark.parser import CommonMarkParser +# Add support for auto-doc +from recommonmark.transform import AutoStructify + # Ensure that sanic is present in the path, to allow sphinx-apidoc to # autogenerate documentation from docstrings root_directory = os.path.dirname(os.getcwd()) @@ -140,3 +143,13 @@ epub_exclude_files = ['search.html'] # -- Custom Settings ------------------------------------------------------- suppress_warnings = ['image.nonlocal_uri'] + + +# app setup hook +def setup(app): + app.add_config_value('recommonmark_config', { + 'auto_toc_tree_section': 'Contents', + 'enable_eval_rst': True, + 'enable_auto_doc_ref': True, + }, True) + app.add_transform(AutoStructify) diff --git a/docs/sanic/request_data.md b/docs/sanic/request_data.md index bf5ae4a8..82bf19c9 100644 --- a/docs/sanic/request_data.md +++ b/docs/sanic/request_data.md @@ -117,3 +117,11 @@ args.get('titles') # => 'Post 1' args.getlist('titles') # => ['Post 1', 'Post 2'] ``` + + +## Full API Reference + +```eval_rst +.. autoclass:: sanic.request.Request + :members: json, token, form, files, args, raw_args, cookies, ip, scheme, host, content_type, path, query_string, url +``` \ No newline at end of file diff --git a/docs/sanic/response.md b/docs/sanic/response.md index 9c3c95f7..91e0da83 100644 --- a/docs/sanic/response.md +++ b/docs/sanic/response.md @@ -110,3 +110,11 @@ def handle_request(request): status=200 ) ``` + + +## Full API Reference + +```eval_rst +.. automodule:: sanic.response + :members: json, text, raw, html, file, file_stream, stream, redirect +``` \ No newline at end of file diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index 9b4d060f..e039e249 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -214,4 +214,3 @@ and `recv` methods to send and receive data respectively. WebSocket support requires the [websockets](https://github.com/aaugustin/websockets) package by Aymeric Augustin. - diff --git a/sanic/response.py b/sanic/response.py index ea233d9a..f4fb1ea6 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -237,6 +237,7 @@ def json(body, status=200, headers=None, content_type="application/json", **kwargs): """ Returns response object with body in json format. + :param body: Response data to be serialized. :param status: Response code. :param headers: Custom Headers. @@ -250,6 +251,7 @@ def text(body, status=200, headers=None, content_type="text/plain; charset=utf-8"): """ Returns response object with body in text format. + :param body: Response data to be encoded. :param status: Response code. :param headers: Custom Headers. @@ -264,6 +266,7 @@ def raw(body, status=200, headers=None, content_type="application/octet-stream"): """ Returns response object without encoding the body. + :param body: Response data. :param status: Response code. :param headers: Custom Headers. @@ -276,6 +279,7 @@ def raw(body, status=200, headers=None, def html(body, status=200, headers=None): """ Returns response object with body in html format. + :param body: Response data to be encoded. :param status: Response code. :param headers: Custom Headers. diff --git a/sanic/router.py b/sanic/router.py index 691f1388..ce491881 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -99,7 +99,16 @@ class Router: return name, _type, pattern def add(self, uri, methods, handler, host=None, strict_slashes=False): + """Add a handler to the route list + :param uri: path to match + :param methods: sequence of accepted method names. If none are + provided, any method is allowed + :param handler: request handler function. + When executed, it should provide a response object. + :param strict_slashes: strict to trailing slash + :return: Nothing + """ # add regular version self._add(uri, methods, handler, host) From 5d00717f39a44f0d9402a2963ab0b1fd89e17919 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 2 Jul 2017 10:02:04 -0700 Subject: [PATCH 003/104] improve doc and remove warnings --- docs/Makefile | 225 +++++++++++++++++++++++++++-- docs/conf.py | 1 - docs/index.rst | 3 + docs/make.bat | 265 +++++++++++++++++++++++++++++++++-- docs/sanic/api_reference.rst | 150 ++++++++++++++++++++ docs/sanic/request_data.md | 8 -- docs/sanic/response.md | 8 -- sanic/config.py | 2 +- sanic/exceptions.py | 7 +- 9 files changed, 628 insertions(+), 41 deletions(-) create mode 100644 docs/sanic/api_reference.rst diff --git a/docs/Makefile b/docs/Makefile index ef166d7d..72b82bed 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,20 +1,225 @@ -# Minimal makefile for Sphinx documentation +# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -SPHINXPROJ = Sanic -SOURCEDIR = . +PAPER = BUILDDIR = _build -# Put it first so that "make" without argument is like "make help". +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" -.PHONY: help Makefile +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/aiographite.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/aiographite.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/aiographite" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/aiographite" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/docs/conf.py b/docs/conf.py index 33f5097d..e254c183 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -148,7 +148,6 @@ suppress_warnings = ['image.nonlocal_uri'] # app setup hook def setup(app): app.add_config_value('recommonmark_config', { - 'auto_toc_tree_section': 'Contents', 'enable_eval_rst': True, 'enable_auto_doc_ref': True, }, True) diff --git a/docs/index.rst b/docs/index.rst index 80e7e70f..9f4fa00c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,7 @@ Guides sanic/blueprints sanic/config sanic/cookies + sanic/decorators sanic/streaming sanic/class_based_views sanic/custom_protocol @@ -25,6 +26,7 @@ Guides sanic/deploying sanic/extensions sanic/contributing + sanic/api_reference Module Documentation @@ -33,4 +35,5 @@ Module Documentation .. toctree:: * :ref:`genindex` +* :ref:`modindex` * :ref:`search` diff --git a/docs/make.bat b/docs/make.bat index 54191087..3887c127 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,19 +1,64 @@ @ECHO OFF -pushd %~dp0 - REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) -set SOURCEDIR=. set BUILDDIR=_build -set SPHINXPROJ=Sanic +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) if "%1" == "" goto help -%SPHINXBUILD% >NUL 2>NUL +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. epub3 to make an epub3 + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + echo. coverage to run coverage check of the documentation if enabled + echo. dummy to check syntax errors of document sources + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +REM Check if sphinx-build is available and fallback to Python version if any +%SPHINXBUILD% 1>NUL 2>NUL +if errorlevel 9009 goto sphinx_python +goto sphinx_ok + +:sphinx_python + +set SPHINXBUILD=python -m sphinx.__init__ +%SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx @@ -26,11 +71,211 @@ if errorlevel 9009 ( exit /b 1 ) -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% -goto end +:sphinx_ok -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\aiographite.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\aiographite.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "epub3" ( + %SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3 + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub3 file is in %BUILDDIR%/epub3. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %~dp0 + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "coverage" ( + %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage + if errorlevel 1 exit /b 1 + echo. + echo.Testing of coverage in the sources finished, look at the ^ +results in %BUILDDIR%/coverage/python.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +if "%1" == "dummy" ( + %SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. Dummy builder generates no files. + goto end +) :end -popd diff --git a/docs/sanic/api_reference.rst b/docs/sanic/api_reference.rst new file mode 100644 index 00000000..5ca7556a --- /dev/null +++ b/docs/sanic/api_reference.rst @@ -0,0 +1,150 @@ +API Reference +============= + +Submodules +---------- + +sanic.app module +---------------- + +.. automodule:: sanic.app + :members: + :undoc-members: + :show-inheritance: + +sanic.blueprints module +----------------------- + +.. automodule:: sanic.blueprints + :members: + :undoc-members: + :show-inheritance: + +sanic.config module +------------------- + +.. automodule:: sanic.config + :members: + :undoc-members: + :show-inheritance: + +sanic.constants module +---------------------- + +.. automodule:: sanic.constants + :members: + :undoc-members: + :show-inheritance: + +sanic.cookies module +-------------------- + +.. automodule:: sanic.cookies + :members: + :undoc-members: + :show-inheritance: + +sanic.exceptions module +----------------------- + +.. automodule:: sanic.exceptions + :members: + :undoc-members: + :show-inheritance: + +sanic.handlers module +--------------------- + +.. automodule:: sanic.handlers + :members: + :undoc-members: + :show-inheritance: + +sanic.log module +---------------- + +.. automodule:: sanic.log + :members: + :undoc-members: + :show-inheritance: + +sanic.request module +-------------------- + +.. automodule:: sanic.request + :members: + :undoc-members: + :show-inheritance: + +sanic.response module +--------------------- + +.. automodule:: sanic.response + :members: + :undoc-members: + :show-inheritance: + +sanic.router module +------------------- + +.. automodule:: sanic.router + :members: + :undoc-members: + :show-inheritance: + +sanic.server module +------------------- + +.. automodule:: sanic.server + :members: + :undoc-members: + :show-inheritance: + +sanic.static module +------------------- + +.. automodule:: sanic.static + :members: + :undoc-members: + :show-inheritance: + +sanic.testing module +-------------------- + +.. automodule:: sanic.testing + :members: + :undoc-members: + :show-inheritance: + +sanic.views module +------------------ + +.. automodule:: sanic.views + :members: + :undoc-members: + :show-inheritance: + +sanic.websocket module +---------------------- + +.. automodule:: sanic.websocket + :members: + :undoc-members: + :show-inheritance: + +sanic.worker module +------------------- + +.. automodule:: sanic.worker + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: sanic + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/sanic/request_data.md b/docs/sanic/request_data.md index 82bf19c9..bf5ae4a8 100644 --- a/docs/sanic/request_data.md +++ b/docs/sanic/request_data.md @@ -117,11 +117,3 @@ args.get('titles') # => 'Post 1' args.getlist('titles') # => ['Post 1', 'Post 2'] ``` - - -## Full API Reference - -```eval_rst -.. autoclass:: sanic.request.Request - :members: json, token, form, files, args, raw_args, cookies, ip, scheme, host, content_type, path, query_string, url -``` \ No newline at end of file diff --git a/docs/sanic/response.md b/docs/sanic/response.md index 91e0da83..9c3c95f7 100644 --- a/docs/sanic/response.md +++ b/docs/sanic/response.md @@ -110,11 +110,3 @@ def handle_request(request): status=200 ) ``` - - -## Full API Reference - -```eval_rst -.. automodule:: sanic.response - :members: json, text, raw, html, file, file_stream, stream, redirect -``` \ No newline at end of file diff --git a/sanic/config.py b/sanic/config.py index e3563bc1..159b6197 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -195,7 +195,7 @@ class Config(dict): def load_environment_vars(self): """ - Looks for any SANIC_ prefixed environment variables and applies + Looks for any ``SANIC_`` prefixed environment variables and applies them to the configuration if present. """ for k, v in os.environ.items(): diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 95e41b4a..c74f3873 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -206,8 +206,8 @@ class Unauthorized(SanicException): :param scheme: Name of the authentication scheme to be used. :param realm: Description of the protected area. (optional) :param challenge: A dict containing values to add to the WWW-Authenticate - header that is generated. This is especially useful when dealing with the - Digest scheme. (optional) + header that is generated. This is especially useful when dealing with the + Digest scheme. (optional) """ pass @@ -230,9 +230,10 @@ def abort(status_code, message=None): """ Raise an exception based on SanicException. Returns the HTTP response message appropriate for the given status code, unless provided. + :param status_code: The HTTP status code to return. :param message: The HTTP response body. Defaults to the messages - in response.py for the given status code. + in response.py for the given status code. """ if message is None: message = COMMON_STATUS_CODES.get(status_code, From e48bd08095e3f6318d62494813d5be9fb1bbf0d4 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 2 Jul 2017 10:05:33 -0700 Subject: [PATCH 004/104] make flake8 happy --- sanic/exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index c74f3873..e3285d1d 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -206,8 +206,8 @@ class Unauthorized(SanicException): :param scheme: Name of the authentication scheme to be used. :param realm: Description of the protected area. (optional) :param challenge: A dict containing values to add to the WWW-Authenticate - header that is generated. This is especially useful when dealing with the - Digest scheme. (optional) + header that is generated. This is especially useful when + dealing with the Digest scheme. (optional) """ pass From eb9af8bceb16349089892ebc46d705f8de8c5c13 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Sun, 9 Jul 2017 08:18:45 +0200 Subject: [PATCH 005/104] Drop aiohttp from benchmark table The reason is: aiohttp with disabled access log shows about 16,000 RPS on sanic's own benchmark. It's pretty much faster than 3,000 RPS from the table. I'm not a Sanic dev team member. You should not trust users to update this table but manage periodic updates yourself. If you don't want to do it --- it's up to you. Please just drop very incorrect and outdated numbers from README in this case. --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index 8a7b2706..8da47f5a 100644 --- a/README.rst +++ b/README.rst @@ -34,8 +34,6 @@ not speed up requests. +-----------+-----------------------+----------------+---------------+ | Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms | +-----------+-----------------------+----------------+---------------+ -| Aiohttp | Python 3.5 + uvloop | 2,979 | 33.42ms | -+-----------+-----------------------+----------------+---------------+ | Tornado | Python 3.5 | 2,138 | 46.66ms | +-----------+-----------------------+----------------+---------------+ From be0f3731b430bab7efdee4167d60ee30acc237fa Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 12 Jul 2017 22:26:58 +0900 Subject: [PATCH 006/104] ensure loop.close() and sys.exit() in gunicorn worker --- sanic/worker.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/sanic/worker.py b/sanic/worker.py index 7c02053c..9f950c34 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -3,6 +3,7 @@ import sys import signal import asyncio import logging +import traceback try: import ssl @@ -69,10 +70,16 @@ class GunicornWorker(base.Worker): trigger_events(self._server_settings.get('before_stop', []), self.loop) self.loop.run_until_complete(self.close()) + except: + traceback.print_exc() finally: - trigger_events(self._server_settings.get('after_stop', []), - self.loop) - self.loop.close() + try: + trigger_events(self._server_settings.get('after_stop', []), + self.loop) + except: + traceback.print_exc() + finally: + self.loop.close() sys.exit(self.exit_code) From 4265ad5f23425b428e9bc0f1de3b5e30675858f2 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 12 Jul 2017 20:18:56 -0700 Subject: [PATCH 007/104] add versioning --- docs/sanic/versioning.md | 50 +++++++++++++++++++++++++++++++ sanic/app.py | 44 ++++++++++++++++----------- sanic/blueprints.py | 64 ++++++++++++++++++++++++---------------- sanic/router.py | 10 ++++++- tests/test_blueprints.py | 26 ++++++++++++++++ tests/test_routes.py | 21 +++++++++++++ 6 files changed, 170 insertions(+), 45 deletions(-) create mode 100644 docs/sanic/versioning.md diff --git a/docs/sanic/versioning.md b/docs/sanic/versioning.md new file mode 100644 index 00000000..85cbd278 --- /dev/null +++ b/docs/sanic/versioning.md @@ -0,0 +1,50 @@ +# Versioning + +You can pass the `version` keyword to the route decorators, or to a blueprint initializer. It will result in the `v{version}` url prefix where `{version}` is the version number. + +## Per route + +You can pass a version number to the routes directly. + +```python +from sanic import response + + +@app.route('/text', verion=1) +def handle_request(request): + return response.text('Hello world! Version 1') + +@app.route('/text', verion=2) +def handle_request(request): + return response.text('Hello world! Version 2') + +app.run(port=80) +``` + +Then with curl: + +```bash +curl localhost/v1/text +curl localhost/v2/text +``` + +## Global blueprint version + +You can also pass a version number to the blueprint, which will apply to all routes. + +```python +from sanic import response +from sanic.blueprints import Blueprint + +bp = Blueprint('test', version=1) + +@bp.route('/html') +def handle_request(request): + return response.html('

Hello world!

') +``` + +Then with curl: + +```bash +curl localhost/v1/html +``` diff --git a/sanic/app.py b/sanic/app.py index 5b071200..f1e8be7e 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -113,7 +113,7 @@ class Sanic: # Decorator def route(self, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=False, stream=False): + strict_slashes=False, stream=False, version=None): """Decorate a function to be registered as a route :param uri: path of the URL @@ -136,42 +136,49 @@ class Sanic: if stream: handler.is_stream = stream self.router.add(uri=uri, methods=methods, handler=handler, - host=host, strict_slashes=strict_slashes) + host=host, strict_slashes=strict_slashes, + version=version) return handler return response # Shorthand method decorators - def get(self, uri, host=None, strict_slashes=False): + def get(self, uri, host=None, strict_slashes=False, version=None): return self.route(uri, methods=frozenset({"GET"}), host=host, - strict_slashes=strict_slashes) + strict_slashes=strict_slashes, version=version) - def post(self, uri, host=None, strict_slashes=False, stream=False): + def post(self, uri, host=None, strict_slashes=False, stream=False, + version=None): return self.route(uri, methods=frozenset({"POST"}), host=host, - strict_slashes=strict_slashes, stream=stream) + strict_slashes=strict_slashes, stream=stream, + version=version) - def put(self, uri, host=None, strict_slashes=False, stream=False): + def put(self, uri, host=None, strict_slashes=False, stream=False, + version=None): return self.route(uri, methods=frozenset({"PUT"}), host=host, - strict_slashes=strict_slashes, stream=stream) + strict_slashes=strict_slashes, stream=stream, + version=version) - def head(self, uri, host=None, strict_slashes=False): + def head(self, uri, host=None, strict_slashes=False, version=None): return self.route(uri, methods=frozenset({"HEAD"}), host=host, - strict_slashes=strict_slashes) + strict_slashes=strict_slashes, version=version) - def options(self, uri, host=None, strict_slashes=False): + def options(self, uri, host=None, strict_slashes=False, version=None): return self.route(uri, methods=frozenset({"OPTIONS"}), host=host, - strict_slashes=strict_slashes) + strict_slashes=strict_slashes, version=version) - def patch(self, uri, host=None, strict_slashes=False, stream=False): + def patch(self, uri, host=None, strict_slashes=False, stream=False, + version=None): return self.route(uri, methods=frozenset({"PATCH"}), host=host, - strict_slashes=strict_slashes, stream=stream) + strict_slashes=strict_slashes, stream=stream, + version=version) - def delete(self, uri, host=None, strict_slashes=False): + def delete(self, uri, host=None, strict_slashes=False, version=None): return self.route(uri, methods=frozenset({"DELETE"}), host=host, - strict_slashes=strict_slashes) + strict_slashes=strict_slashes, version=version) def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=False): + strict_slashes=False, version=None): """A helper method to register class instance or functions as a handler to the application url routes. @@ -204,7 +211,8 @@ class Sanic: break self.route(uri=uri, methods=methods, host=host, - strict_slashes=strict_slashes, stream=stream)(handler) + strict_slashes=strict_slashes, stream=stream, + version=version)(handler) return handler # Decorator diff --git a/sanic/blueprints.py b/sanic/blueprints.py index b3866cbd..0e97903b 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -4,8 +4,8 @@ from sanic.constants import HTTP_METHODS from sanic.views import CompositionView FutureRoute = namedtuple('Route', - ['handler', 'uri', 'methods', - 'host', 'strict_slashes', 'stream']) + ['handler', 'uri', 'methods', 'host', + 'strict_slashes', 'stream', 'version']) FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs']) FutureException = namedtuple('Route', ['handler', 'args', 'kwargs']) @@ -14,7 +14,7 @@ FutureStatic = namedtuple('Route', class Blueprint: - def __init__(self, name, url_prefix=None, host=None): + def __init__(self, name, url_prefix=None, host=None, version=None): """Create a new blueprint :param name: unique name of the blueprint @@ -30,6 +30,7 @@ class Blueprint: self.listeners = defaultdict(list) self.middlewares = [] self.statics = [] + self.version = version def register(self, app, options): """Register the blueprint to the sanic app.""" @@ -43,12 +44,16 @@ class Blueprint: future.handler.__blueprintname__ = self.name # Prepend the blueprint URI prefix if available uri = url_prefix + future.uri if url_prefix else future.uri + + version = future.version or self.version + app.route( uri=uri[1:] if uri.startswith('//') else uri, methods=future.methods, host=future.host or self.host, strict_slashes=future.strict_slashes, - stream=future.stream + stream=future.stream, + version=version )(future.handler) for future in self.websocket_routes: @@ -89,7 +94,7 @@ class Blueprint: app.listener(event)(listener) def route(self, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=False, stream=False): + strict_slashes=False, stream=False, version=None): """Create a blueprint route from a decorated function. :param uri: endpoint at which the route will be accessible. @@ -97,13 +102,13 @@ class Blueprint: """ def decorator(handler): route = FutureRoute( - handler, uri, methods, host, strict_slashes, stream) + handler, uri, methods, host, strict_slashes, stream, version) self.routes.append(route) return handler return decorator def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=False): + strict_slashes=False, version=None): """Create a blueprint route from a function. :param handler: function for handling uri requests. Accepts function, @@ -125,21 +130,22 @@ class Blueprint: methods = handler.handlers.keys() self.route(uri=uri, methods=methods, host=host, - strict_slashes=strict_slashes)(handler) + strict_slashes=strict_slashes, version=version)(handler) return handler - def websocket(self, uri, host=None, strict_slashes=False): + def websocket(self, uri, host=None, strict_slashes=False, version=None): """Create a blueprint websocket route from a decorated function. :param uri: endpoint at which the route will be accessible. """ def decorator(handler): - route = FutureRoute(handler, uri, [], host, strict_slashes, False) + route = FutureRoute(handler, uri, [], host, strict_slashes, + False, version) self.websocket_routes.append(route) return handler return decorator - def add_websocket_route(self, handler, uri, host=None): + def add_websocket_route(self, handler, uri, host=None, version=None): """Create a blueprint websocket route from a function. :param handler: function for handling uri requests. Accepts function, @@ -147,7 +153,7 @@ class Blueprint: :param uri: endpoint at which the route will be accessible. :return: function or class instance """ - self.websocket(uri=uri, host=host)(handler) + self.websocket(uri=uri, host=host, version=version)(handler) return handler def listener(self, event): @@ -193,30 +199,36 @@ class Blueprint: self.statics.append(static) # Shorthand method decorators - def get(self, uri, host=None, strict_slashes=False): + def get(self, uri, host=None, strict_slashes=False, version=None): return self.route(uri, methods=["GET"], host=host, - strict_slashes=strict_slashes) + strict_slashes=strict_slashes, version=version) - def post(self, uri, host=None, strict_slashes=False, stream=False): + def post(self, uri, host=None, strict_slashes=False, stream=False, + version=None): return self.route(uri, methods=["POST"], host=host, - strict_slashes=strict_slashes, stream=stream) + strict_slashes=strict_slashes, stream=stream, + version=version) - def put(self, uri, host=None, strict_slashes=False, stream=False): + def put(self, uri, host=None, strict_slashes=False, stream=False, + version=None): return self.route(uri, methods=["PUT"], host=host, - strict_slashes=strict_slashes, stream=stream) + strict_slashes=strict_slashes, stream=stream, + version=version) - def head(self, uri, host=None, strict_slashes=False): + def head(self, uri, host=None, strict_slashes=False, version=None): return self.route(uri, methods=["HEAD"], host=host, - strict_slashes=strict_slashes) + strict_slashes=strict_slashes, version=version) - def options(self, uri, host=None, strict_slashes=False): + def options(self, uri, host=None, strict_slashes=False, version=None): return self.route(uri, methods=["OPTIONS"], host=host, - strict_slashes=strict_slashes) + strict_slashes=strict_slashes, version=version) - def patch(self, uri, host=None, strict_slashes=False, stream=False): + def patch(self, uri, host=None, strict_slashes=False, stream=False, + version=None): return self.route(uri, methods=["PATCH"], host=host, - strict_slashes=strict_slashes, stream=stream) + strict_slashes=strict_slashes, stream=stream, + version=version) - def delete(self, uri, host=None, strict_slashes=False): + def delete(self, uri, host=None, strict_slashes=False, version=None): return self.route(uri, methods=["DELETE"], host=host, - strict_slashes=strict_slashes) + strict_slashes=strict_slashes, version=version) diff --git a/sanic/router.py b/sanic/router.py index ce491881..efc48f37 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -98,7 +98,8 @@ class Router: return name, _type, pattern - def add(self, uri, methods, handler, host=None, strict_slashes=False): + def add(self, uri, methods, handler, host=None, strict_slashes=False, + version=None): """Add a handler to the route list :param uri: path to match @@ -107,8 +108,15 @@ class Router: :param handler: request handler function. When executed, it should provide a response object. :param strict_slashes: strict to trailing slash + :param version: current version of the route or blueprint. See + docs for further details. :return: Nothing """ + if version is not None: + if uri.startswith('/'): + uri = "/".join(["/v{}".format(str(version)), uri[1:]]) + else: + uri = "/".join(["/v{}".format(str(version)), uri]) # add regular version self._add(uri, methods, handler, host) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 9ab387be..5cb356c2 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -1,16 +1,42 @@ import asyncio import inspect +import pytest from sanic import Sanic from sanic.blueprints import Blueprint from sanic.response import json, text from sanic.exceptions import NotFound, ServerError, InvalidUsage +from sanic.constants import HTTP_METHODS # ------------------------------------------------------------ # # GET # ------------------------------------------------------------ # +@pytest.mark.parametrize('method', HTTP_METHODS) +def test_versioned_routes_get(method): + app = Sanic('test_shorhand_routes_get') + bp = Blueprint('test_text') + + method = method.lower() + + func = getattr(bp, method) + if callable(func): + @func('/{}'.format(method), version=1) + def handler(request): + return text('OK') + else: + print(func) + raise + + app.blueprint(bp) + + client_method = getattr(app.test_client, method) + + request, response = client_method('/v1/{}'.format(method)) + assert response.status == 200 + + def test_bp(): app = Sanic('test_text') bp = Blueprint('test_text') diff --git a/tests/test_routes.py b/tests/test_routes.py index 4afb4a9c..04a682a0 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -4,12 +4,33 @@ import pytest from sanic import Sanic from sanic.response import text from sanic.router import RouteExists, RouteDoesNotExist +from sanic.constants import HTTP_METHODS # ------------------------------------------------------------ # # UTF-8 # ------------------------------------------------------------ # +@pytest.mark.parametrize('method', HTTP_METHODS) +def test_versioned_routes_get(method): + app = Sanic('test_shorhand_routes_get') + + method = method.lower() + + func = getattr(app, method) + if callable(func): + @func('/{}'.format(method), version=1) + def handler(request): + return text('OK') + else: + print(func) + raise + + client_method = getattr(app.test_client, method) + + request, response = client_method('/v1/{}'.format(method)) + assert response.status== 200 + def test_shorthand_routes_get(): app = Sanic('test_shorhand_routes_get') From 426e00b6f464b8c2be8f1f36fe5603d2033aacf7 Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 13 Jul 2017 15:09:04 +0900 Subject: [PATCH 008/104] dont let dictConfig influence already exists configs --- sanic/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sanic/config.py b/sanic/config.py index a9bbff72..b8637bfa 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -17,6 +17,7 @@ _address_dict = { LOGGING = { 'version': 1, + 'disable_existing_loggers': False, 'filters': { 'accessFilter': { '()': DefaultFilter, From b2017cae77721d02cdb3b847dc10b16867121df0 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Thu, 13 Jul 2017 23:41:04 +0200 Subject: [PATCH 009/104] Drop benchmarks from readme --- README.rst | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/README.rst b/README.rst index 8da47f5a..410bd0b8 100644 --- a/README.rst +++ b/README.rst @@ -11,32 +11,6 @@ Sanic is developed `on GitHub `_. Contribu If you have a project that utilizes Sanic make sure to comment on the `issue `_ that we use to track those projects! -Benchmarks ----------- - -All tests were run on an AWS medium instance running ubuntu, using 1 -process. Each script delivered a small JSON response and was tested with -wrk using 100 connections. Pypy was tested for Falcon and Flask but did -not speed up requests. - -+-----------+-----------------------+----------------+---------------+ -| Server | Implementation | Requests/sec | Avg Latency | -+===========+=======================+================+===============+ -| Sanic | Python 3.5 + uvloop | 33,342 | 2.96ms | -+-----------+-----------------------+----------------+---------------+ -| Wheezy | gunicorn + meinheld | 20,244 | 4.94ms | -+-----------+-----------------------+----------------+---------------+ -| Falcon | gunicorn + meinheld | 18,972 | 5.27ms | -+-----------+-----------------------+----------------+---------------+ -| Bottle | gunicorn + meinheld | 13,596 | 7.36ms | -+-----------+-----------------------+----------------+---------------+ -| Flask | gunicorn + meinheld | 4,988 | 20.08ms | -+-----------+-----------------------+----------------+---------------+ -| Kyoukai | Python 3.5 + uvloop | 3,889 | 27.44ms | -+-----------+-----------------------+----------------+---------------+ -| Tornado | Python 3.5 | 2,138 | 46.66ms | -+-----------+-----------------------+----------------+---------------+ - Hello World Example ------------------- From 75378d3567489fa3318de282dfe7b13df205ded2 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Fri, 14 Jul 2017 09:29:16 -0700 Subject: [PATCH 010/104] add remote_addr property for proxy fix --- sanic/request.py | 21 +++++++++++++++++++-- tests/test_requests.py | 27 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index d8674c48..eb05f471 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -45,7 +45,7 @@ class Request(dict): __slots__ = ( 'app', 'headers', 'version', 'method', '_cookies', 'transport', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', - '_ip', '_parsed_url', 'uri_template', 'stream' + '_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr' ) def __init__(self, url_bytes, headers, version, method, transport): @@ -142,7 +142,7 @@ class Request(dict): @property def cookies(self): if self._cookies is None: - cookie = self.headers.get('Cookie') or self.headers.get('cookie') + cookie = self.headers.get('Cookie') if cookie is not None: cookies = SimpleCookie() cookies.load(cookie) @@ -159,6 +159,23 @@ class Request(dict): (None, None)) return self._ip + @property + def remote_addr(self): + """Attempt to return the original client ip based on X-Forwarded-For. + + :return: original client ip. + """ + if not hasattr(self, '_remote_addr'): + forwarded_for = self.headers.get('X-Forwarded-For', '').split(',') + remote_addrs = [ + addr for addr in [addr.strip() for addr in forwarded_for] if addr + ] + if len(remote_addrs) > 0: + self._remote_addr = remote_addrs[0] + else: + self._remote_addr = '' + return self._remote_addr + @property def scheme(self): if self.app.websocket_enabled \ diff --git a/tests/test_requests.py b/tests/test_requests.py index 997f32cb..f0696c7f 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -211,6 +211,32 @@ def test_content_type(): assert response.text == 'application/json' +def test_remote_addr(): + app = Sanic('test_content_type') + + @app.route('/') + async def handler(request): + return text(request.remote_addr) + + headers = { + 'X-Forwarded-For': '127.0.0.1, 127.0.1.2' + } + request, response = app.test_client.get('/', headers=headers) + assert request.remote_addr == '127.0.0.1' + assert response.text == '127.0.0.1' + + request, response = app.test_client.get('/') + assert request.remote_addr == '' + assert response.text == '' + + headers = { + 'X-Forwarded-For': '127.0.0.1, , ,,127.0.1.2' + } + request, response = app.test_client.get('/', headers=headers) + assert request.remote_addr == '127.0.0.1' + assert response.text == '127.0.0.1' + + def test_match_info(): app = Sanic('test_match_info') @@ -259,6 +285,7 @@ def test_post_form_urlencoded(): assert request.form.get('test') == 'OK' + @pytest.mark.parametrize( 'payload', [ '------sanic\r\n' \ From 198bf55b7b47f0344bee3ad5f3246dbaa80847b0 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Fri, 14 Jul 2017 17:23:18 -0700 Subject: [PATCH 011/104] flake8 fix --- sanic/request.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sanic/request.py b/sanic/request.py index eb05f471..27ff011e 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -168,7 +168,9 @@ class Request(dict): if not hasattr(self, '_remote_addr'): forwarded_for = self.headers.get('X-Forwarded-For', '').split(',') remote_addrs = [ - addr for addr in [addr.strip() for addr in forwarded_for] if addr + addr for addr in [ + addr.strip() for addr in forwarded_for + ] if addr ] if len(remote_addrs) > 0: self._remote_addr = remote_addrs[0] From 32be1a649694a704c861d3dc4e9b09165cbbd2df Mon Sep 17 00:00:00 2001 From: Mohamed Akram Date: Thu, 20 Jul 2017 03:02:40 +0400 Subject: [PATCH 012/104] Fix FreeBSD syslog path --- sanic/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/config.py b/sanic/config.py index b8637bfa..6ffcf7a1 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -12,7 +12,7 @@ _address_dict = { 'Windows': ('localhost', 514), 'Darwin': '/var/run/syslog', 'Linux': '/dev/log', - 'FreeBSD': '/dev/log' + 'FreeBSD': '/var/run/log' } LOGGING = { From f34226425e3a2b50749e52f17e74b1bf40246043 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sat, 22 Jul 2017 18:41:53 -0700 Subject: [PATCH 013/104] add jinja2-sanic --- docs/sanic/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index 92b61f8c..5643f4fc 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -24,3 +24,4 @@ A list of Sanic extensions created by the community. - [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation. - [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically. - [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously. +- [jinja2-sanic](https://github.com/yunstanford/jinja2-sanic): a jinja2 template renderer for Sanic.([Documentation](http://jinja2-sanic.readthedocs.io/en/latest/)) From f50dc83829596110acf21c1057d22f3b2de63239 Mon Sep 17 00:00:00 2001 From: zyguan Date: Mon, 24 Jul 2017 01:37:36 +0800 Subject: [PATCH 014/104] handle keep-alive timeout gracefully --- sanic/server.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 2ee48688..f62ba654 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -139,8 +139,10 @@ class HttpProtocol(asyncio.Protocol): self._request_stream_task.cancel() if self._request_handler_task: self._request_handler_task.cancel() - exception = RequestTimeout('Request Timeout') - self.write_error(exception) + try: + raise RequestTimeout('Request Timeout') + except RequestTimeout as exception: + self.write_error(exception) # -------------------------------------------- # # Parsing @@ -317,6 +319,7 @@ class HttpProtocol(asyncio.Protocol): self.cleanup() def write_error(self, exception): + response = None try: response = self.error_handler.response(self.request, exception) version = self.request.version if self.request else '1.1' @@ -331,20 +334,23 @@ class HttpProtocol(asyncio.Protocol): from_error=True) finally: if self.has_log: - extra = { - 'status': response.status, - 'host': '', - 'request': str(self.request) + str(self.url) - } - if response and isinstance(response, HTTPResponse): + extra = dict() + if isinstance(response, HTTPResponse): + extra['status'] = response.status extra['byte'] = len(response.body) else: + extra['status'] = 0 extra['byte'] = -1 if self.request: extra['host'] = '%s:%d' % self.request.ip, extra['request'] = '%s %s' % (self.request.method, self.url) - netlog.info('', extra=extra) + else: + extra['host'] = 'UNKNOWN' + extra['request'] = 'nil' + if self.parser and not (self.keep_alive + and extra['status'] == 408): + netlog.info('', extra=extra) self.transport.close() def bail_out(self, message, from_error=False): From 918e2ba8d06ca0aff63cc2354539274a6a1f6105 Mon Sep 17 00:00:00 2001 From: zyguan Date: Mon, 24 Jul 2017 11:53:11 +0800 Subject: [PATCH 015/104] Revert "fix #752" This reverts commit 599fbcee6e1e427829a9f03de7f7aad2ee67ac22. --- sanic/app.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index f1e8be7e..f0ccad86 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -33,9 +33,7 @@ class Sanic: logging.config.dictConfig(log_config) # Only set up a default log handler if the # end-user application didn't set anything up. - if not (logging.root.handlers and - log.level == logging.NOTSET and - log_config): + if not logging.root.handlers and log.level == logging.NOTSET: formatter = logging.Formatter( "%(asctime)s: %(levelname)s: %(message)s") handler = logging.StreamHandler() From da91b16244958b862b95077815e6a7d39b2d019b Mon Sep 17 00:00:00 2001 From: zyguan Date: Mon, 24 Jul 2017 18:21:15 +0800 Subject: [PATCH 016/104] add tests --- tests/test_logging.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/test_logging.py b/tests/test_logging.py index fc26ca93..d6911d86 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,5 +1,7 @@ -import asyncio import uuid +from importlib import reload + +from sanic.config import LOGGING from sanic.response import text from sanic import Sanic from io import StringIO @@ -10,6 +12,11 @@ function: %(funcName)s(); \ message: %(message)s''' +def reset_logging(): + logging.shutdown() + reload(logging) + + def test_log(): log_stream = StringIO() for handler in logging.root.handlers[:]: @@ -32,5 +39,19 @@ def test_log(): log_text = log_stream.getvalue() assert rand_string in log_text + +def test_default_log_fmt(): + + reset_logging() + Sanic() + for fmt in [h.formatter for h in logging.getLogger('sanic').handlers]: + assert fmt._fmt == LOGGING['formatters']['simple']['format'] + + reset_logging() + Sanic(log_config=None) + for fmt in [h.formatter for h in logging.getLogger('sanic').handlers]: + assert fmt._fmt == "%(asctime)s: %(levelname)s: %(message)s" + + if __name__ == "__main__": test_log() From 621343112d13010a228b39766c3b805ae8bd8993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karolis=20Ma=C5=BEukna?= Date: Tue, 25 Jul 2017 13:29:17 +0300 Subject: [PATCH 017/104] Fix typo in documentation --- docs/sanic/versioning.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sanic/versioning.md b/docs/sanic/versioning.md index 85cbd278..ab6dab22 100644 --- a/docs/sanic/versioning.md +++ b/docs/sanic/versioning.md @@ -10,11 +10,11 @@ You can pass a version number to the routes directly. from sanic import response -@app.route('/text', verion=1) +@app.route('/text', version=1) def handle_request(request): return response.text('Hello world! Version 1') -@app.route('/text', verion=2) +@app.route('/text', version=2) def handle_request(request): return response.text('Hello world! Version 2') From 40776e5324f3d880e8b3c94cd0c5ebe786d41701 Mon Sep 17 00:00:00 2001 From: cclauss Date: Wed, 26 Jul 2017 12:44:30 +0200 Subject: [PATCH 018/104] Comment: F821 undefined name is done on purpose Comment helps readers and `# noqa` silences linters --- tests/test_exceptions_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_exceptions_handler.py b/tests/test_exceptions_handler.py index 006c2cc4..6a959382 100644 --- a/tests/test_exceptions_handler.py +++ b/tests/test_exceptions_handler.py @@ -24,7 +24,7 @@ def handler_3(request): @exception_handler_app.route('/4') def handler_4(request): - foo = bar + foo = bar # noqa -- F821 undefined name 'bar' is done to throw exception return text(foo) From 429f7377cb6e3f0d6e68e94f2315a4c5e520ee0e Mon Sep 17 00:00:00 2001 From: MichaelYusko Date: Wed, 26 Jul 2017 19:32:23 +0300 Subject: [PATCH 019/104] Did the small changes for better readable --- sanic/response.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index f4fb1ea6..902b21c6 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -109,8 +109,9 @@ class BaseHTTPResponse: class StreamingHTTPResponse(BaseHTTPResponse): __slots__ = ( - 'transport', 'streaming_fn', - 'status', 'content_type', 'headers', '_cookies') + 'transport', 'streaming_fn', 'status', + 'content_type', 'headers', '_cookies' + ) def __init__(self, streaming_fn, status=200, headers=None, content_type='text/plain'): From b65eb69d9f680cdd00644186b8b4518081401aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Thu, 27 Jul 2017 23:00:27 +0200 Subject: [PATCH 020/104] Simplified the Unauthorized exception __init__ signature. (again). Use of **kwargs makes it more straight forward and easier to use. --- sanic/exceptions.py | 45 ++++++++++++++++++---------------------- tests/test_exceptions.py | 21 +++++++++---------- 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 21ab2a94..d23fcfee 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -208,44 +208,39 @@ class Unauthorized(SanicException): """ Unauthorized exception (401 HTTP status code). + :param message: Message describing the exception. :param scheme: Name of the authentication scheme to be used. - :param challenge: A dict containing values to add to the WWW-Authenticate - header that is generated. This is especially useful when dealing with - the Digest scheme. (optional) + + When present, kwargs is used to complete the WWW-Authentication header. Examples:: # With a Basic auth-scheme, realm MUST be present: - challenge = {"realm": "Restricted Area"} - raise Unauthorized("Auth required.", "Basic", challenge) + raise Unauthorized("Auth required.", "Basic", realm="Restricted Area") # With a Digest auth-scheme, things are a bit more complicated: - challenge = { - "realm": "Restricted Area", - "qop": "auth, auth-int", - "algorithm": "MD5", - "nonce": "abcdef", - "opaque": "zyxwvu" - } - raise Unauthorized("Auth required.", "Digest", challenge) + raise Unauthorized("Auth required.", + "Digest", + realm="Restricted Area", + qop="auth, auth-int", + algorithm="MD5", + nonce="abcdef", + opaque="zyxwvu") - # With a Bearer auth-scheme, realm is optional: - challenge = {"realm": "Restricted Area"} - raise Unauthorized("Auth required.", "Bearer", challenge) + # With a Bearer auth-scheme, realm is optional so you can write: + raise Unauthorized("Auth required.", "Bearer") + + # or, if you want to specify the realm: + raise Unauthorized("Auth required.", "Bearer", realm="Restricted Area") """ - pass - - def __init__(self, message, scheme, challenge=None): + def __init__(self, message, scheme, **kwargs): super().__init__(message) - chal = "" - - if challenge is not None: - values = ["{!s}={!r}".format(k, v) for k, v in challenge.items()] - chal = ', '.join(values) + values = ["{!s}={!r}".format(k, v) for k, v in kwargs.items()] + challenge = ', '.join(values) self.headers = { - "WWW-Authenticate": "{} {}".format(scheme, chal).rstrip() + "WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip() } diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 620e7891..1521c9ed 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -33,18 +33,17 @@ def exception_app(): @app.route('/401/basic') def handler_401_basic(request): - raise Unauthorized("Unauthorized", "Basic", {"realm": "Sanic"}) + raise Unauthorized("Unauthorized", "Basic", realm="Sanic") @app.route('/401/digest') def handler_401_digest(request): - challenge = { - "realm": "Sanic", - "qop": "auth, auth-int", - "algorithm": "MD5", - "nonce": "abcdef", - "opaque": "zyxwvu", - } - raise Unauthorized("Unauthorized", "Digest", challenge) + raise Unauthorized("Unauthorized", + "Digest", + realm="Sanic", + qop="auth, auth-int", + algorithm="MD5", + nonce="abcdef", + opaque="zyxwvu") @app.route('/401/bearer') def handler_401_bearer(request): @@ -122,7 +121,7 @@ def test_forbidden_exception(exception_app): request, response = exception_app.test_client.get('/403') assert response.status == 403 - + def test_unauthorized_exception(exception_app): """Test the built-in Unauthorized exception""" request, response = exception_app.test_client.get('/401/basic') @@ -132,7 +131,7 @@ def test_unauthorized_exception(exception_app): request, response = exception_app.test_client.get('/401/digest') assert response.status == 401 - + auth_header = response.headers.get('WWW-Authenticate') assert auth_header is not None assert auth_header.startswith('Digest') From eb8f65c58b1b02117dc795bf70869d77e0d6a2c2 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Thu, 27 Jul 2017 22:21:19 -0700 Subject: [PATCH 021/104] switch to use dist: precise --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d8e17093..afac549a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ sudo: false +dist: precise language: python cache: directories: From 69a8bb5e1f26b974ef6f57484eaeb73a9aa13245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Fri, 28 Jul 2017 22:29:45 +0200 Subject: [PATCH 022/104] Fixed a trailing white space in the docstring. --- sanic/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index d23fcfee..0edb0562 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -211,7 +211,7 @@ class Unauthorized(SanicException): :param message: Message describing the exception. :param scheme: Name of the authentication scheme to be used. - When present, kwargs is used to complete the WWW-Authentication header. + When present, kwargs is used to complete the WWW-Authentication header. Examples:: From 1b687f3feb3216fe2f37c4677723b7244041b951 Mon Sep 17 00:00:00 2001 From: akc Date: Tue, 1 Aug 2017 16:32:15 +0800 Subject: [PATCH 023/104] add some example links --- README.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.rst b/README.rst index 410bd0b8..a9bfa982 100644 --- a/README.rst +++ b/README.rst @@ -55,6 +55,16 @@ Documentation :target: https://pypi.python.org/pypi/sanic/ .. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg :target: https://pypi.python.org/pypi/sanic/ + + +Examples +-------- +`Non-Core examples `_. Examples of plugins and Sanic that are outside the scope of Sanic core. + +`Extensions `_. Sanic extensions created by the community. + +`Projects `_. Sanic in production use. + TODO ---- From f99a723627f2b3bf47e70932cf672c88d7a2f169 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 2 Aug 2017 09:05:33 -0700 Subject: [PATCH 024/104] fixed small doc issue --- .gitignore | 1 + docs/sanic/testing.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 73e923d3..4a834a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ settings.py docs/_build/ docs/_api/ build/* +.DS_Store diff --git a/docs/sanic/testing.md b/docs/sanic/testing.md index b8427a00..0aca9184 100644 --- a/docs/sanic/testing.md +++ b/docs/sanic/testing.md @@ -59,7 +59,7 @@ the available arguments to aiohttp can be found [in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session). -# pytest-sanic +## pytest-sanic [pytest-sanic](https://github.com/yunstanford/pytest-sanic) is a pytest plugin, it helps you to test your code asynchronously. Just write tests like, From 9b3fbe45932b70d0872458e70cb39fd1688a3055 Mon Sep 17 00:00:00 2001 From: 7 Date: Wed, 2 Aug 2017 10:15:18 -0700 Subject: [PATCH 025/104] fixed small doc issue (#877) --- .gitignore | 1 + docs/sanic/testing.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 73e923d3..4a834a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ settings.py docs/_build/ docs/_api/ build/* +.DS_Store diff --git a/docs/sanic/testing.md b/docs/sanic/testing.md index b8427a00..0aca9184 100644 --- a/docs/sanic/testing.md +++ b/docs/sanic/testing.md @@ -59,7 +59,7 @@ the available arguments to aiohttp can be found [in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session). -# pytest-sanic +## pytest-sanic [pytest-sanic](https://github.com/yunstanford/pytest-sanic) is a pytest plugin, it helps you to test your code asynchronously. Just write tests like, From f80a6ae228cb0576f2c4660845210ed9da4241d9 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Wed, 2 Aug 2017 19:11:53 -0700 Subject: [PATCH 026/104] Increment to 0.6.0 --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index 4cc0710f..8f35a283 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.5.4' +__version__ = '0.6.0' __all__ = ['Sanic', 'Blueprint'] From 375ed232167a433b20c223e9ffe2d1deb94afc17 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 8 Aug 2017 11:21:52 -0700 Subject: [PATCH 027/104] Weboscket subprotocol negotiation Fixes #874 --- sanic/app.py | 7 +++++-- sanic/websocket.py | 14 +++++++++++++- tests/test_routes.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index f0ccad86..a3acbecf 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -214,9 +214,12 @@ class Sanic: return handler # Decorator - def websocket(self, uri, host=None, strict_slashes=False): + def websocket(self, uri, host=None, strict_slashes=False, + subprotocols=None): """Decorate a function to be registered as a websocket route :param uri: path of the URL + :param subprotocols: optional list of strings with the supported + subprotocols :param host: :return: decorated function """ @@ -236,7 +239,7 @@ class Sanic: # On Python3.5 the Transport classes in asyncio do not # have a get_protocol() method as in uvloop protocol = request.transport._protocol - ws = await protocol.websocket_handshake(request) + ws = await protocol.websocket_handshake(request, subprotocols) # schedule the application handler # its future is kept in self.websocket_tasks in case it diff --git a/sanic/websocket.py b/sanic/websocket.py index 94320a5e..e8e9922f 100644 --- a/sanic/websocket.py +++ b/sanic/websocket.py @@ -41,7 +41,7 @@ class WebSocketProtocol(HttpProtocol): else: super().write_response(response) - async def websocket_handshake(self, request): + async def websocket_handshake(self, request, subprotocols=None): # let the websockets package do the handshake with the client headers = [] @@ -57,6 +57,17 @@ class WebSocketProtocol(HttpProtocol): except InvalidHandshake: raise InvalidUsage('Invalid websocket request') + subprotocol = None + if subprotocols and 'Sec-Websocket-Protocol' in request.headers: + # select a subprotocol + client_subprotocols = [p.strip() for p in request.headers[ + 'Sec-Websocket-Protocol'].split(',')] + for p in client_subprotocols: + if p in subprotocols: + subprotocol = p + set_header('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: @@ -69,5 +80,6 @@ class WebSocketProtocol(HttpProtocol): max_size=self.websocket_max_size, max_queue=self.websocket_max_queue ) + self.websocket.subprotocol = subprotocol self.websocket.connection_made(request.transport) return self.websocket diff --git a/tests/test_routes.py b/tests/test_routes.py index 04a682a0..b356c2d5 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -341,6 +341,7 @@ def test_websocket_route(): @app.websocket('/ws') async def handler(request, ws): + assert ws.subprotocol is None ev.set() request, response = app.test_client.get('/ws', headers={ @@ -352,6 +353,48 @@ def test_websocket_route(): assert ev.is_set() +def test_websocket_route_with_subprotocols(): + app = Sanic('test_websocket_route') + results = [] + + @app.websocket('/ws', subprotocols=['foo', 'bar']) + async def handler(request, ws): + results.append(ws.subprotocol) + + request, response = app.test_client.get('/ws', headers={ + 'Upgrade': 'websocket', + 'Connection': 'upgrade', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Protocol': 'bar'}) + assert response.status == 101 + + request, response = app.test_client.get('/ws', headers={ + 'Upgrade': 'websocket', + 'Connection': 'upgrade', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Protocol': 'bar, foo'}) + assert response.status == 101 + + request, response = app.test_client.get('/ws', headers={ + 'Upgrade': 'websocket', + 'Connection': 'upgrade', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': '13', + 'Sec-WebSocket-Protocol': 'baz'}) + assert response.status == 101 + + request, response = app.test_client.get('/ws', headers={ + 'Upgrade': 'websocket', + 'Connection': 'upgrade', + 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==', + 'Sec-WebSocket-Version': '13'}) + assert response.status == 101 + + assert results == ['bar', 'bar', None, None] + + def test_route_duplicate(): app = Sanic('test_route_duplicate') From d5d1d3b45a449b8b84aea0478712475bfb7ba26e Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Tue, 8 Aug 2017 21:58:10 -0700 Subject: [PATCH 028/104] add trigger before_start events in create_server --- sanic/app.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sanic/app.py b/sanic/app.py index f0ccad86..7587c0c2 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -633,14 +633,28 @@ class Sanic: warnings.simplefilter('default') warnings.warn("stop_event will be removed from future versions.", DeprecationWarning) + 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, has_log=log_config is not None) + # Trigger before_start events + await self.trigger_events(server_settings.get('before_start', []), server_settings.get('loop')) + return await serve(**server_settings) + async def trigger_events(self, events, loop): + """Trigger events (functions or async) + :param events: one or more sync or async functions to execute + :param loop: event loop + """ + for event in events: + result = event(loop) + if isawaitable(result): + await result + async def _run_request_middleware(self, request): # The if improves speed. I don't know why if self.request_middleware: From 80f27b1db9ae7c3eff4201bdc9b4b2896b076626 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Tue, 8 Aug 2017 22:21:40 -0700 Subject: [PATCH 029/104] add unit tests and make flake8 happy --- sanic/app.py | 5 ++++- tests/test_server_events.py | 17 +++++++++++++++++ tox.ini | 1 + 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/sanic/app.py b/sanic/app.py index 7587c0c2..c0c850e8 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -641,7 +641,10 @@ class Sanic: has_log=log_config is not None) # Trigger before_start events - await self.trigger_events(server_settings.get('before_start', []), server_settings.get('loop')) + await self.trigger_events( + server_settings.get('before_start', []), + server_settings.get('loop') + ) return await serve(**server_settings) diff --git a/tests/test_server_events.py b/tests/test_server_events.py index 0dcaba1c..d78f0aed 100644 --- a/tests/test_server_events.py +++ b/tests/test_server_events.py @@ -59,3 +59,20 @@ def test_all_listeners(): start_stop_app(random_name_app) for listener_name in AVAILABLE_LISTENERS: assert random_name_app.name + listener_name == output.pop() + + +async def test_trigger_before_events_create_server(): + + class MySanicDb: + pass + + app = Sanic("test_sanic_app") + + @app.listener('before_server_start') + async def init_db(app, loop): + app.db = MySanicDb() + + await app.create_server() + + assert hasattr(app, "db") + assert isinstance(app.db, MySanicDb) diff --git a/tox.ini b/tox.ini index de1dc2d5..dcebb050 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ deps = coverage pytest pytest-cov + pytest-sanic pytest-sugar aiohttp==1.3.5 chardet<=2.3.0 From fbb2344895ce45f8de30cc20b91e3ac5149ffdc7 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Thu, 10 Aug 2017 07:55:38 -0700 Subject: [PATCH 030/104] fix cov report --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index dcebb050..4c69cbaa 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,9 @@ deps = beautifulsoup4 gunicorn commands = - pytest tests --cov sanic --cov-report term-missing {posargs} + pytest tests --cov sanic --cov-report= {posargs} + coverage combine --append + coverage report -m [testenv:flake8] deps = From 756bd191810df48341f95e234ace411c763833ba Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Thu, 10 Aug 2017 08:39:02 -0700 Subject: [PATCH 031/104] do not fail if no files for coverage combine --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 4c69cbaa..ff43a139 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,7 @@ deps = gunicorn commands = pytest tests --cov sanic --cov-report= {posargs} - coverage combine --append + - coverage combine --append coverage report -m [testenv:flake8] From 2587f6753d20fa9791b19fc0725b8e4f8090b01d Mon Sep 17 00:00:00 2001 From: dongweiming Date: Tue, 15 Aug 2017 22:04:25 +0800 Subject: [PATCH 032/104] Fix blueprint doc --- docs/sanic/blueprints.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sanic/blueprints.md b/docs/sanic/blueprints.md index b4fd06d7..5be33cb6 100644 --- a/docs/sanic/blueprints.md +++ b/docs/sanic/blueprints.md @@ -172,7 +172,7 @@ takes the format `.`. For example: ```python @blueprint_v1.route('/') async def root(request): - url = app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5' + url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5' return redirect(url) From dd241bd6facbc3543133df5066d7c0277c1ae0a4 Mon Sep 17 00:00:00 2001 From: jiaxiaolei Date: Fri, 18 Aug 2017 17:00:34 +0800 Subject: [PATCH 033/104] docs(README): Make it clear and easy to read. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 410bd0b8..b410ddcf 100644 --- a/README.rst +++ b/README.rst @@ -31,13 +31,13 @@ Hello World Example Installation ------------ -- ``python -m pip install sanic`` +- ``pip install sanic`` To install sanic without uvloop or json using bash, you can provide either or both of these environmental variables using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features installation. -- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true python -m pip install sanic`` +- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip install sanic`` Documentation From a55efc832d19d4f67c061ecaf38798b56615e112 Mon Sep 17 00:00:00 2001 From: pkuphy Date: Sat, 19 Aug 2017 01:03:54 +0800 Subject: [PATCH 034/104] fix typo --- docs/sanic/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sanic/middleware.md b/docs/sanic/middleware.md index b2e8b45a..1a7f9d86 100644 --- a/docs/sanic/middleware.md +++ b/docs/sanic/middleware.md @@ -4,7 +4,7 @@ Middleware are functions which are executed before or after requests to the server. They can be used to modify the *request to* or *response from* user-defined handler functions. -Additionally, Sanic providers listeners which allow you to run code at various points of your application's lifecycle. +Additionally, Sanic provides listeners which allow you to run code at various points of your application's lifecycle. ## Middleware From 947364e15f72aa61b76c6c1246ba1891b8f84417 Mon Sep 17 00:00:00 2001 From: jiaxiaolei Date: Sun, 20 Aug 2017 11:11:14 +0800 Subject: [PATCH 035/104] feat(exapmles): add `add_task_sanic.py` --- examples/add_task_sanic.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 examples/add_task_sanic.py diff --git a/examples/add_task_sanic.py b/examples/add_task_sanic.py new file mode 100644 index 00000000..52b4e6bb --- /dev/null +++ b/examples/add_task_sanic.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +import asyncio + +from sanic import Sanic + +app = Sanic() + + +async def notify_server_started_after_five_seconds(): + await asyncio.sleep(5) + print('Server successfully started!') + +app.add_task(notify_server_started_after_five_seconds()) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) From 439ff11d1343b37a59915b2a42c570b6fca187f6 Mon Sep 17 00:00:00 2001 From: Igor Hatarist Date: Sun, 20 Aug 2017 19:28:09 +0300 Subject: [PATCH 036/104] Added a line on headers in the "Request Data" docs --- docs/sanic/request_data.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sanic/request_data.md b/docs/sanic/request_data.md index bf5ae4a8..a62b7e6a 100644 --- a/docs/sanic/request_data.md +++ b/docs/sanic/request_data.md @@ -71,6 +71,8 @@ The following variables are accessible as properties on `Request` objects: return text("You are trying to create a user with the following POST: %s" % request.body) ``` +- `headers` (dict) - A case-insensitive dictionary that contains the request headers. + - `ip` (str) - IP address of the requester. - `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object. From ef81a9f54703aa0adaa8132cc54855353c9a6e33 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 20 Aug 2017 23:11:38 -0700 Subject: [PATCH 037/104] make strict_slashes default value configurable --- sanic/app.py | 37 +++++++++++++++++++++++++------------ sanic/blueprints.py | 36 +++++++++++++++++++++++++----------- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index d4ee8275..53a6a8f6 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -28,7 +28,7 @@ class Sanic: def __init__(self, name=None, router=None, error_handler=None, load_env=True, request_class=None, - log_config=LOGGING): + log_config=LOGGING, strict_slashes=False): if log_config: logging.config.dictConfig(log_config) # Only set up a default log handler if the @@ -58,6 +58,7 @@ class Sanic: self._blueprint_order = [] self.debug = None self.sock = None + self.strict_slashes = strict_slashes self.listeners = defaultdict(list) self.is_running = False self.is_request_stream = False @@ -111,7 +112,7 @@ class Sanic: # Decorator def route(self, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=False, stream=False, version=None): + strict_slashes=None, stream=False, version=None): """Decorate a function to be registered as a route :param uri: path of the URL @@ -130,6 +131,9 @@ class Sanic: if stream: self.is_request_stream = True + if strict_slashes is None: + strict_slashes = self.strict_slashes + def response(handler): if stream: handler.is_stream = stream @@ -141,42 +145,42 @@ class Sanic: return response # Shorthand method decorators - def get(self, uri, host=None, strict_slashes=False, version=None): + def get(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=frozenset({"GET"}), host=host, strict_slashes=strict_slashes, version=version) - def post(self, uri, host=None, strict_slashes=False, stream=False, + def post(self, uri, host=None, strict_slashes=None, stream=False, version=None): return self.route(uri, methods=frozenset({"POST"}), host=host, strict_slashes=strict_slashes, stream=stream, version=version) - def put(self, uri, host=None, strict_slashes=False, stream=False, + def put(self, uri, host=None, strict_slashes=None, stream=False, version=None): return self.route(uri, methods=frozenset({"PUT"}), host=host, strict_slashes=strict_slashes, stream=stream, version=version) - def head(self, uri, host=None, strict_slashes=False, version=None): + def head(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=frozenset({"HEAD"}), host=host, strict_slashes=strict_slashes, version=version) - def options(self, uri, host=None, strict_slashes=False, version=None): + def options(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=frozenset({"OPTIONS"}), host=host, strict_slashes=strict_slashes, version=version) - def patch(self, uri, host=None, strict_slashes=False, stream=False, + def patch(self, uri, host=None, strict_slashes=None, stream=False, version=None): return self.route(uri, methods=frozenset({"PATCH"}), host=host, strict_slashes=strict_slashes, stream=stream, version=version) - def delete(self, uri, host=None, strict_slashes=False, version=None): + def delete(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=frozenset({"DELETE"}), host=host, strict_slashes=strict_slashes, version=version) def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=False, version=None): + strict_slashes=None, version=None): """A helper method to register class instance or functions as a handler to the application url routes. @@ -208,13 +212,16 @@ class Sanic: stream = True break + if strict_slashes is None: + strict_slashes = self.strict_slashes + self.route(uri=uri, methods=methods, host=host, strict_slashes=strict_slashes, stream=stream, version=version)(handler) return handler # Decorator - def websocket(self, uri, host=None, strict_slashes=False, + def websocket(self, uri, host=None, strict_slashes=None, subprotocols=None): """Decorate a function to be registered as a websocket route :param uri: path of the URL @@ -230,6 +237,9 @@ class Sanic: if not uri.startswith('/'): uri = '/' + uri + if strict_slashes is None: + strict_slashes = self.strict_slashes + def response(handler): async def websocket_handler(request, *args, **kwargs): request.app = self @@ -261,8 +271,11 @@ class Sanic: return response def add_websocket_route(self, handler, uri, host=None, - strict_slashes=False): + strict_slashes=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)(handler) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 0e97903b..b899481b 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -14,7 +14,11 @@ FutureStatic = namedtuple('Route', class Blueprint: - def __init__(self, name, url_prefix=None, host=None, version=None): + + def __init__(self, name, + url_prefix=None, + host=None, version=None, + strict_slashes=False): """Create a new blueprint :param name: unique name of the blueprint @@ -31,6 +35,7 @@ class Blueprint: self.middlewares = [] self.statics = [] self.version = version + self.strict_slashes = strict_slashes def register(self, app, options): """Register the blueprint to the sanic app.""" @@ -94,12 +99,15 @@ class Blueprint: app.listener(event)(listener) def route(self, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=False, stream=False, version=None): + strict_slashes=None, stream=False, version=None): """Create a blueprint route from a decorated function. :param uri: endpoint at which the route will be accessible. :param methods: list of acceptable HTTP methods. """ + if strict_slashes is None: + strict_slashes = self.strict_slashes + def decorator(handler): route = FutureRoute( handler, uri, methods, host, strict_slashes, stream, version) @@ -108,7 +116,7 @@ class Blueprint: return decorator def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=False, version=None): + strict_slashes=None, version=None): """Create a blueprint route from a function. :param handler: function for handling uri requests. Accepts function, @@ -125,6 +133,9 @@ class Blueprint: if getattr(handler.view_class, method.lower(), None): methods.add(method) + if strict_slashes is None: + strict_slashes = self.strict_slashes + # handle composition view differently if isinstance(handler, CompositionView): methods = handler.handlers.keys() @@ -133,11 +144,14 @@ class Blueprint: strict_slashes=strict_slashes, version=version)(handler) return handler - def websocket(self, uri, host=None, strict_slashes=False, version=None): + def websocket(self, uri, host=None, strict_slashes=None, version=None): """Create a blueprint websocket route from a decorated function. :param uri: endpoint at which the route will be accessible. """ + if strict_slashes is None: + strict_slashes = self.strict_slashes + def decorator(handler): route = FutureRoute(handler, uri, [], host, strict_slashes, False, version) @@ -199,36 +213,36 @@ class Blueprint: self.statics.append(static) # Shorthand method decorators - def get(self, uri, host=None, strict_slashes=False, version=None): + def get(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=["GET"], host=host, strict_slashes=strict_slashes, version=version) - def post(self, uri, host=None, strict_slashes=False, stream=False, + def post(self, uri, host=None, strict_slashes=None, stream=False, version=None): return self.route(uri, methods=["POST"], host=host, strict_slashes=strict_slashes, stream=stream, version=version) - def put(self, uri, host=None, strict_slashes=False, stream=False, + def put(self, uri, host=None, strict_slashes=None, stream=False, version=None): return self.route(uri, methods=["PUT"], host=host, strict_slashes=strict_slashes, stream=stream, version=version) - def head(self, uri, host=None, strict_slashes=False, version=None): + def head(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=["HEAD"], host=host, strict_slashes=strict_slashes, version=version) - def options(self, uri, host=None, strict_slashes=False, version=None): + def options(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=["OPTIONS"], host=host, strict_slashes=strict_slashes, version=version) - def patch(self, uri, host=None, strict_slashes=False, stream=False, + def patch(self, uri, host=None, strict_slashes=None, stream=False, version=None): return self.route(uri, methods=["PATCH"], host=host, strict_slashes=strict_slashes, stream=stream, version=version) - def delete(self, uri, host=None, strict_slashes=False, version=None): + def delete(self, uri, host=None, strict_slashes=None, version=None): return self.route(uri, methods=["DELETE"], host=host, strict_slashes=strict_slashes, version=version) From 5d23c7644b5003ad2cda6cae8fb42729db4e6628 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 20 Aug 2017 23:37:22 -0700 Subject: [PATCH 038/104] add unit tests --- tests/test_blueprints.py | 59 ++++++++++++++++++++++++++++++++++++++++ tests/test_routes.py | 30 ++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 5cb356c2..7e713da6 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -78,6 +78,65 @@ 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') + bp = Blueprint('test_text', strict_slashes=True) + + @bp.get('/get') + def handler(request): + return text('OK') + + @bp.post('/post/') + def handler(request): + return text('OK') + + app.blueprint(bp) + + request, response = app.test_client.get('/get/') + assert response.status == 404 + + 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') + bp = Blueprint('test_text') + + @bp.get('/get') + def handler(request): + return text('OK') + + @bp.post('/post/') + def handler(request): + return text('OK') + + app.blueprint(bp) + + request, response = app.test_client.get('/get/') + assert response.text == 'OK' + + 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') + bp = Blueprint('test_text', strict_slashes=True) + + @bp.get('/get', strict_slashes=False) + def handler(request): + return text('OK') + + @bp.post('/post/', strict_slashes=False) + def handler(request): + return text('OK') + + app.blueprint(bp) + + request, response = app.test_client.get('/get/') + assert response.text == 'OK' + + request, response = app.test_client.post('/post') + assert response.text == 'OK' def test_bp_with_url_prefix(): app = Sanic('test_text') diff --git a/tests/test_routes.py b/tests/test_routes.py index b356c2d5..b7228d29 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -71,6 +71,36 @@ def test_route_strict_slash(): request, response = app.test_client.post('/post') assert response.status == 404 +def test_route_strict_slash_default_value(): + app = Sanic('test_route_strict_slash', strict_slashes=True) + + @app.get('/get') + def handler(request): + return text('OK') + + request, response = app.test_client.get('/get/') + assert response.status == 404 + +def test_route_strict_slash_without_passing_default_value(): + app = Sanic('test_route_strict_slash') + + @app.get('/get') + def handler(request): + return text('OK') + + request, response = app.test_client.get('/get/') + assert response.text == 'OK' + +def test_route_strict_slash_default_value_can_be_overwritten(): + app = Sanic('test_route_strict_slash', strict_slashes=True) + + @app.get('/get', strict_slashes=False) + def handler(request): + return text('OK') + + request, response = app.test_client.get('/get/') + assert response.text == 'OK' + def test_route_optional_slash(): app = Sanic('test_route_optional_slash') From 63babae63db0f66ac0941f639c41fc7187ac7b98 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Mon, 21 Aug 2017 00:28:01 -0700 Subject: [PATCH 039/104] add doc --- docs/sanic/routing.md | 25 +++++++++++++++++++++++++ sanic/blueprints.py | 1 + 2 files changed, 26 insertions(+) diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index e039e249..f1882684 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -214,3 +214,28 @@ and `recv` methods to send and receive data respectively. WebSocket support requires the [websockets](https://github.com/aaugustin/websockets) package by Aymeric Augustin. + + +## About `strict_slashes` + +You can make `routes` strict to trailing slash or not, it's configurable. + +```python + +# provide default strict_slashes value for all routes +app = Sanic('test_route_strict_slash', strict_slashes=True) + +# you can also overwrite strict_slashes value for specific route +@app.get('/get', strict_slashes=False) +def handler(request): + return text('OK') + +# It also works for blueprints +bp = Blueprint('test_bp_strict_slash', strict_slashes=True) + +@bp.get('/bp/get', strict_slashes=False) +def handler(request): + return text('OK') + +app.blueprint(bp) +``` diff --git a/sanic/blueprints.py b/sanic/blueprints.py index b899481b..235fe909 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -23,6 +23,7 @@ class Blueprint: :param name: unique name of the blueprint :param url_prefix: URL to be prefixed before all route URLs + :param strict_slashes: strict to trailing slash """ self.name = name self.url_prefix = url_prefix From eab809d410ec3dd9291b2fe4e9936fa8b2eeafb0 Mon Sep 17 00:00:00 2001 From: lixxu Date: Mon, 21 Aug 2017 18:05:34 +0800 Subject: [PATCH 040/104] add name option for route building --- docs/sanic/routing.md | 28 +++ sanic/app.py | 64 +++--- sanic/blueprints.py | 88 +++++---- sanic/router.py | 16 +- tests/test_named_routes.py | 391 +++++++++++++++++++++++++++++++++++++ 5 files changed, 517 insertions(+), 70 deletions(-) create mode 100644 tests/test_named_routes.py diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index f1882684..b420a523 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -239,3 +239,31 @@ def handler(request): app.blueprint(bp) ``` + +## User defined route name + +You can pass `name` to change the route name to avoid using the default name (`handler.__name__`). + +```python + +app = Sanic('test_named_route') + +@app.get('/get', name='get_handler') +def handler(request): + return text('OK') + +# then you need use `app.url_for('get_handler')` +# instead of # `app.url_for('handler')` + +# It also works for blueprints +bp = Blueprint('test_named_bp') + +@bp.get('/bp/get', name='get_handler') +def handler(request): + return text('OK') + +app.blueprint(bp) + +# then you need use `app.url_for('test_named_bp.get_handler')` +# instead of `app.url_for('test_named_bp.handler')` +``` diff --git a/sanic/app.py b/sanic/app.py index 53a6a8f6..20c02a5c 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -112,7 +112,7 @@ class Sanic: # Decorator def route(self, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=None, stream=False, version=None): + strict_slashes=None, stream=False, version=None, name=None): """Decorate a function to be registered as a route :param uri: path of the URL @@ -120,6 +120,8 @@ class Sanic: :param host: :param strict_slashes: :param stream: + :param version: + :param name: user defined route name for url_for :return: decorated function """ @@ -139,48 +141,56 @@ class Sanic: handler.is_stream = stream self.router.add(uri=uri, methods=methods, handler=handler, host=host, strict_slashes=strict_slashes, - version=version) + version=version, name=name) return handler return response # Shorthand method decorators - def get(self, uri, host=None, strict_slashes=None, version=None): + def get(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=frozenset({"GET"}), host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) def post(self, uri, host=None, strict_slashes=None, stream=False, - version=None): + version=None, name=None): return self.route(uri, methods=frozenset({"POST"}), host=host, strict_slashes=strict_slashes, stream=stream, - version=version) + version=version, name=name) def put(self, uri, host=None, strict_slashes=None, stream=False, - version=None): + version=None, name=None): return self.route(uri, methods=frozenset({"PUT"}), host=host, strict_slashes=strict_slashes, stream=stream, - version=version) + version=version, name=name) - def head(self, uri, host=None, strict_slashes=None, version=None): + def head(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=frozenset({"HEAD"}), host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) - def options(self, uri, host=None, strict_slashes=None, version=None): + def options(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=frozenset({"OPTIONS"}), host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) def patch(self, uri, host=None, strict_slashes=None, stream=False, - version=None): + version=None, name=None): return self.route(uri, methods=frozenset({"PATCH"}), host=host, strict_slashes=strict_slashes, stream=stream, - version=version) + version=version, name=name) - def delete(self, uri, host=None, strict_slashes=None, version=None): + def delete(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=frozenset({"DELETE"}), host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=None, version=None): + strict_slashes=None, version=None, name=None): """A helper method to register class instance or functions as a handler to the application url routes. @@ -190,6 +200,9 @@ class Sanic: :param methods: list or tuple of methods allowed, these are overridden if using a HTTPMethodView :param host: + :param strict_slashes: + :param version: + :param name: user defined route name for url_for :return: function or class instance """ stream = False @@ -217,12 +230,12 @@ class Sanic: self.route(uri=uri, methods=methods, host=host, strict_slashes=strict_slashes, stream=stream, - version=version)(handler) + version=version, name=name)(handler) return handler # Decorator def websocket(self, uri, host=None, strict_slashes=None, - subprotocols=None): + subprotocols=None, name=None): """Decorate a function to be registered as a websocket route :param uri: path of the URL :param subprotocols: optional list of strings with the supported @@ -265,19 +278,19 @@ class Sanic: self.router.add(uri=uri, handler=websocket_handler, methods=frozenset({'GET'}), host=host, - strict_slashes=strict_slashes) + strict_slashes=strict_slashes, name=name) return handler return response def add_websocket_route(self, handler, uri, host=None, - strict_slashes=None): + strict_slashes=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)(handler) + return self.websocket(uri, host=host, strict_slashes=strict_slashes, + name=name)(handler) def enable_websocket(self, enable=True): """Enable or disable the support for websocket. @@ -400,9 +413,8 @@ class Sanic: uri, route = self.router.find_route_by_view_name(view_name) if not uri or not route: - raise URLBuildError( - 'Endpoint with name `{}` was not found'.format( - view_name)) + raise URLBuildError('Endpoint with name `{}` was not found'.format( + view_name)) if uri != '/' and uri.endswith('/'): uri = uri[:-1] diff --git a/sanic/blueprints.py b/sanic/blueprints.py index 235fe909..548aa7ca 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -5,7 +5,7 @@ from sanic.views import CompositionView FutureRoute = namedtuple('Route', ['handler', 'uri', 'methods', 'host', - 'strict_slashes', 'stream', 'version']) + 'strict_slashes', 'stream', 'version', 'name']) FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host']) FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs']) FutureException = namedtuple('Route', ['handler', 'args', 'kwargs']) @@ -53,14 +53,14 @@ class Blueprint: version = future.version or self.version - app.route( - uri=uri[1:] if uri.startswith('//') else uri, - methods=future.methods, - host=future.host or self.host, - strict_slashes=future.strict_slashes, - stream=future.stream, - version=version - )(future.handler) + app.route(uri=uri[1:] if uri.startswith('//') else uri, + methods=future.methods, + host=future.host or self.host, + strict_slashes=future.strict_slashes, + stream=future.stream, + version=version, + name=future.name, + )(future.handler) for future in self.websocket_routes: # attach the blueprint name to the handler so that it can be @@ -68,11 +68,11 @@ class Blueprint: future.handler.__blueprintname__ = self.name # Prepend the blueprint URI prefix if available uri = url_prefix + future.uri if url_prefix else future.uri - app.websocket( - uri=uri, - host=future.host or self.host, - strict_slashes=future.strict_slashes - )(future.handler) + app.websocket(uri=uri, + host=future.host or self.host, + strict_slashes=future.strict_slashes, + name=future.name, + )(future.handler) # Middleware for future in self.middlewares: @@ -100,7 +100,7 @@ class Blueprint: app.listener(event)(listener) def route(self, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=None, stream=False, version=None): + strict_slashes=None, stream=False, version=None, name=None): """Create a blueprint route from a decorated function. :param uri: endpoint at which the route will be accessible. @@ -111,19 +111,24 @@ class Blueprint: def decorator(handler): route = FutureRoute( - handler, uri, methods, host, strict_slashes, stream, version) + handler, uri, methods, host, strict_slashes, stream, version, + name) self.routes.append(route) return handler return decorator def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None, - strict_slashes=None, version=None): + strict_slashes=None, version=None, name=None): """Create a blueprint route from a function. :param handler: function for handling uri requests. Accepts function, or class instance with a view_class method. :param uri: endpoint at which the route will be accessible. :param methods: list of acceptable HTTP methods. + :param host: + :param strict_slashes: + :param version: + :param name: user defined route name for url_for :return: function or class instance """ # Handle HTTPMethodView differently @@ -142,10 +147,12 @@ class Blueprint: methods = handler.handlers.keys() self.route(uri=uri, methods=methods, host=host, - strict_slashes=strict_slashes, version=version)(handler) + strict_slashes=strict_slashes, version=version, + name=name)(handler) return handler - def websocket(self, uri, host=None, strict_slashes=None, version=None): + def websocket(self, uri, host=None, strict_slashes=None, version=None, + name=None): """Create a blueprint websocket route from a decorated function. :param uri: endpoint at which the route will be accessible. @@ -155,12 +162,13 @@ class Blueprint: def decorator(handler): route = FutureRoute(handler, uri, [], host, strict_slashes, - False, version) + False, version, name) self.websocket_routes.append(route) return handler return decorator - def add_websocket_route(self, handler, uri, host=None, version=None): + def add_websocket_route(self, handler, uri, host=None, version=None, + name=None): """Create a blueprint websocket route from a function. :param handler: function for handling uri requests. Accepts function, @@ -168,7 +176,7 @@ class Blueprint: :param uri: endpoint at which the route will be accessible. :return: function or class instance """ - self.websocket(uri=uri, host=host, version=version)(handler) + self.websocket(uri=uri, host=host, version=version, name=name)(handler) return handler def listener(self, event): @@ -214,36 +222,44 @@ class Blueprint: self.statics.append(static) # Shorthand method decorators - def get(self, uri, host=None, strict_slashes=None, version=None): + def get(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=["GET"], host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) def post(self, uri, host=None, strict_slashes=None, stream=False, - version=None): + version=None, name=None): return self.route(uri, methods=["POST"], host=host, strict_slashes=strict_slashes, stream=stream, - version=version) + version=version, name=name) def put(self, uri, host=None, strict_slashes=None, stream=False, - version=None): + version=None, name=None): return self.route(uri, methods=["PUT"], host=host, strict_slashes=strict_slashes, stream=stream, - version=version) + version=version, name=name) - def head(self, uri, host=None, strict_slashes=None, version=None): + def head(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=["HEAD"], host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) - def options(self, uri, host=None, strict_slashes=None, version=None): + def options(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=["OPTIONS"], host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) def patch(self, uri, host=None, strict_slashes=None, stream=False, - version=None): + version=None, name=None): return self.route(uri, methods=["PATCH"], host=host, strict_slashes=strict_slashes, stream=stream, - version=version) + version=version, name=name) - def delete(self, uri, host=None, strict_slashes=None, version=None): + def delete(self, uri, host=None, strict_slashes=None, version=None, + name=None): return self.route(uri, methods=["DELETE"], host=host, - strict_slashes=strict_slashes, version=version) + strict_slashes=strict_slashes, version=version, + name=name) diff --git a/sanic/router.py b/sanic/router.py index efc48f37..062fecc8 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -99,7 +99,7 @@ class Router: return name, _type, pattern def add(self, uri, methods, handler, host=None, strict_slashes=False, - version=None): + version=None, name=None): """Add a handler to the route list :param uri: path to match @@ -118,7 +118,7 @@ class Router: else: uri = "/".join(["/v{}".format(str(version)), uri]) # add regular version - self._add(uri, methods, handler, host) + self._add(uri, methods, handler, host, name) if strict_slashes: return @@ -135,12 +135,12 @@ class Router: ) # add version with trailing slash if slash_is_missing: - self._add(uri + '/', methods, handler, host) + self._add(uri + '/', methods, handler, host, name) # add version without trailing slash elif without_slash_is_missing: - self._add(uri[:-1], methods, handler, host) + self._add(uri[:-1], methods, handler, host, name) - def _add(self, uri, methods, handler, host=None): + def _add(self, uri, methods, handler, host=None, name=None): """Add a handler to the route list :param uri: path to match @@ -161,7 +161,7 @@ class Router: "host strings, not {!r}".format(host)) for host_ in host: - self.add(uri, methods, handler, host_) + self.add(uri, methods, handler, host_, name) return # Dict for faster lookups of if method allowed @@ -236,9 +236,9 @@ class Router: # if available if hasattr(handler, '__blueprintname__'): handler_name = '{}.{}'.format( - handler.__blueprintname__, handler.__name__) + handler.__blueprintname__, name or handler.__name__) else: - handler_name = getattr(handler, '__name__', None) + handler_name = name or getattr(handler, '__name__', None) route = Route( handler=handler, methods=methods, pattern=pattern, diff --git a/tests/test_named_routes.py b/tests/test_named_routes.py new file mode 100644 index 00000000..730a2206 --- /dev/null +++ b/tests/test_named_routes.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import asyncio +import pytest + +from sanic import Sanic +from sanic.blueprints import Blueprint +from sanic.response import text +from sanic.router import RouteExists, RouteDoesNotExist +from sanic.exceptions import URLBuildError +from sanic.constants import HTTP_METHODS + + +# ------------------------------------------------------------ # +# UTF-8 +# ------------------------------------------------------------ # + +@pytest.mark.parametrize('method', HTTP_METHODS) +def test_versioned_named_routes_get(method): + app = Sanic('test_shorhand_routes_get') + bp = Blueprint('test_bp', url_prefix='/bp') + + method = method.lower() + route_name = 'route_{}'.format(method) + route_name2 = 'route2_{}'.format(method) + + func = getattr(app, method) + if callable(func): + @func('/{}'.format(method), version=1, name=route_name) + def handler(request): + return text('OK') + else: + print(func) + raise + + func = getattr(bp, method) + if callable(func): + @func('/{}'.format(method), version=1, name=route_name2) + def handler2(request): + return text('OK') + + else: + print(func) + raise + + app.blueprint(bp) + + assert app.router.routes_all['/v1/{}'.format(method)].name == route_name + + route = app.router.routes_all['/v1/bp/{}'.format(method)] + assert route.name == 'test_bp.{}'.format(route_name2) + + assert app.url_for(route_name) == '/v1/{}'.format(method) + url = app.url_for('test_bp.{}'.format(route_name2)) + assert url == '/v1/bp/{}'.format(method) + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_shorthand_default_routes_get(): + app = Sanic('test_shorhand_routes_get') + + @app.get('/get') + def handler(request): + return text('OK') + + assert app.router.routes_all['/get'].name == 'handler' + assert app.url_for('handler') == '/get' + + +def test_shorthand_named_routes_get(): + app = Sanic('test_shorhand_routes_get') + bp = Blueprint('test_bp', url_prefix='/bp') + + @app.get('/get', name='route_get') + def handler(request): + return text('OK') + + @bp.get('/get', name='route_bp') + def handler2(request): + return text('Blueprint') + + app.blueprint(bp) + + assert app.router.routes_all['/get'].name == 'route_get' + assert app.url_for('route_get') == '/get' + with pytest.raises(URLBuildError): + app.url_for('handler') + + assert app.router.routes_all['/bp/get'].name == 'test_bp.route_bp' + assert app.url_for('test_bp.route_bp') == '/bp/get' + with pytest.raises(URLBuildError): + app.url_for('test_bp.handler2') + + +def test_shorthand_named_routes_post(): + app = Sanic('test_shorhand_routes_post') + + @app.post('/post', name='route_name') + def handler(request): + return text('OK') + + assert app.router.routes_all['/post'].name == 'route_name' + assert app.url_for('route_name') == '/post' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_shorthand_named_routes_put(): + app = Sanic('test_shorhand_routes_put') + + @app.put('/put', name='route_put') + def handler(request): + assert request.stream is None + return text('OK') + + assert app.is_request_stream is False + assert app.router.routes_all['/put'].name == 'route_put' + assert app.url_for('route_put') == '/put' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_shorthand_named_routes_delete(): + app = Sanic('test_shorhand_routes_delete') + + @app.delete('/delete', name='route_delete') + def handler(request): + assert request.stream is None + return text('OK') + + assert app.is_request_stream is False + assert app.router.routes_all['/delete'].name == 'route_delete' + assert app.url_for('route_delete') == '/delete' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_shorthand_named_routes_patch(): + app = Sanic('test_shorhand_routes_patch') + + @app.patch('/patch', name='route_patch') + def handler(request): + assert request.stream is None + return text('OK') + + assert app.is_request_stream is False + assert app.router.routes_all['/patch'].name == 'route_patch' + assert app.url_for('route_patch') == '/patch' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_shorthand_named_routes_head(): + app = Sanic('test_shorhand_routes_head') + + @app.head('/head', name='route_head') + def handler(request): + assert request.stream is None + return text('OK') + + assert app.is_request_stream is False + assert app.router.routes_all['/head'].name == 'route_head' + assert app.url_for('route_head') == '/head' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_shorthand_named_routes_options(): + app = Sanic('test_shorhand_routes_options') + + @app.options('/options', name='route_options') + def handler(request): + assert request.stream is None + return text('OK') + + assert app.is_request_stream is False + assert app.router.routes_all['/options'].name == 'route_options' + assert app.url_for('route_options') == '/options' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_named_static_routes(): + app = Sanic('test_dynamic_route') + + @app.route('/test', name='route_test') + async def handler1(request): + return text('OK1') + + @app.route('/pizazz', name='route_pizazz') + async def handler2(request): + return text('OK2') + + assert app.router.routes_all['/test'].name == 'route_test' + assert app.router.routes_static['/test'].name == 'route_test' + assert app.url_for('route_test') == '/test' + with pytest.raises(URLBuildError): + app.url_for('handler1') + + assert app.router.routes_all['/pizazz'].name == 'route_pizazz' + assert app.router.routes_static['/pizazz'].name == 'route_pizazz' + assert app.url_for('route_pizazz') == '/pizazz' + with pytest.raises(URLBuildError): + app.url_for('handler2') + + +def test_named_dynamic_route(): + app = Sanic('test_dynamic_route') + + results = [] + + @app.route('/folder/', name='route_dynamic') + async def handler(request, name): + results.append(name) + return text('OK') + + assert app.router.routes_all['/folder/'].name == 'route_dynamic' + assert app.url_for('route_dynamic', name='test') == '/folder/test' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_dynamic_named_route_regex(): + app = Sanic('test_dynamic_route_regex') + + @app.route('/folder/', name='route_re') + async def handler(request, folder_id): + return text('OK') + + route = app.router.routes_all['/folder/'] + assert route.name == 'route_re' + assert app.url_for('route_re', folder_id='test') == '/folder/test' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_dynamic_named_route_path(): + app = Sanic('test_dynamic_route_path') + + @app.route('//info', name='route_dynamic_path') + async def handler(request, path): + return text('OK') + + route = app.router.routes_all['//info'] + assert route.name == 'route_dynamic_path' + assert app.url_for('route_dynamic_path', path='path/1') == '/path/1/info' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_dynamic_named_route_unhashable(): + app = Sanic('test_dynamic_route_unhashable') + + @app.route('/folder//end/', + name='route_unhashable') + async def handler(request, unhashable): + return text('OK') + + route = app.router.routes_all['/folder//end/'] + assert route.name == 'route_unhashable' + url = app.url_for('route_unhashable', unhashable='test/asdf') + assert url == '/folder/test/asdf/end' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_websocket_named_route(): + app = Sanic('test_websocket_route') + ev = asyncio.Event() + + @app.websocket('/ws', name='route_ws') + async def handler(request, ws): + assert ws.subprotocol is None + ev.set() + + assert app.router.routes_all['/ws'].name == 'route_ws' + assert app.url_for('route_ws') == '/ws' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_websocket_named_route_with_subprotocols(): + app = Sanic('test_websocket_route') + results = [] + + @app.websocket('/ws', subprotocols=['foo', 'bar'], name='route_ws') + async def handler(request, ws): + results.append(ws.subprotocol) + + assert app.router.routes_all['/ws'].name == 'route_ws' + assert app.url_for('route_ws') == '/ws' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_static_add_named_route(): + app = Sanic('test_static_add_route') + + async def handler1(request): + return text('OK1') + + async def handler2(request): + return text('OK2') + + app.add_route(handler1, '/test', name='route_test') + app.add_route(handler2, '/test2', name='route_test2') + + assert app.router.routes_all['/test'].name == 'route_test' + assert app.router.routes_static['/test'].name == 'route_test' + assert app.url_for('route_test') == '/test' + with pytest.raises(URLBuildError): + app.url_for('handler1') + + assert app.router.routes_all['/test2'].name == 'route_test2' + assert app.router.routes_static['/test2'].name == 'route_test2' + assert app.url_for('route_test2') == '/test2' + with pytest.raises(URLBuildError): + app.url_for('handler2') + + +def test_dynamic_add_named_route(): + app = Sanic('test_dynamic_add_route') + + results = [] + + async def handler(request, name): + results.append(name) + return text('OK') + + app.add_route(handler, '/folder/', name='route_dynamic') + assert app.router.routes_all['/folder/'].name == 'route_dynamic' + assert app.url_for('route_dynamic', name='test') == '/folder/test' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_dynamic_add_named_route_unhashable(): + app = Sanic('test_dynamic_add_route_unhashable') + + async def handler(request, unhashable): + return text('OK') + + app.add_route(handler, '/folder//end/', + name='route_unhashable') + route = app.router.routes_all['/folder//end/'] + assert route.name == 'route_unhashable' + url = app.url_for('route_unhashable', unhashable='folder1') + assert url == '/folder/folder1/end' + with pytest.raises(URLBuildError): + app.url_for('handler') + + +def test_overload_routes(): + app = Sanic('test_dynamic_route') + + @app.route('/overload', methods=['GET'], name='route_first') + async def handler1(request): + return text('OK1') + + @app.route('/overload', methods=['POST', 'PUT'], name='route_second') + async def handler2(request): + return text('OK2') + + @app.route('/overload2', methods=['POST', 'PUT'], name='route_third') + async def handler3(request): + return text('OK2') + + request, response = app.test_client.get(app.url_for('route_first')) + assert response.text == 'OK1' + + request, response = app.test_client.post(app.url_for('route_first')) + assert response.text == 'OK2' + + request, response = app.test_client.put(app.url_for('route_first')) + assert response.text == 'OK2' + + assert app.router.routes_all['/overload'].name == 'route_first' + with pytest.raises(URLBuildError): + app.url_for('handler1') + + with pytest.raises(URLBuildError): + app.url_for('handler2') + + with pytest.raises(URLBuildError): + app.url_for('route_second') + + assert app.url_for('route_third') == '/overload2' + with pytest.raises(URLBuildError): + app.url_for('handler3') From 91f031b66182bd5985f42018aca59e06c99cabde Mon Sep 17 00:00:00 2001 From: jiaxiaolei Date: Mon, 21 Aug 2017 22:28:23 +0800 Subject: [PATCH 041/104] feat(examples): add `authorized_sanic.py` You can check a request if the client is authorized to access a resource by the decorator `authorized` --- examples/authorized_sanic.py | 42 ++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 examples/authorized_sanic.py diff --git a/examples/authorized_sanic.py b/examples/authorized_sanic.py new file mode 100644 index 00000000..f6b17426 --- /dev/null +++ b/examples/authorized_sanic.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +from sanic import Sanic +from functools import wraps +from sanic.response import json + +app = Sanic() + + +def check_request_for_authorization_status(request): + # Note: Define your check, for instance cookie, session. + flag = True + return flag + + +def authorized(): + def decorator(f): + @wraps(f) + async def decorated_function(request, *args, **kwargs): + # run some method that checks the request + # for the client's authorization status + is_authorized = check_request_for_authorization_status(request) + + if is_authorized: + # the user is authorized. + # run the handler method and return the response + response = await f(request, *args, **kwargs) + return response + else: + # the user is not authorized. + return json({'status': 'not_authorized'}, 403) + return decorated_function + return decorator + + +@app.route("/") +@authorized() +async def test(request): + return json({'status': 'authorized'}) + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) From 762b2782eef869584ff45738649451a2c82f26a8 Mon Sep 17 00:00:00 2001 From: lixxu Date: Tue, 22 Aug 2017 14:02:38 +0800 Subject: [PATCH 042/104] use name to define route name for different methods on same url --- docs/sanic/routing.md | 34 +++++++++++++++++++++++++++++++ sanic/router.py | 41 +++++++++++++++++++++----------------- tests/test_named_routes.py | 27 +++++++++++-------------- 3 files changed, 69 insertions(+), 33 deletions(-) diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index b420a523..49b7c0b8 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -266,4 +266,38 @@ app.blueprint(bp) # then you need use `app.url_for('test_named_bp.get_handler')` # instead of `app.url_for('test_named_bp.handler')` + +# different names can be used for same url with different methods + +@app.get('/test', name='route_test') +def handler(request): + return text('OK') + +@app.post('/test', name='route_post') +def handler2(request): + return text('OK POST') + +@app.put('/test', name='route_put') +def handler3(request): + return text('OK PUT') + +# below url are the same, you can use any of them +# '/test' +app.url_for('route_test') +# app.url_for('route_post') +# app.url_for('route_put') + +# for same handler name with different methods +# you need specify the name (it's url_for issue) +@app.get('/get') +def handler(request): + return text('OK') + +@app.post('/post', name='post_handler') +def handler(request): + return text('OK') + +# then +# app.url_for('handler') == '/get' +# app.url_for('post_handler') == '/post' ``` diff --git a/sanic/router.py b/sanic/router.py index 062fecc8..79faaf1e 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -67,6 +67,7 @@ class Router: def __init__(self): self.routes_all = {} + self.routes_names = {} self.routes_static = {} self.routes_dynamic = defaultdict(list) self.routes_always_check = [] @@ -125,13 +126,12 @@ class Router: # Add versions with and without trailing / slash_is_missing = ( - not uri[-1] == '/' - and not self.routes_all.get(uri + '/', False) + not uri[-1] == '/' and not self.routes_all.get(uri + '/', False) ) without_slash_is_missing = ( - uri[-1] == '/' - and not self.routes_all.get(uri[:-1], False) - and not uri == '/' + uri[-1] == '/' and not + self.routes_all.get(uri[:-1], False) and not + uri == '/' ) # add version with trailing slash if slash_is_missing: @@ -229,22 +229,26 @@ class Router: else: route = self.routes_all.get(uri) + # prefix the handler name with the blueprint name + # if available + if hasattr(handler, '__blueprintname__'): + handler_name = '{}.{}'.format( + handler.__blueprintname__, name or handler.__name__) + else: + handler_name = name or getattr(handler, '__name__', None) + if route: route = merge_route(route, methods, handler) else: - # prefix the handler name with the blueprint name - # if available - if hasattr(handler, '__blueprintname__'): - handler_name = '{}.{}'.format( - handler.__blueprintname__, name or handler.__name__) - else: - handler_name = name or getattr(handler, '__name__', None) - route = Route( handler=handler, methods=methods, pattern=pattern, parameters=parameters, name=handler_name, uri=uri) self.routes_all[uri] = route + pairs = self.routes_names.get(handler_name) + if not (pairs and (pairs[0] + '/' == uri or uri + '/' == pairs[0])): + self.routes_names[handler_name] = (uri, route) + if properties['unhashable']: self.routes_always_check.append(route) elif parameters: @@ -265,6 +269,11 @@ class Router: uri = host + uri try: route = self.routes_all.pop(uri) + for handler_name, pairs in self.routes_names.items(): + if pairs[0] == uri: + self.routes_names.pop(handler_name) + break + except KeyError: raise RouteDoesNotExist("Route was not registered: {}".format(uri)) @@ -289,11 +298,7 @@ class Router: if not view_name: return (None, None) - for uri, route in self.routes_all.items(): - if route.name == view_name: - return uri, route - - return (None, None) + return self.routes_names.get(view_name, (None, None)) def get(self, request): """Get a request handler based on the URL of the request, or raises an diff --git a/tests/test_named_routes.py b/tests/test_named_routes.py index 730a2206..ca377e8d 100644 --- a/tests/test_named_routes.py +++ b/tests/test_named_routes.py @@ -7,7 +7,6 @@ import pytest from sanic import Sanic from sanic.blueprints import Blueprint from sanic.response import text -from sanic.router import RouteExists, RouteDoesNotExist from sanic.exceptions import URLBuildError from sanic.constants import HTTP_METHODS @@ -360,11 +359,7 @@ def test_overload_routes(): return text('OK1') @app.route('/overload', methods=['POST', 'PUT'], name='route_second') - async def handler2(request): - return text('OK2') - - @app.route('/overload2', methods=['POST', 'PUT'], name='route_third') - async def handler3(request): + async def handler1(request): return text('OK2') request, response = app.test_client.get(app.url_for('route_first')) @@ -376,16 +371,18 @@ def test_overload_routes(): request, response = app.test_client.put(app.url_for('route_first')) assert response.text == 'OK2' + request, response = app.test_client.get(app.url_for('route_second')) + assert response.text == 'OK1' + + request, response = app.test_client.post(app.url_for('route_second')) + assert response.text == 'OK2' + + request, response = app.test_client.put(app.url_for('route_second')) + assert response.text == 'OK2' + assert app.router.routes_all['/overload'].name == 'route_first' with pytest.raises(URLBuildError): app.url_for('handler1') - with pytest.raises(URLBuildError): - app.url_for('handler2') - - with pytest.raises(URLBuildError): - app.url_for('route_second') - - assert app.url_for('route_third') == '/overload2' - with pytest.raises(URLBuildError): - app.url_for('handler3') + assert app.url_for('route_first') == '/overload' + assert app.url_for('route_second') == app.url_for('route_first') From 35e028cd99aebc500f9cac428ba328c17da8e043 Mon Sep 17 00:00:00 2001 From: xmsun Date: Tue, 22 Aug 2017 16:40:42 +0800 Subject: [PATCH 043/104] fix: error param --- examples/try_everything.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/try_everything.py b/examples/try_everything.py index 2bc1c7b3..76967be1 100644 --- a/examples/try_everything.py +++ b/examples/try_everything.py @@ -18,7 +18,7 @@ def test_sync(request): return response.json({"test": True}) -@app.route("/dynamic//") +@app.route("/dynamic//") def test_params(request, name, i): return response.text("yeehaww {} {}".format(name, i)) From 6038813d0324bd0b7000b993d3ea7ffadc836678 Mon Sep 17 00:00:00 2001 From: Darren Date: Thu, 24 Aug 2017 22:46:39 +0800 Subject: [PATCH 044/104] fix #914, change arguments of Unauthorized.__init__ --- sanic/exceptions.py | 29 ++++++++++++++++++----------- tests/test_exceptions.py | 20 +++++++++++++++++--- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 0edb0562..9663ea7c 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -209,6 +209,7 @@ class Unauthorized(SanicException): Unauthorized exception (401 HTTP status code). :param message: Message describing the exception. + :param status_code: HTTP Status code. :param scheme: Name of the authentication scheme to be used. When present, kwargs is used to complete the WWW-Authentication header. @@ -216,11 +217,13 @@ class Unauthorized(SanicException): Examples:: # With a Basic auth-scheme, realm MUST be present: - raise Unauthorized("Auth required.", "Basic", realm="Restricted Area") + raise Unauthorized("Auth required.", + scheme="Basic", + realm="Restricted Area") # With a Digest auth-scheme, things are a bit more complicated: raise Unauthorized("Auth required.", - "Digest", + scheme="Digest", realm="Restricted Area", qop="auth, auth-int", algorithm="MD5", @@ -228,20 +231,24 @@ class Unauthorized(SanicException): opaque="zyxwvu") # With a Bearer auth-scheme, realm is optional so you can write: - raise Unauthorized("Auth required.", "Bearer") + raise Unauthorized("Auth required.", scheme="Bearer") # or, if you want to specify the realm: - raise Unauthorized("Auth required.", "Bearer", realm="Restricted Area") + raise Unauthorized("Auth required.", + scheme="Bearer", + realm="Restricted Area") """ - def __init__(self, message, scheme, **kwargs): - super().__init__(message) + def __init__(self, message, status_code=None, scheme=None, **kwargs): + super().__init__(message, status_code) - values = ["{!s}={!r}".format(k, v) for k, v in kwargs.items()] - challenge = ', '.join(values) + # if auth-scheme is specified, set "WWW-Authenticate" header + if scheme is not None: + values = ["{!s}={!r}".format(k, v) for k, v in kwargs.items()] + challenge = ', '.join(values) - self.headers = { - "WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip() - } + self.headers = { + "WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip() + } def abort(status_code, message=None): diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 1521c9ed..c535059c 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -31,14 +31,18 @@ def exception_app(): def handler_403(request): raise Forbidden("Forbidden") + @app.route('/401') + def handler_401(request): + raise Unauthorized("Unauthorized") + @app.route('/401/basic') def handler_401_basic(request): - raise Unauthorized("Unauthorized", "Basic", realm="Sanic") + raise Unauthorized("Unauthorized", scheme="Basic", realm="Sanic") @app.route('/401/digest') def handler_401_digest(request): raise Unauthorized("Unauthorized", - "Digest", + scheme="Digest", realm="Sanic", qop="auth, auth-int", algorithm="MD5", @@ -47,12 +51,16 @@ def exception_app(): @app.route('/401/bearer') def handler_401_bearer(request): - raise Unauthorized("Unauthorized", "Bearer") + raise Unauthorized("Unauthorized", scheme="Bearer") @app.route('/invalid') def handler_invalid(request): raise InvalidUsage("OK") + @app.route('/abort/401') + def handler_invalid(request): + abort(401) + @app.route('/abort') def handler_invalid(request): abort(500) @@ -124,6 +132,9 @@ def test_forbidden_exception(exception_app): def test_unauthorized_exception(exception_app): """Test the built-in Unauthorized exception""" + request, response = exception_app.test_client.get('/401') + assert response.status == 401 + request, response = exception_app.test_client.get('/401/basic') assert response.status == 401 assert response.headers.get('WWW-Authenticate') is not None @@ -186,5 +197,8 @@ def test_exception_in_exception_handler_debug_off(exception_app): def test_abort(exception_app): """Test the abort function""" + request, response = exception_app.test_client.get('/abort/401') + assert response.status == 401 + request, response = exception_app.test_client.get('/abort') assert response.status == 500 From 0a72168f8f925f593a6ae0e8f5e4f26a6cfcd53b Mon Sep 17 00:00:00 2001 From: Timur Date: Tue, 29 Aug 2017 23:05:57 +0300 Subject: [PATCH 045/104] Example logging X-Request-Id transparently --- examples/log_request_id.py | 86 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 examples/log_request_id.py diff --git a/examples/log_request_id.py b/examples/log_request_id.py new file mode 100644 index 00000000..a6dba418 --- /dev/null +++ b/examples/log_request_id.py @@ -0,0 +1,86 @@ +''' +Based on example from https://github.com/Skyscanner/aiotask-context +and `examples/{override_logging,run_async}.py`. + +Needs https://github.com/Skyscanner/aiotask-context/tree/52efbc21e2e1def2d52abb9a8e951f3ce5e6f690 or newer + +$ pip install git+https://github.com/Skyscanner/aiotask-context.git +''' + +import asyncio +import uuid +import logging +from signal import signal, SIGINT + +from sanic import Sanic +from sanic import response + +import uvloop +import aiotask_context as context + +log = logging.getLogger(__name__) + + +class RequestIdFilter(logging.Filter): + def filter(self, record): + record.request_id = context.get('X-Request-ID') + return True + + +LOG_SETTINGS = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'level': 'DEBUG', + 'formatter': 'default', + 'filters': ['requestid'], + }, + }, + 'filters': { + 'requestid': { + '()': RequestIdFilter, + }, + }, + 'formatters': { + 'default': { + 'format': '%(asctime)s %(levelname)s %(name)s:%(lineno)d %(request_id)s | %(message)s', + }, + }, + 'loggers': { + '': { + 'level': 'DEBUG', + 'handlers': ['console'], + 'propagate': True + }, + } +} + + +app = Sanic(__name__, log_config=LOG_SETTINGS) + + +@app.middleware('request') +async def set_request_id(request): + request_id = request.headers.get('X-Request-ID') or str(uuid.uuid4()) + context.set("X-Request-ID", request_id) + + +@app.route("/") +async def test(request): + log.debug('X-Request-ID: %s', context.get('X-Request-ID')) + log.info('Hello from test!') + return response.json({"test": True}) + + +if __name__ == '__main__': + asyncio.set_event_loop(uvloop.new_event_loop()) + server = app.create_server(host="0.0.0.0", port=8000) + loop = asyncio.get_event_loop() + loop.set_task_factory(context.task_factory) + task = asyncio.ensure_future(server) + try: + loop.run_forever() + except: + loop.stop() From f49554aa5723d8730dbd23c884b5d207a5f32d3b Mon Sep 17 00:00:00 2001 From: Maksim Anisenkov Date: Wed, 30 Aug 2017 15:28:12 +0200 Subject: [PATCH 046/104] Fix LICENSE date and name --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 63b4b681..74ee7987 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) [year] [fullname] +Copyright (c) 2016-present Channel Cat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. From c59a8a60eb7ceaf3b8f93a9abcd1db639338ecb9 Mon Sep 17 00:00:00 2001 From: Tim Mundt Date: Tue, 5 Sep 2017 09:53:33 +0200 Subject: [PATCH 047/104] make the prefix for environment variables alterable --- sanic/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index 6ffcf7a1..6a53e476 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -195,14 +195,14 @@ class Config(dict): if key.isupper(): self[key] = getattr(obj, key) - def load_environment_vars(self): + def load_environment_vars(self, prefix=SANIC_PREFIX): """ - Looks for any ``SANIC_`` prefixed environment variables and applies + Looks for prefixed environment variables and applies them to the configuration if present. """ for k, v in os.environ.items(): - if k.startswith(SANIC_PREFIX): - _, config_key = k.split(SANIC_PREFIX, 1) + if k.startswith(prefix): + _, config_key = k.split(prefix, 1) try: self[config_key] = int(v) except ValueError: From 97d8b9e90806e735104657e658f83c0e94215085 Mon Sep 17 00:00:00 2001 From: Tim Mundt Date: Tue, 5 Sep 2017 10:41:55 +0200 Subject: [PATCH 048/104] documentation for env var prefix; allow passing in the prefix through the app constructor --- docs/sanic/config.md | 10 ++++++++-- sanic/config.py | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/sanic/config.md b/docs/sanic/config.md index ab63f7c8..01ee7fc3 100644 --- a/docs/sanic/config.md +++ b/docs/sanic/config.md @@ -29,9 +29,15 @@ In general the convention is to only have UPPERCASE configuration parameters. Th There are several ways how to load configuration. -### From environment variables. +### From Environment Variables -Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically. You can pass the `load_env` boolean to the Sanic constructor to override that: +Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically and fed into the `REQUEST_TIMEOUT` config variable. You can pass a different prefix to Sanic: + +```python +app = Sanic(load_env='MYAPP_') +``` + +Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead: ```python app = Sanic(load_env=False) diff --git a/sanic/config.py b/sanic/config.py index 6a53e476..18514425 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -131,7 +131,8 @@ class Config(dict): self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec if load_env: - self.load_environment_vars() + prefix = SANIC_PREFIX if load_env == True else load_env + self.load_environment_vars(prefix=load_env) def __getattr__(self, attr): try: From 9572ecc5ea2f6346c9a7879b1093719fd8b6b495 Mon Sep 17 00:00:00 2001 From: Tim Mundt Date: Tue, 5 Sep 2017 10:58:48 +0200 Subject: [PATCH 049/104] test for env var prefix --- sanic/config.py | 2 +- tests/test_config.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index 18514425..853e4a99 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -132,7 +132,7 @@ class Config(dict): if load_env: prefix = SANIC_PREFIX if load_env == True else load_env - self.load_environment_vars(prefix=load_env) + self.load_environment_vars(prefix=prefix) def __getattr__(self, attr): try: diff --git a/tests/test_config.py b/tests/test_config.py index aa7a0e4d..e393d02b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -19,15 +19,21 @@ def test_load_from_object(): def test_auto_load_env(): environ["SANIC_TEST_ANSWER"] = "42" app = Sanic() - assert app.config.TEST_ANSWER == "42" + assert app.config.TEST_ANSWER == 42 del environ["SANIC_TEST_ANSWER"] -def test_auto_load_env(): +def test_dont_load_env(): environ["SANIC_TEST_ANSWER"] = "42" app = Sanic(load_env=False) assert getattr(app.config, 'TEST_ANSWER', None) == 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') config = b""" From e2e25eb751beb4879be6b64ddb2ded6c48301c96 Mon Sep 17 00:00:00 2001 From: Tim Mundt Date: Tue, 5 Sep 2017 11:05:31 +0200 Subject: [PATCH 050/104] fixed flake convention --- sanic/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/config.py b/sanic/config.py index 853e4a99..e8846465 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -131,7 +131,7 @@ class Config(dict): self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec if load_env: - prefix = SANIC_PREFIX if load_env == True else load_env + prefix = SANIC_PREFIX if load_env is True else load_env self.load_environment_vars(prefix=prefix) def __getattr__(self, attr): From bc20dc5c62ba21f4668caaef6b69a963619fd35d Mon Sep 17 00:00:00 2001 From: lixxu Date: Wed, 6 Sep 2017 19:17:52 +0800 Subject: [PATCH 051/104] use url_for for url building for static files --- docs/sanic/blueprints.md | 9 +- docs/sanic/routing.md | 31 +++ docs/sanic/static_files.md | 30 ++- sanic/app.py | 35 ++- sanic/blueprints.py | 5 + sanic/router.py | 31 ++- sanic/static.py | 9 +- tests/static/bp/decode me.txt | 1 + tests/static/bp/python.png | Bin 0 -> 11252 bytes tests/static/bp/test.file | 1 + tests/test_url_building.py | 6 +- tests/test_url_for_static.py | 446 ++++++++++++++++++++++++++++++++++ 12 files changed, 589 insertions(+), 15 deletions(-) create mode 100644 tests/static/bp/decode me.txt create mode 100644 tests/static/bp/python.png create mode 100644 tests/static/bp/test.file create mode 100644 tests/test_url_for_static.py diff --git a/docs/sanic/blueprints.md b/docs/sanic/blueprints.md index 5be33cb6..1a7c5293 100644 --- a/docs/sanic/blueprints.md +++ b/docs/sanic/blueprints.md @@ -93,7 +93,14 @@ def ignore_404s(request, exception): Static files can be served globally, under the blueprint prefix. ```python -bp.static('/folder/to/serve', '/web/path') + +# suppose bp.name == 'bp' + +bp.static('/web/path', '/folder/to/serve') +# also you can pass name parameter to it for url_for +bp.static('/web/path', '/folder/to/server', name='uploads') +app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt' + ``` ## Start and stop diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index 49b7c0b8..7a562b66 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -301,3 +301,34 @@ def handler(request): # app.url_for('handler') == '/get' # app.url_for('post_handler') == '/post' ``` + +## Build URL for static files + +You can use `url_for` for static file url building now. +If it's for file directly, `filename` can be ignored. + +```python + +app = Sanic('test_static') +app.static('/static', './static') +app.static('/uploads', './uploads', name='uploads') +app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') + +bp = Blueprint('bp', url_prefix='bp') +bp.static('/static', './static') +bp.static('/uploads', './uploads', name='uploads') +bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') +app.blueprint(bp) + +# then build the url +app.url_for('static', filename='file.txt') == '/static/file.txt' +app.url_for('static', name='static', filename='file.txt') == '/static/file.txt' +app.url_for('static', name='uploads', filename='file.txt') == 'uploads/file.txt' +app.url_for('static', name='best_png') == '/the_best.png' + +# blueprint url building +app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt' +app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt' +app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png' + +``` diff --git a/docs/sanic/static_files.md b/docs/sanic/static_files.md index f0ce9d78..3419cad1 100644 --- a/docs/sanic/static_files.md +++ b/docs/sanic/static_files.md @@ -6,16 +6,40 @@ filename. The file specified will then be accessible via the given endpoint. ```python from sanic import Sanic +from sanic.blueprints import Blueprint + app = Sanic(__name__) # Serves files from the static folder to the URL /static app.static('/static', './static') +# use url_for to build the url, name defaults to 'static' and can be ignored +app.url_for('static', filename='file.txt') == '/static/file.txt' +app.url_for('static', name='static', filename='file.txt') == '/static/file.txt' # Serves the file /home/ubuntu/test.png when the URL /the_best.png # is requested -app.static('/the_best.png', '/home/ubuntu/test.png') +app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') + +# you can use url_for to build the static file url +# you can ignore name and filename parameters if you don't define it +app.url_for('static', name='best_png') == '/the_best.png' +app.url_for('static', name='best_png', filename='any') == '/the_best.png' + +# you need define the name for other static files +app.static('/another.png', '/home/ubuntu/another.png', name='another') +app.url_for('static', name='another') == '/another.png' +app.url_for('static', name='another', filename='any') == '/another.png' + +# also, you can use static for blueprint +bp = Blueprint('bp', url_prefix='/bp') +bp.static('/static', './static') + +# servers the file directly +bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') +app.blueprint(bp) + +app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt' +app.url_for('static', name='bp.best_png') == '/bp/test_best.png' app.run(host="0.0.0.0", port=8000) ``` - -Note: currently you cannot build a URL for a static file using `url_for`. diff --git a/sanic/app.py b/sanic/app.py index 20c02a5c..3776c915 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -354,13 +354,13 @@ class Sanic: # Static Files def static(self, uri, file_or_directory, pattern=r'/?.+', use_modified_since=True, use_content_range=False, - stream_large_files=False): + stream_large_files=False, name='static'): """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) + stream_large_files, name) def blueprint(self, blueprint, **options): """Register a blueprint on the application. @@ -410,12 +410,32 @@ class Sanic: URLBuildError """ # find the route by the supplied view name - uri, route = self.router.find_route_by_view_name(view_name) + kw = {} + # special static files url_for + if view_name == 'static': + kw.update(name=kwargs.pop('name', 'static')) + elif view_name.endswith('.static'): # blueprint.static + kwargs.pop('name', None) + kw.update(name=view_name) - if not uri or not route: + uri, route = self.router.find_route_by_view_name(view_name, **kw) + if not (uri and route): raise URLBuildError('Endpoint with name `{}` was not found'.format( view_name)) + if view_name == 'static' or view_name.endswith('.static'): + filename = kwargs.pop('filename', None) + # it's static folder + if 'dk`%$jxm&aCH| zHSaGv&Xz5k0?IN*P$Rw-~F0AT2l5f@SOTEF!7ZllsvzjmzA zSGQ~v+wR`XyHem4^OPP1z~f89nNV=BO`?eii>nnwM$17Ky!u7ch6KE(D&u2iJ&vKF z$%&d_z(FSqONu5d%(QkmZlB7jH?Q~vUg)cLOm|TlpBxQ)Y6L#!xnAM_QTTY$@kQ=w zMqy1(iZ!F8z#o&AWdC1YwQEG5k5dAd7Yaf(FT06)JjZQ|uRVwN`pDQU?Q=G`m*u|0 zIjb5Gv}2SK;4}4929sO4ILHhQ*9+L*mxkPTWf5NI=J7fFIQ+S6@>t-E*OJ_b#>J)BT+FdtV_mRl(=xmqPqnbHA6KD)L1j8c z$tJfbZK4`r3@NS#YfWvPFZ(OwDdW}p`euITM#Zsy_S^#(mFeLs^Sq49dVO>061A4E z%-nP4xGZqf*MI}^MS9dc@SV$RrK5GX*z$6#KJ|Xab;Il6LHwU<@)&3tq4;m93x zF53f2)fBmj039^II!OkJr2N7J4+a`)VJoR?sh87M!?`e=Jg8230yA~bjE^UG`}BQL zFaNfG&8`5uwtD0#ebl)IcH>NYu>-#<(MN?U-sjJ^f@UtQcdMJ%{;Ub+BV&G@$>gl$ z`9HNx#iel74ZVJ|=LA7L3DY-0*9EIy?|+r`4UwU>qyDbcxscp2a;)%7@;h-pXWjHtO5pAyW#5&w9v|~E z99H!j(`h$D4Qapj`Q4+k*YM=&_V9Wm{Hy!3MSJSiahp}rQ6JCf^Yyvg2Kp|6zQAvU z@FttE#Yob_T9hI-_^>dDb~*m$6I)mU1;o$4kGd0X0r zsrQq?9(B+?O)?ul7r_weoDI;6@rY8F6ITE#kWnhOEkq}_Qm|(b6SXU^5LxSmYeU@^ zR8Npyx_xJYTsatS$5bCmku8O$xBM@$DH5+^`1hNy2QOviwMu1q(+%O3+|HJnw0ax} zHW&AfI-Yi4|6T<*_}dYg{_J9Q=<)@=9lu2q$b&&h;ovD~JR~T?4P2K9cd{~=*_O{& z3@1dh{>;qW-f4k1uYd|)jqSas^QVbtlCOU>J5RP6G_!LyyjcQWc#47^`E{#Yw`$6q zS#Z=2CbhQC6PJ9mSwfK+`hL_c1>Qu%1n4fx7n$0FLI&QL-48X_yG^im1Ak5{des(w za7|pcsZ4ogB&6nQqqah+D{Ty?GwC|*%oasCRI>P{VfRf|L_{H z!|4)*&VdM1H+r5FA>ytl4Rky8_Uh-M0YxE2!iB>xV9Muu-m$eEE$jbVkMr!Ol&7p= zGc%BFRN|`vQtN%}UL9*BX-inZXfuB{2Q$`vrt8Eow$>Mq4w=kcZH~Hj!1p3^5Pw|t z$7#lKzHI%pg)eqv6ln!Os~a6(3Dg(2{oBC2y|*Ol*n3-!=entmApzHIJy31TYJ`^URU5LsXK2vd1~HANcluK(W30PW`gD z{n-b1;G1kPtyEYM?Qz1wfk{$tjZ83y%EhnCrC@UD6)YyJzY*~EJa@@x{PFr2SzPY) zt4peu!D|}3&A7T$Nw~7dwjS_jlexMX00zUvxQnFjZ3!Qj%3?!7*9 z_$FM-yTby)Ut2YAjeGi=@%FX)rxof{X$j zP_)y7^oUwdofN5q&_|ygYVSd>?oo`8M661Fo+A*G?!Zg}+vYPk;QP%Sbca~!dU}0RTO=nTIEqZo9)=h4WvdhrmW3`AD`|%&HC%x} zSNnOz>;}Z24(yebi~E+S+vj=B3!bQJhk`xwRe~XqiKUG1^nOyp(Pn$pbt%lNR1ws0 zOjgH63#t?y6csN7)tivFheg6*_TkhgzoA;C(cf1>@;t?>o55X5#x_{QaX@_m}k}^u1EFf3_Rd=9n~Om~<`)ClCH7jodV32cl5EOl(i1jQ{|u_qh*~b5=bDEdXg7ME2{n&E z&XSC8$2N+s4K&lSc1hZ$7p6ZW+x4%T?L+6bsnU$nBpn@YL&Rkpz&qy02bzdQu7cqs zGZV#udjmP3d-KfR$Z092$s&ZOTnF}hm^IJ8}81fgFI%E{|Bq|f?b0^?NDyeUtY)oV; zurf%O#VCDO-Kh4%6h=f{Dt@Ia<|mnvvdjG=e;TDaV$Ph?##v(=?*S_vZGA@{u{y1| z`huQ<0te6cnYOCZKY85))#K(8vE@3^cBzXZ#~hRyk<3QmwXXBr1obN> zv-gJ*YujjcwaW2%Bx83%_xY69t>*GR9gWA!)zC>>7KwelbSv8_JScMfpeOys8aW1N zKE1B3vq&pIP?nLd9JNn6QOrv` z>h(vSBlbeW`@kgq6652EZV;D9d zGvPtg7+`#8WzdO?ttGniS6|J)n*yr1sPL#V$Ql2WFC_-;KeI8gu%4~OAqCec>(?LW zG@lcXzXYCnd~WZBe)Eb1OfqaiP)E%KtZtNevPjesL<;6c{{2P77bcbK%&5R?fw8bJ zAKPj|ng2uAh@OP(Hmz%~{u7PvilMrf_{`zHNS7g6FGz1=7vWtvUH7MUyh3Y49^dtF zt$s%^fphap#+!3}f$feLHj4!a4igVag;uMJP7@7!f5Fo9vTVdRL2!0wK4O%IbTa<& z$WhS!wzv7$)1Qa}m${=J?|acdr~ZcjZcKd>J`Wi*QAd3?ZJnn(S|I9Gw{zkQ=f+N{ zvW6bCIEFDvTtLi%UzB#a1XU)QsHugJiU?q}i4kvr8H5ddu8nsObUU!JO*|4iW4>uC z2PcO6JGaGv4-r3W2&b&RPBI)4tkMwBJ={<|kBc}MSvayctX1OYG%ABUoDpo~UiyMr zy69UA{AE-Pcm4hyCR@)v6K0aa%u{Rk%Xpx-$?NKIpo2TuuTRKf$W3hd z`bymK>gd6*D$;b|-_U)_t~IUzKnANQ#ik+)yD%+XV_h1ePzUOJ9fZu4xAH+if}0Vq zaLp=$);R*gvQ8wn))<=*7p$%QE`A0)3O)ZN_)gaze%cSWgOP(wlnhu{T?qQ&!PYiq zQS>5$Mp!o_;vtU>W&kJB>zJN5?EW(Z%L^?nPw%c&+ix_S7sCb~e-wQ6QhD{)`5jpz z-ccBSD##7C_`iw1kL1M~6oP~371o=8sC@U(NSva-41?6OcV;`6IXR(II?H%J(;f?T zQ1c&mqOb#PuhETcne6gQ{$82{1fVE|8EfiV)Pmieos_}l?}F&UX(Axn%F)Tt%Ab1A z7WTZbQz($im}-o1{To#P`fe{OC2Y?uOl0{kFrCpE&_#+3%s6 z-IxVm54^dLnFBM|hYy66tY8iq8hZoOI34cQC5+kZmnAgu=T^CH1fbTsCajsKx_R^-$vd5Vg5y9NMHtFKWEQV+Cxh#MB5c)uxhs=~fBiSeJIO^;nD?TJY2b7Fjicv2`pfH!y8T~R z#+RM&*0yQxbK9CK3hLxUO|d$o1-0mg)vi(kkrRQHMSUPI%@1RHX9)Y|K$M_wNI#K3 zzay3V=K98Y=oG7DO{_ZMpW_q~C}J3Sn1vO?DRW~~-o#9U8X`?$4^ahENx#2>-h)7^YKF5dRwdr{6SC-XX7>h4W_pRs5 zuyuz2!wN^=X)_iDy3ewa;RyO7L&7SL$eAOetBrj;|3uPpd&hP2^bratY_gD_U1flB z3siIT20n*kL2?+J@=8yC$@YDJX@K{(REey?e z6HQExBwSR~jFbRuHae!lZ9ncmrDxFEvcuiUxU4+`H!X+MT_IqFLepnbHVb6+ z4%TbqZieY+=H|NX;$vH9nXq`+Hx5B!=ZM)OP=_amiM$TlT9AHt5++}haGzy&nvNOy ztCFN4wit&|eojszQBx1I34S*S)}6Kd%_-5kAQCtmp`~kMxb)+qIk6qG%qdc-6dPY> zxCV=xA7#;ps=|*YSEkn5rK0PK=ZQeyubj_O!eEhH%{a~Yy}Md%sm?zwI$A#UJQheL+v7P*w2+=<1&K#&k~#mdzJ)MTs1-AeerS z)`dLV$3eSjjDD2k_$5Y*wUI(RGInI6!wIzRIYje$+wQgQ=y!4g_A7LBTGL!1h1z*% z6;RUSH^rMo$~n%=;fv*tu0kti&6E4qsC!{X1lVmtIy?Yia!V-4F51%Ayjv5{KV{rn zTcP(4?N7O@)$_}a*H$?mFPi`|2H8MVh^b<*@(sA5Vf)n2CHT+O8&pPk-&qf)4VV0J zd_9bJ(W13hH?+AMFc|&9$sxp;+q&*om$jODJQq1%lnUlp|4?Du(b-mq;k<EMO(RnCZy0bz_^l5+gPs0TEjU0Qb$c zXh>YJx-sYo#UB^a!DldidJos((?4MP#nD^ZtorS9(x~U8YB0vPE)@nn^>2!Ca6QDT z6tr9KcXdJ&vCkt&CYq#c|y zpdKDzfkIR(`WBK+fXrtXHgL#d*I-1yj+VAutkqP^>^jgw!d0gvcM9RQ)U6OdG3~vr zhOy>Kyv=yG-BXXG_Y0-S2+W=dy=Q2eS!8FV1@R57Og`M1ASeN4R54)b@6D-Wbsya? zCI^_vC}k*a`d)KY-L5pAC7(?X7_jMpG!jMphCgQZ0@)zl^D?<=tT}ad<+~e`U*(<& z$^3NkFD8k9l=-4+$_Y&-@qQt2hTgB%+kILkz5#X*ZmmYEwlsW&vi%}EA#Z*j#_LOP zYI>)tAKge4`=(ml){~RF9M@e~8J#ElLK>WI1*N9Q`Hwei8WhE@S@_{?^F{-QW4|j@ z!pg(GF(e-F8=>@y`x#I*J41RtuBxI88$=OlD&#JcXWShK9SCO(>Ik9An?A zM>mH*IVe=oV5*+*6w&E_0nFPavX8q*e)!Fag^L)1cJxQ&-B6oZ(OdWW8u#J$?c}t7 zS-9xp*eq9{wJ^5QL$2DoIO}gVo6`~;e~vG8lw&gyd&>)^4rfl3P)4lb;omi@GdHhb z86siZx_HqdrV)qhWX~$a5Z61f)C$u*$*aM@==8baINRCBtDPsUU>J=lu^E~paI}k5F~b|i zWdRhTAJTN47RwfUbfywJC;lQpfc*YAD3=44R}P>XC|whq; zzMj$!SmP5{b1M9R!Kk@HeJ0SH8gl=ksP$pEoDn-u6a%|PTq{d)KlnbWqs2g9Ng8oR znE%?!?kB@!zRqVcxBCWo?s;994}$szV*E4bO+ zm2}_g7+r~~T%S!p-Gg>U?W91BA;RhHPTOsXoPb%P7+eF)oC{^SGNiE|j@kN~?dghm zyvPVo=b|~t9BNqM5Dar=tE;Ax)ug$qT=07HauBna0I(4t${Krn`;)I$h??qJ3X(?x z5`fO;Y#m;&a%4Tq8No?trCn9Z9x7~fBoHAB4fCcQ9K#H%ES5#kM_Ym^FZ)JPxD`T; zDvt;ERVGrQ)U;@2TXWG`%%z=Fount)l;mJ0BZ;L2g3uJR+Hk@>64@f+6RWc=Rh_u7xUy1c^U!IUuynLu;QRPC$ z?NGoUB^~zbZY{HyC18a1=Iw2l-C`44xoc z+ZLHPimZXPQP)9WpnCnwXAA}~@@YqBi{kmC7A-AgcIP4UWRjbOl4M>}U6Smyfb z;^YU>=Yq}pR!hN^pN81}3FQHAu3Vd&n*rk%en*P={(~>$EFTLW{{|u)2Gu(ZNbDU9 z8_NnxaqS3tE_tRT&t)#hRLf|V^FkWjQAJYaUas>YT2N1~0e$K4iN-?0&E54j!VKYd zKSf29xoiu#vlxlI6OrG5*rhrX?%YpBf*op_tOeLG8tOzGssP(AOd1ic@-t6}_pEZi z?8w)Qyj!-;n?()N%T>#Smy$292OQh|6bPBZ7Jg!>yfPc4sW`PyCLTgB%AFq`#(v&=r&W>x`s^Ie-S3=>Cf z8p{?#1}o(8-bqgn+d)|g{rMZBKu9~(B(20bRw6uy8+~}A`T1O2EIo$3aWADv4R2H7 zI>;N}I7zqt7}9^_ly)OIxBdGR3q7ekfwZbEDC4HoHm+ET-fjc5rYX^LW79@1BJHPwf z+fsbhHz_Gaa*)X=oEc&TTL=uk4TV9Zp!~I>V`y`0yR>Gs#1v~nO1dMxB1vwap9I%~|OeB@~LxA5po)#&MalCxBwUOS= zsT(p8D;6RUE52wE1cytGMFxL&MLc^!Ha_ZQbxmw=(Ol%j5cl{P18N=i_}wBvPp(#m zq;i2(DyEcIo1NtbgUUYXfSmQ;zXc`#?%I&DpAhe^tCjx{Ia7c2HC1xMKcdL5gkgj# zbKMDI*{%(35*dxRFN}pbh$#mLE^YZw44nhIl}Jo;xBoy;#gHp4oTu?~2=tM91ORwp z1oS?ByJ|%E#ZJ^Dh_zQyOJtDl+@leOQMI8>6zeJRG8)22Hc07;ezDuM;o7v3APT12 zDAbgbR_9@@R7w0&=%q3A>9cg+xv4mAB5-wNPjzHuw>C|oF`D|bU}OcD4-%rN;7Y_5 z_-|UuNU)cQb3df2n={$8Rr>+J#_%)XE>liV!i0m&VJ5r2f;LmKxYL-fwl)u*_^Ntf z&rn3Y19O=@B)HlFx1w;nZ?1zJ^d4 zPW*F5j^9$8OV%R!>Y}-Ynn$5VoG~#~X+V2Ovq3b_(BMjB<}&Cbg>Vwvim`(P6GxA~ z3iYgHBAB_UQ>Vsyj*TdHedOE1q`f~G%$CB5RrJXxkx&be#wpjEGvkQ2_*=e4L->d3 zy4Tl|<6V8AL<&^R>%wWjNf2{I#&mI#?(Ul7g z7cOOsb3}=->n>EA?=1BqJ0dpfWon(5*Aq-GGvdm^{5O*I9d9DKGT?kaA2{loSALrV z)J8bNx!g!dSu_~N+_)#a4J8~JzHX@g_l!vW_GKGBmcC*$FHl`%p&-7E6E#`tVaXv< zFZO#s@>uQ$%U$O(RU|_h%E@*A3nMg;Wm*3mm!&ANjkVMgR}Ph&2N$78|LF^03HmO5 zqXpSWd{I-`=c+TQb^qpEF0$q3!;BMrtT(=pj%8B0QB9itqvk8hW?;XvS<_vuTv}=W zNQC)Fx7F_j#D51a zo5#ZseTMgoWm$qcu)Infp9_~G9vvTIEZ6Yc@8C5CaB`8BT5!hlHyS_vDfG$@eVeRy zpGJO`g8B~8-4X!qpn7Cf@#$2$DxgYS#$3BPGq0=Dy^GxUfwoEG4dYp7dx zcuTT#(xB+Ted`{uH9uEkk+Z3KhZB^rZc8H00jSrWmE~C)(7ibNNCV~ql^ob1&KMK> zDhN}Kr=DoxzO=bgXMLq`!WWMIYNC*luAogFXTjio;{nY*+6lY&tj3k?#QgVI+XRyT z*B}!&H=2F`wu`0oNpm?Xtf9|Q4!*s}i9Ml=5AiKE*Ck{KEILVQl_C#{0dZfWSYuO= zknzlYy&4ve3#lgRNZR9vQpD48u^{1m#KdW)L`@iEBd=OSp9@?BEgbn*dEs;zK#I(T zj3M?qy#b)I!bt-ufwIJ?)fTK*%;)2MKXICnHT~Wo0{m}dgNmS+BJ0NhZp153S~L|p zTy9`e_JBERC!>fd?kpNdGtSsj>yZWl8_7+PGUeqL5SGJVA()mdbi*8UsxFjfSq`Z` z>Q{d2c$dPxMOGPS7rquI8l&_l($&HFgwd#g?f84)d*LHZ%99sN+&;dNpzwMhwboqX zWvFk{0$-N4*I)V@g`6KmfDjooj@$Q{QFVdy{kiH^7IRf<7jto=_uYl{{iCbF95XaH z5@uNK=HiS>HAGC*Po_8wQw&XwjP@=;Eb20!JN>w^u{L0ykkz(+2y8*;q?Dtba$I>? z)oQV^IP=(I6Y_dR7?^B1WH2Fs1~7ExM-@gdVt-}?C*OYFnk{Spek&BXsQZ{|8E}6R z@_yRm$Hp9R=_@jeib^&Ua@OJ#B_})YuZFg^$||Yx$RUq4 zSPzr_J5|)f{c512GR@t~Jt))d-VnMLsGKW^E>~u<;?*1K z?`DAbsg6q?b|9pfY6$*YvzLeaG;$T+wR!xqmoD8()F)p1oVRP-Oez{Wpb$y}V`XwU7m-zJ< z*Aw>ec)fyie0@gYQw}Fo1NhzwDJFbI*EEdVF*K9K9OYmB%)!QU-yK`TK?FjkA~|y{ zXNYDnDvZo){~3Po)Mf3aW6!@j7hR~y=X;WR%1*C61c}nr<41&3RGCUo8M2N=kFQp( zSoH_vLO2<9o2T`#64~^=?SM<@p%DrnAow_pySFFwK1*Kma;`Sz=5XnuIyfR2I%Zo_ z=y0LpQ0h|HL?CpbRiMXEK91H~$pA(4=_P%eqoR;vB5X=KM8;VdrlE>t0_$Mh9n&6- zfWqk=mPamq2hEvY86=*lfCxfC#xk`Ajx`+L!#gc|QTghUK@NlIG_cY~G7z7h zRD-^2YR_ewz$}fQuBZz^3`$j}T{HIqsL&0s^o?Vfg>p!~; zp7}4TRFt1;!^{B(fjxMDn?a%X9YMa)?b#p62~m3{wheh%E$H=SXBczIo%y`$9amjOy~N~%X=FOX&? znRe7s745IAjwdQAbSiZ!Y$|0cY;tw-a<4*HjGSZowOam^^LfVC13$+LLwJB?uE&GM?k^i1 zPpHqp9b}o$;Y;C9WXC5wHlJ+UJ#=^j!PPx3f0+xc;*MK%afmDc9;L5&flL46$-s z)|&e3lSD&3pH+J4^^p=DcMoU>Nps1{NtQRIDruH^ClcQiHs?fS)ybA0daOz;%`Mt3*ss4)gjV7DmY;5(AKtVTp$EeGm`0kv zA|I5&beAF3834gV;;MFs4+=#(_a80ipvcc zB!~ZY_L3WT&kHA;Nz9iKQ@#Mj*ns!*Zf)C|-X|EKW*KMU9_<>*fWRzF$ilN|Qq2dL zI@2in<_8?TL%w;sMH&oRkV@#2v!cXX=y88jJdALn8&20|MoGs$lr*r>ic`lZ-biB3 za254m0qV~r>aTL0DH1KIJh~qt*b{5FWof~8`m^{DT0(rhAs{F_ltdWc*XUOP!;lvW zGSTx@!@j9$l{@;*FD7EykIt`K(DIg)j+_8$t`K3KT(*KuAW4CK9jZ`q7Tih*l?Y-e$QB zsMR1Yalc5J-M-geE&VIdrMqDzL0vP0Lm2cfcG&Aj2tBnNkNTUEOS{3w|gy%RcH&*F^O1ENgQ{B7T1 z_-wdRL2#gg1(HQ-)W|76@0*ASFYjfs{9{(?C(IxU%7!gt-%D+=67H9+g>2E)z_zaQ zRrfAOnrKx-6Czc}Jse?VLu~?&<>>7Jq`-Ko!d#w09vIc00|BUuY^CO%M0kMY@>+=9 zZ9Hk^l(#48>%9=b+oqlzvOxr6pTUx}HSjiF?5_sZXlJ%US;6;PS4)yfSnYQ<+x9BK zi0UC_N~%R-2QhcH+T@DNqzl^mK_>8DG7ez$aGwPdQFC7z%y%Xw=lYlob(aT=ZfKd~ zjPv@-lE!-%s}{8eX5uMNF2eg$hpqm#nfK@a;1ue=1)t0p{H#G;jWrCwtQXFotUWmN zEz|H~|IWt*TT36K=9wQ{HW3Hq6RmxXa*P&2qjXr638@L7a;P)$U*6@l{@jb8{UzwMVtT(Sx0*KFrk;bhKf z3J!?qvRp}(3rr9Mp;@GvrsG*HPV~MCa$yVNly7_h1>NG4aU;oHAI9&E{-%~*&N2vv z!tW7BvWadaAy-I7s+4g!$=u<_AJx$x$Y_^&K8s6LIA)Cmif7AxF*nJwo8e=%v2O$HWmj8~(PZ{k3akK+b#D(Ya#Bw>) zjE~&gWS1I?y)%K9<#xZN3R0i=a{?m513} z`6QX`2;<;d@O-+Wq-{W41m133-{OO9AzpFFEs5IV>J>5thsPy_? zz&!q~Jwt`$6MSc)g6Uk&$bI%!{g#SUtfIu5IGd(W9btq*;l&~l@BLEFf$04o(pzsd3H@~+bs0P9U$1**4LFXDgYRvlHv_S| zn8^3y4RwFkYdxoFttb(P1uM8VZ}hCJj=(GYs!1VAzA>WR)gVpL10QNe{z;)RQJmYhL#E9S=-Ym7$q$m_&8sH0;LXc+v2+V?6H_T_%~-eHQe; z0nM<5E?kRS)$}W(hrsd;36FYc6MLbHk~Sg4-9T{eec|!$8y~XZXRD0c9S$c)REt83 z`=8CkhAnU_`)=ka89QZ`v6jUo+XETLQ`IVubT{g(m^5ms^1irz?w7* Date: Wed, 6 Sep 2017 19:19:59 +0800 Subject: [PATCH 052/104] missing '/' in doc --- docs/sanic/routing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index 7a562b66..98179e17 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -323,7 +323,7 @@ app.blueprint(bp) # then build the url app.url_for('static', filename='file.txt') == '/static/file.txt' app.url_for('static', name='static', filename='file.txt') == '/static/file.txt' -app.url_for('static', name='uploads', filename='file.txt') == 'uploads/file.txt' +app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt' app.url_for('static', name='best_png') == '/the_best.png' # blueprint url building From 73c04f5a8970a77bd1af9ad9f7e3423137d3318e Mon Sep 17 00:00:00 2001 From: Anatoly Ivanov Date: Fri, 8 Sep 2017 14:21:49 +0300 Subject: [PATCH 053/104] Added information on request.token The manual lacked info about request.token, which keeps authorization data. See https://github.com/channelcat/sanic/blob/master/sanic/request.py#L84 for details --- docs/sanic/request_data.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sanic/request_data.md b/docs/sanic/request_data.md index a62b7e6a..e778faf6 100644 --- a/docs/sanic/request_data.md +++ b/docs/sanic/request_data.md @@ -97,6 +97,7 @@ The following variables are accessible as properties on `Request` objects: - `path`: The path of the request: `/posts/1/` - `query_string`: The query string of the request: `foo=bar` or a blank string `''` - `uri_template`: Template for matching route handler: `/posts//` +- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=` ## Accessing values using `get` and `getlist` From c4417b399b85a52589409da616840c262395cc05 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Fri, 8 Sep 2017 17:47:05 -0700 Subject: [PATCH 054/104] fixing debug logging --- sanic/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index e8846465..0c2cc701 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -81,11 +81,11 @@ LOGGING = { }, 'loggers': { 'sanic': { - 'level': 'DEBUG', + 'level': 'INFO', 'handlers': ['internal', 'errorStream'] }, 'network': { - 'level': 'DEBUG', + 'level': 'INFO', 'handlers': ['accessStream', 'errorStream'] } } From c9a40c180a60f7c6f4943c619d81d71093e703c1 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 10 Sep 2017 11:11:16 -0700 Subject: [PATCH 055/104] remove some logging stuff --- sanic/app.py | 18 ++--------- sanic/config.py | 83 ------------------------------------------------- sanic/log.py | 13 -------- 3 files changed, 3 insertions(+), 111 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 20c02a5c..c585bf43 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -10,7 +10,7 @@ from traceback import format_exc from urllib.parse import urlencode, urlunparse from ssl import create_default_context, Purpose -from sanic.config import Config, LOGGING +from sanic.config import Config from sanic.constants import HTTP_METHODS from sanic.exceptions import ServerError, URLBuildError, SanicException from sanic.handlers import ErrorHandler @@ -28,18 +28,7 @@ class Sanic: def __init__(self, name=None, router=None, error_handler=None, load_env=True, request_class=None, - log_config=LOGGING, strict_slashes=False): - if log_config: - logging.config.dictConfig(log_config) - # Only set up a default log handler if the - # end-user application didn't set anything up. - if not logging.root.handlers and log.level == logging.NOTSET: - formatter = logging.Formatter( - "%(asctime)s: %(levelname)s: %(message)s") - handler = logging.StreamHandler() - handler.setFormatter(formatter) - log.addHandler(handler) - log.setLevel(logging.INFO) + strict_slashes=False): # Get name from previous stack frame if name is None: @@ -51,7 +40,6 @@ class Sanic: self.request_class = request_class self.error_handler = error_handler or ErrorHandler() self.config = Config(load_env=load_env) - self.log_config = log_config self.request_middleware = deque() self.response_middleware = deque() self.blueprints = {} @@ -642,7 +630,7 @@ class Sanic: async def create_server(self, host=None, port=None, debug=False, ssl=None, sock=None, protocol=None, backlog=100, stop_event=None, - log_config=LOGGING): + log_config=None): """Asynchronous version of `run`. NOTE: This does not support multiprocessing and is not the preferred diff --git a/sanic/config.py b/sanic/config.py index 0c2cc701..716ec1a6 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -15,89 +15,6 @@ _address_dict = { 'FreeBSD': '/var/run/log' } -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'accessFilter': { - '()': DefaultFilter, - 'param': [0, 10, 20] - }, - 'errorFilter': { - '()': DefaultFilter, - 'param': [30, 40, 50] - } - }, - 'formatters': { - 'simple': { - 'format': '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s', - 'datefmt': '%Y-%m-%d %H:%M:%S' - }, - 'access': { - 'format': '%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: ' + - '%(request)s %(message)s %(status)d %(byte)d', - 'datefmt': '%Y-%m-%d %H:%M:%S' - } - }, - 'handlers': { - 'internal': { - 'class': 'logging.StreamHandler', - 'filters': ['accessFilter'], - 'formatter': 'simple', - 'stream': sys.stderr - }, - 'accessStream': { - 'class': 'logging.StreamHandler', - 'filters': ['accessFilter'], - 'formatter': 'access', - 'stream': sys.stderr - }, - 'errorStream': { - 'class': 'logging.StreamHandler', - 'filters': ['errorFilter'], - 'formatter': 'simple', - 'stream': sys.stderr - }, - # before you use accessSysLog, be sure that log levels - # 0, 10, 20 have been enabled in you syslog configuration - # otherwise you won't be able to see the output in syslog - # logging file. - 'accessSysLog': { - 'class': 'logging.handlers.SysLogHandler', - 'address': _address_dict.get(platform.system(), - ('localhost', 514)), - 'facility': syslog.LOG_DAEMON, - 'filters': ['accessFilter'], - 'formatter': 'access' - }, - 'errorSysLog': { - 'class': 'logging.handlers.SysLogHandler', - 'address': _address_dict.get(platform.system(), - ('localhost', 514)), - 'facility': syslog.LOG_DAEMON, - 'filters': ['errorFilter'], - 'formatter': 'simple' - }, - }, - 'loggers': { - 'sanic': { - 'level': 'INFO', - 'handlers': ['internal', 'errorStream'] - }, - 'network': { - 'level': 'INFO', - 'handlers': ['accessStream', 'errorStream'] - } - } -} - -# this happens when using container or systems without syslog -# keep things in config would cause file not exists error -_addr = LOGGING['handlers']['accessSysLog']['address'] -if type(_addr) is str and not os.path.exists(_addr): - LOGGING['handlers'].pop('accessSysLog') - LOGGING['handlers'].pop('errorSysLog') - class Config(dict): def __init__(self, defaults=None, load_env=True, keep_alive=True): diff --git a/sanic/log.py b/sanic/log.py index 760ad1c6..a7933c0d 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,18 +1,5 @@ import logging -class DefaultFilter(logging.Filter): - - def __init__(self, param=None): - self.param = param - - def filter(self, record): - if self.param is None: - return True - if record.levelno in self.param: - return True - return False - - log = logging.getLogger('sanic') netlog = logging.getLogger('network') From c9cbc00e362eda60f3af296d743e1ee6ca69626f Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 10 Sep 2017 18:38:52 -0700 Subject: [PATCH 056/104] use access_log as param --- sanic/app.py | 17 ++++++----------- sanic/server.py | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index c585bf43..595b50c1 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -567,7 +567,7 @@ class Sanic: def run(self, host=None, port=None, debug=False, ssl=None, sock=None, workers=1, protocol=None, backlog=100, stop_event=None, register_sys_signals=True, - log_config=None): + access_log=True): """Run the HTTP Server and listen until keyboard interrupt or term signal. On termination, drain connections before closing. @@ -588,9 +588,6 @@ class Sanic: if sock is None: host, port = host or "127.0.0.1", port or 8000 - if log_config: - self.log_config = log_config - logging.config.dictConfig(log_config) if protocol is None: protocol = (WebSocketProtocol if self.websocket_enabled else HttpProtocol) @@ -603,7 +600,7 @@ class Sanic: host=host, port=port, debug=debug, ssl=ssl, sock=sock, workers=workers, protocol=protocol, backlog=backlog, register_sys_signals=register_sys_signals, - has_log=self.log_config is not None) + access_log=access_log) try: self.is_running = True @@ -630,7 +627,7 @@ class Sanic: async def create_server(self, host=None, port=None, debug=False, ssl=None, sock=None, protocol=None, backlog=100, stop_event=None, - log_config=None): + access_log=True): """Asynchronous version of `run`. NOTE: This does not support multiprocessing and is not the preferred @@ -639,8 +636,6 @@ class Sanic: if sock is None: host, port = host or "127.0.0.1", port or 8000 - if log_config: - logging.config.dictConfig(log_config) if protocol is None: protocol = (WebSocketProtocol if self.websocket_enabled else HttpProtocol) @@ -654,7 +649,7 @@ class Sanic: host=host, port=port, debug=debug, ssl=ssl, sock=sock, loop=get_event_loop(), protocol=protocol, backlog=backlog, run_async=True, - has_log=log_config is not None) + access_log=access_log) # Trigger before_start events await self.trigger_events( @@ -699,7 +694,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, has_log=True): + register_sys_signals=True, run_async=False, access_log=True): """Helper function used by `run` and `create_server`.""" if isinstance(ssl, dict): # try common aliaseses @@ -738,7 +733,7 @@ class Sanic: 'loop': loop, 'register_sys_signals': register_sys_signals, 'backlog': backlog, - 'has_log': has_log, + 'access_log': access_log, 'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE, 'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE, 'graceful_shutdown_timeout': self.config.GRACEFUL_SHUTDOWN_TIMEOUT diff --git a/sanic/server.py b/sanic/server.py index f62ba654..3e52d634 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -65,15 +65,15 @@ class HttpProtocol(asyncio.Protocol): # request config 'request_handler', 'request_timeout', 'request_max_size', 'request_class', 'is_request_stream', 'router', - # enable or disable access log / error log purpose - 'has_log', + # enable or disable access log purpose + 'access_log', # connection management '_total_request_size', '_timeout_handler', '_last_communication_time', '_is_stream_handler') def __init__(self, *, loop, request_handler, error_handler, signal=Signal(), connections=set(), request_timeout=60, - request_max_size=None, request_class=None, has_log=True, + request_max_size=None, request_class=None, access_log=True, keep_alive=True, is_request_stream=False, router=None, state=None, debug=False, **kwargs): self.loop = loop @@ -84,7 +84,7 @@ class HttpProtocol(asyncio.Protocol): self.headers = None self.router = router self.signal = signal - self.has_log = has_log + self.access_log = access_log self.connections = connections self.request_handler = request_handler self.error_handler = error_handler @@ -246,7 +246,7 @@ class HttpProtocol(asyncio.Protocol): response.output( self.request.version, keep_alive, self.request_timeout)) - if self.has_log: + if self.access_log: netlog.info('', extra={ 'status': response.status, 'byte': len(response.body), @@ -288,7 +288,7 @@ class HttpProtocol(asyncio.Protocol): response.transport = self.transport await response.stream( self.request.version, keep_alive, self.request_timeout) - if self.has_log: + if self.access_log: netlog.info('', extra={ 'status': response.status, 'byte': -1, @@ -333,7 +333,7 @@ class HttpProtocol(asyncio.Protocol): "Writing error failed, connection closed {}".format(repr(e)), from_error=True) finally: - if self.has_log: + if self.access_log: extra = dict() if isinstance(response, HTTPResponse): extra['status'] = response.status @@ -424,7 +424,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_timeout=60, ssl=None, sock=None, request_max_size=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, register_sys_signals=True, run_async=False, connections=None, - signal=Signal(), request_class=None, has_log=True, keep_alive=True, + signal=Signal(), request_class=None, access_log=True, keep_alive=True, is_request_stream=False, router=None, websocket_max_size=None, websocket_max_queue=None, state=None, graceful_shutdown_timeout=15.0): @@ -453,7 +453,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, :param loop: asyncio compatible event loop :param protocol: subclass of asyncio protocol class :param request_class: Request class to use - :param has_log: disable/enable access log and error log + :param access_log: disable/enable access log :param is_request_stream: disable/enable Request.stream :param router: Router object :return: Nothing @@ -476,7 +476,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_timeout=request_timeout, request_max_size=request_max_size, request_class=request_class, - has_log=has_log, + access_log=access_log, keep_alive=keep_alive, is_request_stream=is_request_stream, router=router, From 986135ff76f0a4e94d0842e5d1c79c5c43491e2f Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 10 Sep 2017 18:39:42 -0700 Subject: [PATCH 057/104] remove DefaultFilter --- sanic/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index 716ec1a6..1f0bbd3e 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -4,8 +4,6 @@ import syslog import platform import types -from sanic.log import DefaultFilter - SANIC_PREFIX = 'SANIC_' _address_dict = { From 8f6fa5e9ffe0580407c8a999e35171664b4010b8 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 10 Sep 2017 18:44:54 -0700 Subject: [PATCH 058/104] old logging cleanup --- tests/test_logging.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/test_logging.py b/tests/test_logging.py index d6911d86..3d75dbe0 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,7 +1,6 @@ import uuid from importlib import reload -from sanic.config import LOGGING from sanic.response import text from sanic import Sanic from io import StringIO @@ -38,20 +37,3 @@ def test_log(): request, response = app.test_client.get('/') log_text = log_stream.getvalue() assert rand_string in log_text - - -def test_default_log_fmt(): - - reset_logging() - Sanic() - for fmt in [h.formatter for h in logging.getLogger('sanic').handlers]: - assert fmt._fmt == LOGGING['formatters']['simple']['format'] - - reset_logging() - Sanic(log_config=None) - for fmt in [h.formatter for h in logging.getLogger('sanic').handlers]: - assert fmt._fmt == "%(asctime)s: %(levelname)s: %(message)s" - - -if __name__ == "__main__": - test_log() From 4bdb9a2c8e0d323352e136624e9558c4d1633aa9 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 10 Sep 2017 23:19:09 -0700 Subject: [PATCH 059/104] prototype --- sanic/log.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/sanic/log.py b/sanic/log.py index a7933c0d..3d254d6c 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,5 +1,56 @@ import logging +LOGGING_CONFIG_DEFAULTS = dict( + version=1, + disable_existing_loggers=False, + + loggers={ + "root": { + "level": "INFO", + "handlers": ["console"]}, + "sanic.error": { + "level": "INFO", + "handlers": ["error_console"], + "propagate": True, + "qualname": "sanic.error" + }, + + "sanic.access": { + "level": "INFO", + "handlers": ["console"], + "propagate": True, + "qualname": "sanic.access" + } + }, + handlers={ + "console": { + "class": "logging.StreamHandler", + "formatter": "generic", + "stream": "sys.stdout" + }, + "error_console": { + "class": "logging.StreamHandler", + "formatter": "generic", + "stream": "sys.stderr" + }, + }, + formatters={ + "generic": { + "format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s", + "datefmt": "[%Y-%m-%d %H:%M:%S %z]", + "class": "logging.Formatter" + } + } +) + + +class AccessLogger: + + def __init__(self, logger, access_log_format=None): + pass + + log = logging.getLogger('sanic') -netlog = logging.getLogger('network') +error_logger = logging.getLogger('sanic.error') +access_logger = logging.getLogger('sanic.access') From 2979e03148540d4d16e797ebb1999da58843d87d Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Mon, 11 Sep 2017 17:17:33 +1000 Subject: [PATCH 060/104] WIP - Split RequestTimeout, ResponseTimout, and KeepAliveTimeout into different timeouts, with different callbacks. --- sanic/app.py | 2 + sanic/config.py | 8 ++ sanic/exceptions.py | 14 +++ sanic/server.py | 136 +++++++++++++++++++++++------ tests/test_keep_alive_timeout.py | 142 +++++++++++++++++++++++++++++++ tests/test_request_timeout.py | 101 +++++++++++++++++----- tests/test_response_timeout.py | 38 +++++++++ 7 files changed, 395 insertions(+), 46 deletions(-) create mode 100644 tests/test_keep_alive_timeout.py create mode 100644 tests/test_response_timeout.py diff --git a/sanic/app.py b/sanic/app.py index 20c02a5c..4a2ea01c 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -745,6 +745,8 @@ class Sanic: 'request_handler': self.handle_request, 'error_handler': self.error_handler, 'request_timeout': self.config.REQUEST_TIMEOUT, + 'response_timeout': self.config.RESPONSE_TIMEOUT, + 'keep_alive_timeout': self.config.KEEP_ALIVE_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, 'keep_alive': self.config.KEEP_ALIVE, 'loop': loop, diff --git a/sanic/config.py b/sanic/config.py index 0c2cc701..560fa2ec 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -125,7 +125,15 @@ class Config(dict): """ self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes self.REQUEST_TIMEOUT = 60 # 60 seconds + self.RESPONSE_TIMEOUT = 60 # 60 seconds self.KEEP_ALIVE = keep_alive + # Apache httpd server default keepalive timeout = 5 seconds + # Nginx server default keepalive timeout = 75 seconds + # Nginx performance tuning guidelines uses keepalive timeout = 15 seconds + # IE client hard keepalive limit = 60 seconds + # Firefox client hard keepalive limit = 115 seconds + + self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes self.WEBSOCKET_MAX_QUEUE = 32 self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 9663ea7c..e2d808f7 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -155,6 +155,13 @@ class ServerError(SanicException): pass +@add_status_code(503) +class ServiceUnavailable(SanicException): + """The server is currently unavailable (because it is overloaded or + down for maintenance). Generally, this is a temporary state.""" + pass + + class URLBuildError(ServerError): pass @@ -170,6 +177,13 @@ class FileNotFound(NotFound): @add_status_code(408) class RequestTimeout(SanicException): + """The Web server (running the Web site) thinks that there has been too + long an interval of time between 1) the establishment of an IP + connection (socket) between the client and the server and + 2) the receipt of any data on that socket, so the server has dropped + the connection. The socket connection has actually been lost - the Web + server has 'timed out' on that particular socket connection. + """ pass diff --git a/sanic/server.py b/sanic/server.py index f62ba654..bcef8a91 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -28,7 +28,8 @@ from sanic.log import log, netlog from sanic.response import HTTPResponse from sanic.request import Request from sanic.exceptions import ( - RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError) + RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError, + ServiceUnavailable) current_time = None @@ -63,16 +64,19 @@ class HttpProtocol(asyncio.Protocol): # request params 'parser', 'request', 'url', 'headers', # request config - 'request_handler', 'request_timeout', 'request_max_size', - 'request_class', 'is_request_stream', 'router', + 'request_handler', 'request_timeout', 'response_timeout', + 'keep_alive_timeout', 'request_max_size', 'request_class', + 'is_request_stream', 'router', # enable or disable access log / error log purpose 'has_log', # connection management - '_total_request_size', '_timeout_handler', '_last_communication_time', - '_is_stream_handler') + '_total_request_size', '_request_timeout_handler', + '_response_timeout_handler', '_keep_alive_timeout_handler', + '_last_request_time', '_last_response_time', '_is_stream_handler') def __init__(self, *, loop, request_handler, error_handler, signal=Signal(), connections=set(), request_timeout=60, + response_timeout=60, keep_alive_timeout=15, request_max_size=None, request_class=None, has_log=True, keep_alive=True, is_request_stream=False, router=None, state=None, debug=False, **kwargs): @@ -89,13 +93,18 @@ class HttpProtocol(asyncio.Protocol): self.request_handler = request_handler self.error_handler = error_handler self.request_timeout = request_timeout + self.response_timeout = response_timeout + self.keep_alive_timeout = keep_alive_timeout self.request_max_size = request_max_size self.request_class = request_class or Request self.is_request_stream = is_request_stream self._is_stream_handler = False self._total_request_size = 0 - self._timeout_handler = None + self._request_timeout_handler = None + self._response_timeout_handler = None + self._keep_alive_timeout_handler = None self._last_request_time = None + self._last_response_time = None self._request_handler_task = None self._request_stream_task = None self._keep_alive = keep_alive @@ -118,22 +127,32 @@ class HttpProtocol(asyncio.Protocol): def connection_made(self, transport): self.connections.add(self) - self._timeout_handler = self.loop.call_later( - self.request_timeout, self.connection_timeout) + self._request_timeout_handler = self.loop.call_later( + self.request_timeout, self.request_timeout_callback) self.transport = transport self._last_request_time = current_time def connection_lost(self, exc): self.connections.discard(self) - self._timeout_handler.cancel() + if self._request_timeout_handler: + self._request_timeout_handler.cancel() + if self._response_timeout_handler: + self._response_timeout_handler.cancel() + if self._keep_alive_timeout_handler: + self._keep_alive_timeout_handler.cancel() - def connection_timeout(self): - # Check if + def request_timeout_callback(self): + # See the docstring in the RequestTimeout exception, to see + # exactly what this timeout is checking for. + # Check if elapsed time since request initiated exceeds our + # configured maximum request timeout value time_elapsed = current_time - self._last_request_time if time_elapsed < self.request_timeout: time_left = self.request_timeout - time_elapsed - self._timeout_handler = ( - self.loop.call_later(time_left, self.connection_timeout)) + self._request_timeout_handler = ( + self.loop.call_later(time_left, + self.request_timeout_callback) + ) else: if self._request_stream_task: self._request_stream_task.cancel() @@ -144,6 +163,37 @@ class HttpProtocol(asyncio.Protocol): except RequestTimeout as exception: self.write_error(exception) + def response_timeout_callback(self): + # Check if elapsed time since response was initiated exceeds our + # configured maximum request timeout value + time_elapsed = current_time - self._last_request_time + if time_elapsed < self.response_timeout: + time_left = self.response_timeout - time_elapsed + self._response_timeout_handler = ( + self.loop.call_later(time_left, + self.response_timeout_callback) + ) + else: + try: + raise ServiceUnavailable('Response Timeout') + except ServiceUnavailable as exception: + self.write_error(exception) + + def keep_alive_timeout_callback(self): + # Check if elapsed time since last response exceeds our configured + # maximum keep alive timeout value + time_elapsed = current_time - self._last_response_time + if time_elapsed < self.keep_alive_timeout: + time_left = self.keep_alive_timeout - time_elapsed + self._keep_alive_timeout_handler = ( + self.loop.call_later(time_left, + self.keep_alive_timeout_callback) + ) + else: + log.info('KeepAlive Timeout. Closing connection.') + self.transport.close() + + # -------------------------------------------- # # Parsing # -------------------------------------------- # @@ -204,6 +254,11 @@ class HttpProtocol(asyncio.Protocol): method=self.parser.get_method().decode(), transport=self.transport ) + # Remove any existing KeepAlive handler here, + # It will be recreated if required on the new request. + if self._keep_alive_timeout_handler: + self._keep_alive_timeout_handler.cancel() + self._keep_alive_timeout_handler = None if self.is_request_stream: self._is_stream_handler = self.router.is_stream_handler( self.request) @@ -219,6 +274,11 @@ class HttpProtocol(asyncio.Protocol): self.request.body.append(body) def on_message_complete(self): + # Entire request (headers and whole body) is received. + # We can cancel and remove the request timeout handler now. + if self._request_timeout_handler: + self._request_timeout_handler.cancel() + self._request_timeout_handler = None if self.is_request_stream and self._is_stream_handler: self._request_stream_task = self.loop.create_task( self.request.stream.put(None)) @@ -227,6 +287,9 @@ class HttpProtocol(asyncio.Protocol): self.execute_request_handler() def execute_request_handler(self): + self._response_timeout_handler = self.loop.call_later( + self.response_timeout, self.response_timeout_callback) + self._last_request_time = current_time self._request_handler_task = self.loop.create_task( self.request_handler( self.request, @@ -240,12 +303,15 @@ class HttpProtocol(asyncio.Protocol): """ Writes response content synchronously to the transport. """ + if self._response_timeout_handler: + self._response_timeout_handler.cancel() + self._response_timeout_handler = None try: keep_alive = self.keep_alive self.transport.write( response.output( self.request.version, keep_alive, - self.request_timeout)) + self.keep_alive_timeout)) if self.has_log: netlog.info('', extra={ 'status': response.status, @@ -273,7 +339,10 @@ class HttpProtocol(asyncio.Protocol): if not keep_alive: self.transport.close() else: - self._last_request_time = current_time + self._keep_alive_timeout_handler = self.loop.call_later( + self.keep_alive_timeout, + self.keep_alive_timeout_callback) + self._last_response_time = current_time self.cleanup() async def stream_response(self, response): @@ -282,12 +351,14 @@ class HttpProtocol(asyncio.Protocol): the transport to the response so the response consumer can write to the response as needed. """ - + 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 await response.stream( - self.request.version, keep_alive, self.request_timeout) + self.request.version, keep_alive, self.keep_alive_timeout) if self.has_log: netlog.info('', extra={ 'status': response.status, @@ -315,10 +386,18 @@ class HttpProtocol(asyncio.Protocol): if not keep_alive: self.transport.close() else: - self._last_request_time = current_time + self._keep_alive_timeout_handler = self.loop.call_later( + self.keep_alive_timeout, + self.keep_alive_timeout_callback) + self._last_response_time = current_time self.cleanup() def write_error(self, exception): + # An error _is_ a response. + # Don't throw a response timeout, when a response _is_ given. + if self._response_timeout_handler: + self._response_timeout_handler.cancel() + self._response_timeout_handler = None response = None try: response = self.error_handler.response(self.request, exception) @@ -330,8 +409,9 @@ class HttpProtocol(asyncio.Protocol): self.request.ip if self.request else 'Unknown')) except Exception as e: self.bail_out( - "Writing error failed, connection closed {}".format(repr(e)), - from_error=True) + "Writing error failed, connection closed {}".format( + repr(e)), from_error=True + ) finally: if self.has_log: extra = dict() @@ -367,6 +447,9 @@ class HttpProtocol(asyncio.Protocol): log.error(message) def cleanup(self): + """This is called when KeepAlive feature is used, + it resets the connection in order for it to be able + to handle receiving another request on the same connection.""" self.parser = None self.request = None self.url = None @@ -421,12 +504,13 @@ def trigger_events(events, loop): def serve(host, port, request_handler, error_handler, before_start=None, after_start=None, before_stop=None, after_stop=None, debug=False, - request_timeout=60, ssl=None, sock=None, request_max_size=None, - reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, + request_timeout=60, response_timeout=60, keep_alive_timeout=60, + ssl=None, sock=None, request_max_size=None, reuse_port=False, + loop=None, protocol=HttpProtocol, backlog=100, register_sys_signals=True, run_async=False, connections=None, - signal=Signal(), request_class=None, has_log=True, keep_alive=True, - is_request_stream=False, router=None, websocket_max_size=None, - websocket_max_queue=None, state=None, + signal=Signal(), request_class=None, has_log=True, + keep_alive=True, is_request_stream=False, router=None, + websocket_max_size=None, websocket_max_queue=None, state=None, graceful_shutdown_timeout=15.0): """Start asynchronous HTTP Server on an individual process. @@ -474,6 +558,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_handler=request_handler, error_handler=error_handler, request_timeout=request_timeout, + response_timeout=response_timeout, + keep_alive_timeout=keep_alive_timeout, request_max_size=request_max_size, request_class=request_class, has_log=has_log, diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py new file mode 100644 index 00000000..28030144 --- /dev/null +++ b/tests/test_keep_alive_timeout.py @@ -0,0 +1,142 @@ +from json import JSONDecodeError +from sanic import Sanic +from time import sleep as sync_sleep +import asyncio +from sanic.response import text +from sanic.config import Config +import aiohttp +from aiohttp import TCPConnector +from sanic.testing import SanicTestClient, HOST, PORT + + +class ReuseableTCPConnector(TCPConnector): + def __init__(self, *args, **kwargs): + super(ReuseableTCPConnector, self).__init__(*args, **kwargs) + self.conn = None + + @asyncio.coroutine + def connect(self, req): + if self.conn: + return self.conn + conn = yield from super(ReuseableTCPConnector, self).connect(req) + self.conn = conn + return conn + + def close(self): + return super(ReuseableTCPConnector, self).close() + + +class ReuseableSanicTestClient(SanicTestClient): + def __init__(self, app): + super(ReuseableSanicTestClient, self).__init__(app) + self._tcp_connector = None + self._session = None + + def _sanic_endpoint_test( + self, method='get', uri='/', gather_request=True, + debug=False, server_kwargs={}, + *request_args, **request_kwargs): + results = [None, None] + exceptions = [] + + if gather_request: + def _collect_request(request): + if results[0] is None: + results[0] = request + + self.app.request_middleware.appendleft(_collect_request) + + @self.app.listener('after_server_start') + async def _collect_response(sanic, loop): + try: + response = await self._local_request( + method, uri, *request_args, + **request_kwargs) + results[-1] = response + except Exception as e: + log.error( + 'Exception:\n{}'.format(traceback.format_exc())) + exceptions.append(e) + self.app.stop() + + server = self.app.create_server(host=HOST, debug=debug, port=PORT, **server_kwargs) + self.app.listeners['after_server_start'].pop() + + if exceptions: + raise ValueError( + "Exception during request: {}".format(exceptions)) + + if gather_request: + try: + request, response = results + return request, response + except: + raise ValueError( + "Request and response object expected, got ({})".format( + results)) + else: + try: + return results[-1] + except: + raise ValueError( + "Request object expected, got ({})".format(results)) + + async def _local_request(self, method, uri, cookies=None, *args, + **kwargs): + if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')): + url = uri + else: + url = 'http://{host}:{port}{uri}'.format( + host=HOST, port=PORT, uri=uri) + if self._session: + session = self._session + else: + if self._tcp_connector: + conn = self._tcp_connector + else: + conn = ReuseableTCPConnector(verify_ssl=False) + self._tcp_connector = conn + session = aiohttp.ClientSession(cookies=cookies, + connector=conn) + self._session = session + + async with getattr(session, method.lower())( + url, *args, **kwargs) as response: + try: + response.text = await response.text() + except UnicodeDecodeError: + response.text = None + + try: + response.json = await response.json() + except (JSONDecodeError, + UnicodeDecodeError, + aiohttp.ClientResponseError): + response.json = None + + response.body = await response.read() + return response + + +Config.KEEP_ALIVE_TIMEOUT = 30 +Config.KEEP_ALIVE = True +keep_alive_timeout_app = Sanic('test_request_timeout') + + +@keep_alive_timeout_app.route('/1') +async def handler(request): + return text('OK') + + +def test_keep_alive_timeout(): + client = ReuseableSanicTestClient(keep_alive_timeout_app) + headers = { + 'Connection': 'keep-alive' + } + request, response = client.get('/1', headers=headers) + assert response.status == 200 + #sync_sleep(2) + request, response = client.get('/1') + assert response.status == 200 + + diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 404aec12..e6c1f657 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -1,38 +1,97 @@ +from json import JSONDecodeError from sanic import Sanic import asyncio from sanic.response import text from sanic.exceptions import RequestTimeout from sanic.config import Config +import aiohttp +from aiohttp import TCPConnector +from sanic.testing import SanicTestClient, HOST, PORT + + +class DelayableTCPConnector(TCPConnector): + class DelayableHttpRequest(object): + def __new__(cls, req, delay): + cls = super(DelayableTCPConnector.DelayableHttpRequest, cls).\ + __new__(cls) + cls.req = req + cls.delay = delay + return cls + + def __getattr__(self, item): + return getattr(self.req, item) + + def send(self, *args, **kwargs): + if self.delay and self.delay > 0: + _ = yield from asyncio.sleep(self.delay) + self.req.send(*args, **kwargs) + + def __init__(self, *args, **kwargs): + _post_connect_delay = kwargs.pop('post_connect_delay', 0) + _pre_request_delay = kwargs.pop('pre_request_delay', 0) + super(DelayableTCPConnector, self).__init__(*args, **kwargs) + self._post_connect_delay = _post_connect_delay + self._pre_request_delay = _pre_request_delay + + @asyncio.coroutine + def connect(self, req): + req = DelayableTCPConnector.\ + DelayableHttpRequest(req, self._pre_request_delay) + conn = yield from super(DelayableTCPConnector, self).connect(req) + if self._post_connect_delay and self._post_connect_delay > 0: + _ = yield from asyncio.sleep(self._post_connect_delay) + return conn + + +class DelayableSanicTestClient(SanicTestClient): + def __init__(self, app, request_delay=1): + super(DelayableSanicTestClient, self).__init__(app) + self._request_delay = request_delay + + async def _local_request(self, method, uri, cookies=None, *args, + **kwargs): + if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')): + url = uri + else: + url = 'http://{host}:{port}{uri}'.format( + host=HOST, port=PORT, uri=uri) + + conn = DelayableTCPConnector(pre_request_delay=self._request_delay, + verify_ssl=False) + async with aiohttp.ClientSession( + cookies=cookies, connector=conn) as session: + # Insert a delay after creating the connection + # But before sending the request. + + async with getattr(session, method.lower())( + url, *args, **kwargs) as response: + try: + response.text = await response.text() + except UnicodeDecodeError: + response.text = None + + try: + response.json = await response.json() + except (JSONDecodeError, + UnicodeDecodeError, + aiohttp.ClientResponseError): + response.json = None + + response.body = await response.read() + return response + Config.REQUEST_TIMEOUT = 1 -request_timeout_app = Sanic('test_request_timeout') request_timeout_default_app = Sanic('test_request_timeout_default') -@request_timeout_app.route('/1') -async def handler_1(request): - await asyncio.sleep(2) - return text('OK') - - -@request_timeout_app.exception(RequestTimeout) -def handler_exception(request, exception): - return text('Request Timeout from error_handler.', 408) - - -def test_server_error_request_timeout(): - request, response = request_timeout_app.test_client.get('/1') - assert response.status == 408 - assert response.text == 'Request Timeout from error_handler.' - - @request_timeout_default_app.route('/1') -async def handler_2(request): - await asyncio.sleep(2) +async def handler(request): return text('OK') def test_default_server_error_request_timeout(): - request, response = request_timeout_default_app.test_client.get('/1') + client = DelayableSanicTestClient(request_timeout_default_app, 2) + request, response = client.get('/1') assert response.status == 408 assert response.text == 'Error: Request Timeout' diff --git a/tests/test_response_timeout.py b/tests/test_response_timeout.py new file mode 100644 index 00000000..bf55a42e --- /dev/null +++ b/tests/test_response_timeout.py @@ -0,0 +1,38 @@ +from sanic import Sanic +import asyncio +from sanic.response import text +from sanic.exceptions import ServiceUnavailable +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_timeout_app.route('/1') +async def handler_1(request): + await asyncio.sleep(2) + return text('OK') + + +@response_timeout_app.exception(ServiceUnavailable) +def handler_exception(request, exception): + return text('Response Timeout from error_handler.', 503) + + +def test_server_error_response_timeout(): + request, response = response_timeout_app.test_client.get('/1') + assert response.status == 503 + assert response.text == 'Response Timeout from error_handler.' + + +@response_timeout_default_app.route('/1') +async def handler_2(request): + await asyncio.sleep(2) + return text('OK') + + +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' From 1a74accd65cfe6a1c52d3697ab4552285fcd95c9 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Tue, 12 Sep 2017 13:09:42 +1000 Subject: [PATCH 061/104] finished the keepalive_timeout tests --- tests/test_keep_alive_timeout.py | 164 ++++++++++++++++++++++++++----- 1 file changed, 138 insertions(+), 26 deletions(-) diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 28030144..12e1629d 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -4,6 +4,7 @@ from time import sleep as sync_sleep import asyncio from sanic.response import text from sanic.config import Config +from sanic import server import aiohttp from aiohttp import TCPConnector from sanic.testing import SanicTestClient, HOST, PORT @@ -12,33 +13,40 @@ from sanic.testing import SanicTestClient, HOST, PORT class ReuseableTCPConnector(TCPConnector): def __init__(self, *args, **kwargs): super(ReuseableTCPConnector, self).__init__(*args, **kwargs) - self.conn = None + self.old_proto = None @asyncio.coroutine def connect(self, req): - if self.conn: - return self.conn - conn = yield from super(ReuseableTCPConnector, self).connect(req) - self.conn = conn - return conn - - def close(self): - return super(ReuseableTCPConnector, self).close() + new_conn = yield from super(ReuseableTCPConnector, self)\ + .connect(req) + 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!") + self.old_proto = new_conn.protocol + return new_conn class ReuseableSanicTestClient(SanicTestClient): - def __init__(self, app): + def __init__(self, app, loop=None): super(ReuseableSanicTestClient, self).__init__(app) + if loop is None: + loop = asyncio.get_event_loop() + self._loop = loop + self._server = None self._tcp_connector = None self._session = None + # Copied from SanicTestClient, but with some changes to reuse the + # same loop for the same app. def _sanic_endpoint_test( self, method='get', uri='/', gather_request=True, debug=False, server_kwargs={}, *request_args, **request_kwargs): + loop = self._loop results = [None, None] exceptions = [] - + do_kill_server = request_kwargs.pop('end_server', False) if gather_request: def _collect_request(request): if results[0] is None: @@ -47,26 +55,53 @@ class ReuseableSanicTestClient(SanicTestClient): self.app.request_middleware.appendleft(_collect_request) @self.app.listener('after_server_start') - async def _collect_response(sanic, loop): + async def _collect_response(loop): try: + if do_kill_server: + request_kwargs['end_session'] = True response = await self._local_request( method, uri, *request_args, **request_kwargs) results[-1] = response except Exception as e: - log.error( - 'Exception:\n{}'.format(traceback.format_exc())) + import traceback + traceback.print_tb(e.__traceback__) exceptions.append(e) - self.app.stop() + #Don't stop here! self.app.stop() - server = self.app.create_server(host=HOST, debug=debug, port=PORT, **server_kwargs) + if self._server is not None: + _server = self._server + else: + _server_co = self.app.create_server(host=HOST, debug=debug, + port=PORT, **server_kwargs) + + server.trigger_events( + self.app.listeners['before_server_start'], loop) + + try: + loop._stopping = False + http_server = loop.run_until_complete(_server_co) + except Exception as e: + raise e + self._server = _server = http_server + server.trigger_events( + self.app.listeners['after_server_start'], loop) self.app.listeners['after_server_start'].pop() + if do_kill_server: + try: + _server.close() + self._server = None + loop.run_until_complete(_server.wait_closed()) + self.app.stop() + except Exception as e: + exceptions.append(e) if exceptions: raise ValueError( "Exception during request: {}".format(exceptions)) if gather_request: + self.app.request_middleware.pop() try: request, response = results return request, response @@ -81,20 +116,29 @@ class ReuseableSanicTestClient(SanicTestClient): raise ValueError( "Request object expected, got ({})".format(results)) + # Copied from SanicTestClient, but with some changes to reuse the + # same TCPConnection and the sane ClientSession more than once. + # Note, you cannot use the same session if you are in a _different_ + # loop, so the changes above are required too. async def _local_request(self, method, uri, cookies=None, *args, **kwargs): + request_keepalive = kwargs.pop('request_keepalive', + Config.KEEP_ALIVE_TIMEOUT) if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')): url = uri else: url = 'http://{host}:{port}{uri}'.format( host=HOST, port=PORT, uri=uri) + do_kill_session = kwargs.pop('end_session', False) if self._session: session = self._session else: if self._tcp_connector: conn = self._tcp_connector else: - conn = ReuseableTCPConnector(verify_ssl=False) + conn = ReuseableTCPConnector(verify_ssl=False, + keepalive_timeout= + request_keepalive) self._tcp_connector = conn session = aiohttp.ClientSession(cookies=cookies, connector=conn) @@ -115,28 +159,96 @@ class ReuseableSanicTestClient(SanicTestClient): response.json = None response.body = await response.read() - return response + if do_kill_session: + session.close() + self._session = None + return response -Config.KEEP_ALIVE_TIMEOUT = 30 +Config.KEEP_ALIVE_TIMEOUT = 2 Config.KEEP_ALIVE = True -keep_alive_timeout_app = Sanic('test_request_timeout') +keep_alive_timeout_app_reuse = Sanic('test_ka_timeout_reuse') +keep_alive_app_client_timeout = Sanic('test_ka_client_timeout') +keep_alive_app_server_timeout = Sanic('test_ka_server_timeout') -@keep_alive_timeout_app.route('/1') -async def handler(request): +@keep_alive_timeout_app_reuse.route('/1') +async def handler1(request): return text('OK') -def test_keep_alive_timeout(): - client = ReuseableSanicTestClient(keep_alive_timeout_app) +@keep_alive_app_client_timeout.route('/1') +async def handler2(request): + return text('OK') + + +@keep_alive_app_server_timeout.route('/1') +async def handler3(request): + return text('OK') + + +def test_keep_alive_timeout_reuse(): + """If the server keep-alive timeout and client keep-alive timeout are + both longer than the delay, the client _and_ server will successfully + reuse the existing connection.""" + loop = asyncio.get_event_loop() + client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop) headers = { 'Connection': 'keep-alive' } request, response = client.get('/1', headers=headers) assert response.status == 200 - #sync_sleep(2) - request, response = client.get('/1') + assert response.text == 'OK' + sync_sleep(1) + request, response = client.get('/1', end_server=True) assert response.status == 200 + assert response.text == 'OK' +def test_keep_alive_client_timeout(): + """If the server keep-alive timeout is longer than the client + keep-alive timeout, client will try to create a new connection here.""" + loop = asyncio.get_event_loop() + client = ReuseableSanicTestClient(keep_alive_app_client_timeout, + loop) + headers = { + 'Connection': 'keep-alive' + } + request, response = client.get('/1', headers=headers, + request_keepalive=1) + assert response.status == 200 + assert response.text == 'OK' + sync_sleep(3) + exception = None + try: + request, response = client.get('/1', end_server=True) + except ValueError as e: + exception = e + assert exception is not None + assert isinstance(exception, ValueError) + assert "got a new connection" in exception.args[0] + + +def test_keep_alive_server_timeout(): + """If the client keep-alive timeout is longer than the server + keep-alive timeout, the client will get a 'Connection reset' error.""" + loop = asyncio.get_event_loop() + client = ReuseableSanicTestClient(keep_alive_app_server_timeout, + loop) + headers = { + 'Connection': 'keep-alive' + } + request, response = client.get('/1', headers=headers, + request_keepalive=5) + assert response.status == 200 + assert response.text == 'OK' + sync_sleep(3) + exception = None + try: + request, response = client.get('/1', end_server=True) + except ValueError as e: + exception = e + assert exception is not None + assert isinstance(exception, ValueError) + assert "Connection reset" in exception.args[0] + From 173f94216a797041b54a8c4a84ca8e530dbdda4b Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Tue, 12 Sep 2017 13:40:43 +1000 Subject: [PATCH 062/104] Fixed the delays, and expected responses, in the keepalive_timeout tests --- tests/test_keep_alive_timeout.py | 50 ++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 12e1629d..09c51d00 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -1,7 +1,7 @@ from json import JSONDecodeError from sanic import Sanic -from time import sleep as sync_sleep import asyncio +from asyncio import sleep as aio_sleep from sanic.response import text from sanic.config import Config from sanic import server @@ -63,10 +63,8 @@ class ReuseableSanicTestClient(SanicTestClient): method, uri, *request_args, **request_kwargs) results[-1] = response - except Exception as e: - import traceback - traceback.print_tb(e.__traceback__) - exceptions.append(e) + except Exception as e2: + exceptions.append(e2) #Don't stop here! self.app.stop() if self._server is not None: @@ -81,8 +79,8 @@ class ReuseableSanicTestClient(SanicTestClient): try: loop._stopping = False http_server = loop.run_until_complete(_server_co) - except Exception as e: - raise e + except Exception as e1: + raise e1 self._server = _server = http_server server.trigger_events( self.app.listeners['after_server_start'], loop) @@ -94,8 +92,8 @@ class ReuseableSanicTestClient(SanicTestClient): self._server = None loop.run_until_complete(_server.wait_closed()) self.app.stop() - except Exception as e: - exceptions.append(e) + except Exception as e3: + exceptions.append(e3) if exceptions: raise ValueError( "Exception during request: {}".format(exceptions)) @@ -137,11 +135,13 @@ class ReuseableSanicTestClient(SanicTestClient): conn = self._tcp_connector else: conn = ReuseableTCPConnector(verify_ssl=False, + loop=self._loop, keepalive_timeout= request_keepalive) self._tcp_connector = conn session = aiohttp.ClientSession(cookies=cookies, - connector=conn) + connector=conn, + loop=self._loop) self._session = session async with getattr(session, method.lower())( @@ -191,7 +191,8 @@ def test_keep_alive_timeout_reuse(): """If the server keep-alive timeout and client keep-alive timeout are both longer than the delay, the client _and_ server will successfully reuse the existing connection.""" - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop) headers = { 'Connection': 'keep-alive' @@ -199,7 +200,7 @@ def test_keep_alive_timeout_reuse(): request, response = client.get('/1', headers=headers) assert response.status == 200 assert response.text == 'OK' - sync_sleep(1) + loop.run_until_complete(aio_sleep(1)) request, response = client.get('/1', end_server=True) assert response.status == 200 assert response.text == 'OK' @@ -208,7 +209,8 @@ def test_keep_alive_timeout_reuse(): def test_keep_alive_client_timeout(): """If the server keep-alive timeout is longer than the client keep-alive timeout, client will try to create a new connection here.""" - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop) headers = { @@ -218,10 +220,11 @@ def test_keep_alive_client_timeout(): request_keepalive=1) assert response.status == 200 assert response.text == 'OK' - sync_sleep(3) + loop.run_until_complete(aio_sleep(2)) exception = None try: - request, response = client.get('/1', end_server=True) + request, response = client.get('/1', end_server=True, + request_keepalive=1) except ValueError as e: exception = e assert exception is not None @@ -231,24 +234,29 @@ def test_keep_alive_client_timeout(): def test_keep_alive_server_timeout(): """If the client keep-alive timeout is longer than the server - keep-alive timeout, the client will get a 'Connection reset' error.""" - loop = asyncio.get_event_loop() + keep-alive timeout, the client will either a 'Connection reset' error + _or_ a new connection. Depending on how the event-loop handles the + broken server connection.""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop) headers = { 'Connection': 'keep-alive' } request, response = client.get('/1', headers=headers, - request_keepalive=5) + request_keepalive=60) assert response.status == 200 assert response.text == 'OK' - sync_sleep(3) + loop.run_until_complete(aio_sleep(3)) exception = None try: - request, response = client.get('/1', end_server=True) + request, response = client.get('/1', request_keepalive=60, + end_server=True) except ValueError as e: exception = e assert exception is not None assert isinstance(exception, ValueError) - assert "Connection reset" in exception.args[0] + assert "Connection reset" in exception.args[0] or \ + "got a new connection" in exception.args[0] From a46e004f07600f9041da1c05d640bd870a643a4c Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Mon, 11 Sep 2017 22:12:49 -0700 Subject: [PATCH 063/104] apply new loggers --- sanic/__main__.py | 6 +++--- sanic/app.py | 14 +++++++------- sanic/handlers.py | 8 ++++---- sanic/log.py | 11 +++-------- sanic/request.py | 4 ++-- sanic/server.py | 34 +++++++++++++++++----------------- sanic/testing.py | 6 +++--- 7 files changed, 39 insertions(+), 44 deletions(-) diff --git a/sanic/__main__.py b/sanic/__main__.py index cc580566..1473aa5f 100644 --- a/sanic/__main__.py +++ b/sanic/__main__.py @@ -1,7 +1,7 @@ from argparse import ArgumentParser from importlib import import_module -from sanic.log import log +from sanic.log import logger from sanic.app import Sanic if __name__ == "__main__": @@ -36,9 +36,9 @@ if __name__ == "__main__": app.run(host=args.host, port=args.port, workers=args.workers, debug=args.debug, ssl=ssl) except ImportError as e: - log.error("No module named {} found.\n" + logger.error("No module named {} found.\n" " Example File: project/sanic_server.py -> app\n" " Example Module: project.sanic_server.app" .format(e.name)) except ValueError as e: - log.error("{}".format(e)) + logger.error("{}".format(e)) diff --git a/sanic/app.py b/sanic/app.py index 595b50c1..4c0fa15e 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -14,7 +14,7 @@ from sanic.config import Config from sanic.constants import HTTP_METHODS from sanic.exceptions import ServerError, URLBuildError, SanicException from sanic.handlers import ErrorHandler -from sanic.log import log +from sanic.log import logger, error_logger from sanic.response import HTTPResponse, StreamingHTTPResponse from sanic.router import Router from sanic.server import serve, serve_multiple, HttpProtocol, Signal @@ -542,7 +542,7 @@ class Sanic: response = await self._run_response_middleware(request, response) except: - log.exception( + error_logger.exception( 'Exception occured in one of response middleware handlers' ) @@ -609,12 +609,12 @@ class Sanic: else: serve_multiple(server_settings, workers) except: - log.exception( + error_logger.exception( 'Experienced exception while trying to serve') raise finally: self.is_running = False - log.info("Server Stopped") + logger.info("Server Stopped") def stop(self): """This kills the Sanic""" @@ -757,9 +757,9 @@ class Sanic: server_settings[settings_name] = listeners if debug: - log.setLevel(logging.DEBUG) + logger.setLevel(logging.DEBUG) if self.config.LOGO is not None: - log.debug(self.config.LOGO) + logger.debug(self.config.LOGO) if run_async: server_settings['run_async'] = True @@ -769,6 +769,6 @@ class Sanic: proto = "http" if ssl is not None: proto = "https" - log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) + logger.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) return server_settings diff --git a/sanic/handlers.py b/sanic/handlers.py index 6a87fd5d..2e335b09 100644 --- a/sanic/handlers.py +++ b/sanic/handlers.py @@ -12,7 +12,7 @@ from sanic.exceptions import ( TRACEBACK_WRAPPER_HTML, TRACEBACK_WRAPPER_INNER_HTML, TRACEBACK_BORDER) -from sanic.log import log +from sanic.log import logger from sanic.response import text, html @@ -90,7 +90,7 @@ class ErrorHandler: 'Exception raised in exception handler "{}" ' 'for uri: "{}"\n{}').format( handler.__name__, url, format_exc()) - log.error(response_message) + logger.error(response_message) return text(response_message, 500) else: return text('An error occurred while handling an error', 500) @@ -101,7 +101,7 @@ class ErrorHandler: Override this method in an ErrorHandler subclass to prevent logging exceptions. """ - getattr(log, level)(message) + getattr(logger, level)(message) def default(self, request, exception): self.log(format_exc()) @@ -117,7 +117,7 @@ class ErrorHandler: response_message = ( 'Exception occurred while handling uri: "{}"\n{}'.format( request.url, format_exc())) - log.error(response_message) + logger.error(response_message) return html(html_output, status=500) else: return html(INTERNAL_SERVER_ERROR_HTML, status=500) diff --git a/sanic/log.py b/sanic/log.py index 3d254d6c..5abce0e2 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -8,7 +8,8 @@ LOGGING_CONFIG_DEFAULTS = dict( loggers={ "root": { "level": "INFO", - "handlers": ["console"]}, + "handlers": ["console"] + }, "sanic.error": { "level": "INFO", "handlers": ["error_console"], @@ -45,12 +46,6 @@ LOGGING_CONFIG_DEFAULTS = dict( ) -class AccessLogger: - - def __init__(self, logger, access_log_format=None): - pass - - -log = logging.getLogger('sanic') +logger = logging.getLogger('root') error_logger = logging.getLogger('sanic.error') access_logger = logging.getLogger('sanic.access') diff --git a/sanic/request.py b/sanic/request.py index 27ff011e..b54d463f 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -17,7 +17,7 @@ except ImportError: json_loads = json.loads from sanic.exceptions import InvalidUsage -from sanic.log import log +from sanic.log import logger DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" @@ -114,7 +114,7 @@ class Request(dict): self.parsed_form, self.parsed_files = ( parse_multipart_form(self.body, boundary)) except Exception: - log.exception("Failed when parsing form") + logger.exception("Failed when parsing form") return self.parsed_form diff --git a/sanic/server.py b/sanic/server.py index 3e52d634..7764b120 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -24,7 +24,7 @@ try: except ImportError: async_loop = asyncio -from sanic.log import log, netlog +from sanic.log import logger, access_logger, error_logger from sanic.response import HTTPResponse from sanic.request import Request from sanic.exceptions import ( @@ -247,7 +247,7 @@ class HttpProtocol(asyncio.Protocol): self.request.version, keep_alive, self.request_timeout)) if self.access_log: - netlog.info('', extra={ + access_logger.info('', extra={ 'status': response.status, 'byte': len(response.body), 'host': '{0}:{1}'.format(self.request.ip[0], @@ -256,13 +256,13 @@ class HttpProtocol(asyncio.Protocol): self.request.url) }) except AttributeError: - log.error( + logger.error( ('Invalid response object for url {}, ' 'Expected Type: HTTPResponse, Actual Type: {}').format( self.url, type(response))) self.write_error(ServerError('Invalid response type')) except RuntimeError: - log.error( + logger.error( 'Connection lost before response written @ {}'.format( self.request.ip)) except Exception as e: @@ -289,7 +289,7 @@ class HttpProtocol(asyncio.Protocol): await response.stream( self.request.version, keep_alive, self.request_timeout) if self.access_log: - netlog.info('', extra={ + access_logger.info('', extra={ 'status': response.status, 'byte': -1, 'host': '{0}:{1}'.format(self.request.ip[0], @@ -298,13 +298,13 @@ class HttpProtocol(asyncio.Protocol): self.request.url) }) except AttributeError: - log.error( + logger.error( ('Invalid response object for url {}, ' 'Expected Type: HTTPResponse, Actual Type: {}').format( self.url, type(response))) self.write_error(ServerError('Invalid response type')) except RuntimeError: - log.error( + logger.error( 'Connection lost before response written @ {}'.format( self.request.ip)) except Exception as e: @@ -325,7 +325,7 @@ class HttpProtocol(asyncio.Protocol): version = self.request.version if self.request else '1.1' self.transport.write(response.output(version)) except RuntimeError: - log.error( + logger.error( 'Connection lost before error written @ {}'.format( self.request.ip if self.request else 'Unknown')) except Exception as e: @@ -350,21 +350,21 @@ class HttpProtocol(asyncio.Protocol): extra['request'] = 'nil' if self.parser and not (self.keep_alive and extra['status'] == 408): - netlog.info('', extra=extra) + access_logger.info('', extra=extra) self.transport.close() def bail_out(self, message, from_error=False): if from_error or self.transport.is_closing(): - log.error( + logger.error( ("Transport closed @ {} and exception " "experienced during error handling").format( self.transport.get_extra_info('peername'))) - log.debug( + logger.debug( 'Exception:\n{}'.format(traceback.format_exc())) else: exception = ServerError(message) self.write_error(exception) - log.error(message) + logger.error(message) def cleanup(self): self.parser = None @@ -508,7 +508,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, try: http_server = loop.run_until_complete(server_coroutine) except: - log.exception("Unable to start server") + logger.exception("Unable to start server") return trigger_events(after_start, loop) @@ -519,14 +519,14 @@ def serve(host, port, request_handler, error_handler, before_start=None, try: loop.add_signal_handler(_signal, loop.stop) except NotImplementedError: - log.warn('Sanic tried to use loop.add_signal_handler but it is' + logger.warn('Sanic tried to use loop.add_signal_handler but it is' ' not implemented on this platform.') pid = os.getpid() try: - log.info('Starting worker [{}]'.format(pid)) + logger.info('Starting worker [{}]'.format(pid)) loop.run_forever() finally: - log.info("Stopping worker [{}]".format(pid)) + logger.info("Stopping worker [{}]".format(pid)) # Run the on_stop function if provided trigger_events(before_stop, loop) @@ -588,7 +588,7 @@ def serve_multiple(server_settings, workers): server_settings['port'] = None def sig_handler(signal, frame): - log.info("Received signal {}. Shutting down.".format( + logger.info("Received signal {}. Shutting down.".format( Signals(signal).name)) for process in processes: os.kill(process.pid, SIGINT) diff --git a/sanic/testing.py b/sanic/testing.py index de26d025..5d233d7b 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -1,7 +1,7 @@ import traceback from json import JSONDecodeError -from sanic.log import log +from sanic.log import logger HOST = '127.0.0.1' PORT = 42101 @@ -19,7 +19,7 @@ class SanicTestClient: url = 'http://{host}:{port}{uri}'.format( host=HOST, port=PORT, uri=uri) - log.info(url) + logger.info(url) conn = aiohttp.TCPConnector(verify_ssl=False) async with aiohttp.ClientSession( cookies=cookies, connector=conn) as session: @@ -61,7 +61,7 @@ class SanicTestClient: **request_kwargs) results[-1] = response except Exception as e: - log.error( + logger.error( 'Exception:\n{}'.format(traceback.format_exc())) exceptions.append(e) self.app.stop() From 8eb59ad4dc27261351ea9d1eb7f645ee4f0a9a40 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Wed, 13 Sep 2017 10:18:36 +1000 Subject: [PATCH 064/104] Fixed error where the RequestTimeout test wasn't actually testing the correct behaviour Fixed error where KeepAliveTimeout wasn't being triggered in the test suite, when using uvloop Fixed test cases when using other asyncio loops such as uvloop Fixed Flake8 linting errors --- sanic/config.py | 2 +- sanic/server.py | 1 - tests/test_keep_alive_timeout.py | 13 +++- tests/test_request_timeout.py | 102 +++++++++++++++++++++++++------ 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index 560fa2ec..de91280f 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -129,7 +129,7 @@ class Config(dict): self.KEEP_ALIVE = keep_alive # Apache httpd server default keepalive timeout = 5 seconds # Nginx server default keepalive timeout = 75 seconds - # Nginx performance tuning guidelines uses keepalive timeout = 15 seconds + # Nginx performance tuning guidelines uses keepalive = 15 seconds # IE client hard keepalive limit = 60 seconds # Firefox client hard keepalive limit = 115 seconds diff --git a/sanic/server.py b/sanic/server.py index bcef8a91..eb9864cd 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -193,7 +193,6 @@ class HttpProtocol(asyncio.Protocol): log.info('KeepAlive Timeout. Closing connection.') self.transport.close() - # -------------------------------------------- # # Parsing # -------------------------------------------- # diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 09c51d00..15f6d705 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -20,10 +20,11 @@ class ReuseableTCPConnector(TCPConnector): new_conn = yield from super(ReuseableTCPConnector, self)\ .connect(req) if self.old_proto is not None: - if self.old_proto != new_conn.protocol: + if self.old_proto != new_conn._protocol: raise RuntimeError( "We got a new connection, wanted the same one!") - self.old_proto = new_conn.protocol + print(new_conn.__dict__) + self.old_proto = new_conn._protocol return new_conn @@ -64,6 +65,8 @@ class ReuseableSanicTestClient(SanicTestClient): **request_kwargs) results[-1] = response except Exception as e2: + import traceback + traceback.print_tb(e2.__traceback__) exceptions.append(e2) #Don't stop here! self.app.stop() @@ -80,6 +83,8 @@ class ReuseableSanicTestClient(SanicTestClient): loop._stopping = False http_server = loop.run_until_complete(_server_co) except Exception as e1: + import traceback + traceback.print_tb(e1.__traceback__) raise e1 self._server = _server = http_server server.trigger_events( @@ -93,7 +98,9 @@ class ReuseableSanicTestClient(SanicTestClient): loop.run_until_complete(_server.wait_closed()) self.app.stop() except Exception as e3: - exceptions.append(e3) + import traceback + traceback.print_tb(e3.__traceback__) + exceptions.append(e3) if exceptions: raise ValueError( "Exception during request: {}".format(exceptions)) diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index e6c1f657..a1d8a885 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -1,8 +1,8 @@ from json import JSONDecodeError + from sanic import Sanic import asyncio from sanic.response import text -from sanic.exceptions import RequestTimeout from sanic.config import Config import aiohttp from aiohttp import TCPConnector @@ -10,21 +10,68 @@ from sanic.testing import SanicTestClient, HOST, PORT class DelayableTCPConnector(TCPConnector): - class DelayableHttpRequest(object): + + class RequestContextManager(object): def __new__(cls, req, delay): - cls = super(DelayableTCPConnector.DelayableHttpRequest, cls).\ + cls = super(DelayableTCPConnector.RequestContextManager, cls).\ __new__(cls) cls.req = req + cls.send_task = None + cls.resp = None + cls.orig_send = getattr(req, 'send') + cls.orig_start = None cls.delay = delay + cls._acting_as = req return cls def __getattr__(self, item): - return getattr(self.req, item) + acting_as = self._acting_as + return getattr(acting_as, item) + + @asyncio.coroutine + def start(self, connection, read_until_eof=False): + if self.send_task is None: + raise RuntimeError("do a send() before you do a start()") + resp = yield from self.send_task + self.send_task = None + self.resp = resp + self._acting_as = self.resp + self.orig_start = getattr(resp, 'start') + + try: + ret = yield from self.orig_start(connection, + read_until_eof) + except Exception as e: + raise e + return ret + + def close(self): + if self.resp is not None: + self.resp.close() + if self.send_task is not None: + self.send_task.cancel() + + @asyncio.coroutine + def delayed_send(self, *args, **kwargs): + req = self.req + if self.delay and self.delay > 0: + #sync_sleep(self.delay) + _ = yield from asyncio.sleep(self.delay) + t = req.loop.time() + print("sending at {}".format(t), flush=True) + conn = next(iter(args)) # first arg is connection + try: + delayed_resp = self.orig_send(*args, **kwargs) + except Exception as e: + return aiohttp.ClientResponse(req.method, req.url) + return delayed_resp def send(self, *args, **kwargs): - if self.delay and self.delay > 0: - _ = yield from asyncio.sleep(self.delay) - self.req.send(*args, **kwargs) + gen = self.delayed_send(*args, **kwargs) + task = self.req.loop.create_task(gen) + self.send_task = task + self._acting_as = task + return self def __init__(self, *args, **kwargs): _post_connect_delay = kwargs.pop('post_connect_delay', 0) @@ -35,31 +82,37 @@ class DelayableTCPConnector(TCPConnector): @asyncio.coroutine def connect(self, req): - req = DelayableTCPConnector.\ - DelayableHttpRequest(req, self._pre_request_delay) + d_req = DelayableTCPConnector.\ + RequestContextManager(req, self._pre_request_delay) conn = yield from super(DelayableTCPConnector, self).connect(req) if self._post_connect_delay and self._post_connect_delay > 0: - _ = yield from asyncio.sleep(self._post_connect_delay) + _ = yield from 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 class DelayableSanicTestClient(SanicTestClient): - def __init__(self, app, request_delay=1): + def __init__(self, app, loop, request_delay=1): super(DelayableSanicTestClient, self).__init__(app) self._request_delay = request_delay + self._loop = None async def _local_request(self, method, uri, cookies=None, *args, **kwargs): + if self._loop is None: + self._loop = asyncio.get_event_loop() if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')): url = uri else: url = 'http://{host}:{port}{uri}'.format( host=HOST, port=PORT, uri=uri) - conn = DelayableTCPConnector(pre_request_delay=self._request_delay, - verify_ssl=False) - async with aiohttp.ClientSession( - cookies=cookies, connector=conn) as session: + verify_ssl=False, loop=self._loop) + async with aiohttp.ClientSession(cookies=cookies, connector=conn, + loop=self._loop) as session: # Insert a delay after creating the connection # But before sending the request. @@ -81,17 +134,30 @@ class DelayableSanicTestClient(SanicTestClient): return response -Config.REQUEST_TIMEOUT = 1 +Config.REQUEST_TIMEOUT = 2 request_timeout_default_app = Sanic('test_request_timeout_default') +request_no_timeout_app = Sanic('test_request_no_timeout') @request_timeout_default_app.route('/1') -async def handler(request): +async def handler1(request): + return text('OK') + + +@request_no_timeout_app.route('/1') +async def handler2(request): return text('OK') def test_default_server_error_request_timeout(): - client = DelayableSanicTestClient(request_timeout_default_app, 2) + client = DelayableSanicTestClient(request_timeout_default_app, None, 3) request, response = client.get('/1') assert response.status == 408 assert response.text == 'Error: Request Timeout' + + +def test_default_server_error_request_dont_timeout(): + client = DelayableSanicTestClient(request_no_timeout_app, None, 1) + request, response = client.get('/1') + assert response.status == 200 + assert response.text == 'OK' From 24bdb1ce98d8c918cc1503892cd148a4e5b7c3d7 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Tue, 12 Sep 2017 23:42:42 -0700 Subject: [PATCH 065/104] add unit tests/refactoring --- sanic/app.py | 15 +++++++++++---- sanic/config.py | 8 +------- sanic/log.py | 15 +++++++++++++-- sanic/request.py | 4 ++-- tests/test_logging.py | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 4c0fa15e..1d4497d0 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -14,7 +14,7 @@ from sanic.config import Config from sanic.constants import HTTP_METHODS from sanic.exceptions import ServerError, URLBuildError, SanicException from sanic.handlers import ErrorHandler -from sanic.log import logger, error_logger +from sanic.log import logger, error_logger, LOGGING_CONFIG_DEFAULTS from sanic.response import HTTPResponse, StreamingHTTPResponse from sanic.router import Router from sanic.server import serve, serve_multiple, HttpProtocol, Signal @@ -28,13 +28,16 @@ class Sanic: def __init__(self, name=None, router=None, error_handler=None, load_env=True, request_class=None, - strict_slashes=False): + strict_slashes=False, log_config=None): # Get name from previous stack frame if name is None: frame_records = stack()[1] name = getmodulename(frame_records[1]) + # logging + logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) + self.name = name self.router = router or Router() self.request_class = request_class @@ -567,7 +570,7 @@ class Sanic: def run(self, host=None, port=None, debug=False, ssl=None, sock=None, workers=1, protocol=None, backlog=100, stop_event=None, register_sys_signals=True, - access_log=True): + access_log=True, log_config=None): """Run the HTTP Server and listen until keyboard interrupt or term signal. On termination, drain connections before closing. @@ -585,6 +588,8 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ + logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) + if sock is None: host, port = host or "127.0.0.1", port or 8000 @@ -627,12 +632,14 @@ class Sanic: async def create_server(self, host=None, port=None, debug=False, ssl=None, sock=None, protocol=None, backlog=100, stop_event=None, - access_log=True): + access_log=True, log_config=None): """Asynchronous version of `run`. NOTE: This does not support multiprocessing and is not the preferred way to run a Sanic application. """ + logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) + if sock is None: host, port = host or "127.0.0.1", port or 8000 diff --git a/sanic/config.py b/sanic/config.py index 1f0bbd3e..b5430445 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -4,14 +4,8 @@ import syslog import platform import types -SANIC_PREFIX = 'SANIC_' -_address_dict = { - 'Windows': ('localhost', 514), - 'Darwin': '/var/run/syslog', - 'Linux': '/dev/log', - 'FreeBSD': '/var/run/log' -} +SANIC_PREFIX = 'SANIC_' class Config(dict): diff --git a/sanic/log.py b/sanic/log.py index 5abce0e2..72636cd0 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -19,7 +19,7 @@ LOGGING_CONFIG_DEFAULTS = dict( "sanic.access": { "level": "INFO", - "handlers": ["console"], + "handlers": ["access_console"], "propagate": True, "qualname": "sanic.access" } @@ -35,13 +35,24 @@ LOGGING_CONFIG_DEFAULTS = dict( "formatter": "generic", "stream": "sys.stderr" }, + "access_console": { + "class": "logging.StreamHandler", + "formatter": "access", + "stream": "sys.stdout" + }, }, formatters={ "generic": { "format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s", "datefmt": "[%Y-%m-%d %H:%M:%S %z]", "class": "logging.Formatter" - } + }, + "access": { + "format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " + + "%(request)s %(message)s %(status)d %(byte)d", + "datefmt": "[%Y-%m-%d %H:%M:%S %z]", + "class": "logging.Formatter" + }, } ) diff --git a/sanic/request.py b/sanic/request.py index b54d463f..c842cb4b 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -17,7 +17,7 @@ except ImportError: json_loads = json.loads from sanic.exceptions import InvalidUsage -from sanic.log import logger +from sanic.log import error_logger DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" @@ -114,7 +114,7 @@ class Request(dict): self.parsed_form, self.parsed_files = ( parse_multipart_form(self.body, boundary)) except Exception: - logger.exception("Failed when parsing form") + error_logger.exception("Failed when parsing form") return self.parsed_form diff --git a/tests/test_logging.py b/tests/test_logging.py index 3d75dbe0..112c94a0 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -2,6 +2,7 @@ import uuid from importlib import reload from sanic.response import text +from sanic.log import LOGGING_CONFIG_DEFAULTS from sanic import Sanic from io import StringIO import logging @@ -37,3 +38,36 @@ def test_log(): request, response = app.test_client.get('/') log_text = log_stream.getvalue() assert rand_string in log_text + + +def test_logging_defaults(): + reset_logging() + app = Sanic("test_logging") + + for fmt in [h.formatter for h in logging.getLogger('root').handlers]: + assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format'] + + for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]: + assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format'] + + for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]: + assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['access']['format'] + + +def test_logging_pass_customer_logconfig(): + reset_logging() + + modified_config = LOGGING_CONFIG_DEFAULTS + modified_config['formatters']['generic']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s' + modified_config['formatters']['access']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s' + + app = Sanic("test_logging", log_config=modified_config) + + for fmt in [h.formatter for h in logging.getLogger('root').handlers]: + assert fmt._fmt == modified_config['formatters']['generic']['format'] + + for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]: + assert fmt._fmt == modified_config['formatters']['generic']['format'] + + for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]: + assert fmt._fmt == modified_config['formatters']['access']['format'] From 2e5d1ddff985da5ec116d50560eca323188f647a Mon Sep 17 00:00:00 2001 From: aiosin Date: Wed, 13 Sep 2017 14:08:29 +0200 Subject: [PATCH 066/104] add status codes and teapot example --- examples/teapot.py | 13 +++++++++++++ sanic/response.py | 2 ++ 2 files changed, 15 insertions(+) create mode 100644 examples/teapot.py diff --git a/examples/teapot.py b/examples/teapot.py new file mode 100644 index 00000000..897f7836 --- /dev/null +++ b/examples/teapot.py @@ -0,0 +1,13 @@ +from sanic import Sanic +from sanic import response as res + +app = Sanic(__name__) + + +@app.route("/") +async def test(req): + return res.text("I\'m a teapot", status=418) + + +if __name__ == '__main__': + app.run(host="0.0.0.0", port=8000) diff --git a/sanic/response.py b/sanic/response.py index 902b21c6..3b0ef449 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -56,6 +56,7 @@ ALL_STATUS_CODES = { 415: b'Unsupported Media Type', 416: b'Requested Range Not Satisfiable', 417: b'Expectation Failed', + 418: b'I\'m a teapot', 422: b'Unprocessable Entity', 423: b'Locked', 424: b'Failed Dependency', @@ -63,6 +64,7 @@ ALL_STATUS_CODES = { 428: b'Precondition Required', 429: b'Too Many Requests', 431: b'Request Header Fields Too Large', + 451: b'Unavailable For Legal Reasons', 500: b'Internal Server Error', 501: b'Not Implemented', 502: b'Bad Gateway', From 9c4b0f7b15f240f81b249848e649278226eb0a62 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 13 Sep 2017 07:40:42 -0700 Subject: [PATCH 067/104] fix flake8 --- sanic/__main__.py | 6 +-- sanic/config.py | 3 -- sanic/log.py | 98 +++++++++++++++++++++++------------------------ sanic/server.py | 13 ++++--- 4 files changed, 59 insertions(+), 61 deletions(-) diff --git a/sanic/__main__.py b/sanic/__main__.py index 1473aa5f..594256f8 100644 --- a/sanic/__main__.py +++ b/sanic/__main__.py @@ -37,8 +37,8 @@ if __name__ == "__main__": workers=args.workers, debug=args.debug, ssl=ssl) except ImportError as e: logger.error("No module named {} found.\n" - " Example File: project/sanic_server.py -> app\n" - " Example Module: project.sanic_server.app" - .format(e.name)) + " Example File: project/sanic_server.py -> app\n" + " Example Module: project.sanic_server.app" + .format(e.name)) except ValueError as e: logger.error("{}".format(e)) diff --git a/sanic/config.py b/sanic/config.py index b5430445..a79c1c12 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,7 +1,4 @@ import os -import sys -import syslog -import platform import types diff --git a/sanic/log.py b/sanic/log.py index 72636cd0..f9f96005 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -2,58 +2,58 @@ import logging LOGGING_CONFIG_DEFAULTS = dict( - version=1, - disable_existing_loggers=False, + version=1, + disable_existing_loggers=False, - loggers={ - "root": { - "level": "INFO", - "handlers": ["console"] - }, - "sanic.error": { - "level": "INFO", - "handlers": ["error_console"], - "propagate": True, - "qualname": "sanic.error" - }, + loggers={ + "root": { + "level": "INFO", + "handlers": ["console"] + }, + "sanic.error": { + "level": "INFO", + "handlers": ["error_console"], + "propagate": True, + "qualname": "sanic.error" + }, - "sanic.access": { - "level": "INFO", - "handlers": ["access_console"], - "propagate": True, - "qualname": "sanic.access" - } - }, - handlers={ - "console": { - "class": "logging.StreamHandler", - "formatter": "generic", - "stream": "sys.stdout" - }, - "error_console": { - "class": "logging.StreamHandler", - "formatter": "generic", - "stream": "sys.stderr" - }, - "access_console": { - "class": "logging.StreamHandler", - "formatter": "access", - "stream": "sys.stdout" - }, - }, - formatters={ - "generic": { - "format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s", - "datefmt": "[%Y-%m-%d %H:%M:%S %z]", - "class": "logging.Formatter" - }, - "access": { - "format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " + - "%(request)s %(message)s %(status)d %(byte)d", - "datefmt": "[%Y-%m-%d %H:%M:%S %z]", - "class": "logging.Formatter" - }, + "sanic.access": { + "level": "INFO", + "handlers": ["access_console"], + "propagate": True, + "qualname": "sanic.access" } + }, + handlers={ + "console": { + "class": "logging.StreamHandler", + "formatter": "generic", + "stream": "sys.stdout" + }, + "error_console": { + "class": "logging.StreamHandler", + "formatter": "generic", + "stream": "sys.stderr" + }, + "access_console": { + "class": "logging.StreamHandler", + "formatter": "access", + "stream": "sys.stdout" + }, + }, + formatters={ + "generic": { + "format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s", + "datefmt": "[%Y-%m-%d %H:%M:%S %z]", + "class": "logging.Formatter" + }, + "access": { + "format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " + + "%(request)s %(message)s %(status)d %(byte)d", + "datefmt": "[%Y-%m-%d %H:%M:%S %z]", + "class": "logging.Formatter" + }, + } ) diff --git a/sanic/server.py b/sanic/server.py index 7764b120..75c4ee55 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -24,7 +24,7 @@ try: except ImportError: async_loop = asyncio -from sanic.log import logger, access_logger, error_logger +from sanic.log import logger, access_logger from sanic.response import HTTPResponse from sanic.request import Request from sanic.exceptions import ( @@ -424,9 +424,9 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_timeout=60, ssl=None, sock=None, request_max_size=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, register_sys_signals=True, run_async=False, connections=None, - signal=Signal(), request_class=None, access_log=True, keep_alive=True, - is_request_stream=False, router=None, websocket_max_size=None, - websocket_max_queue=None, state=None, + signal=Signal(), request_class=None, access_log=True, + keep_alive=True, is_request_stream=False, router=None, + websocket_max_size=None, websocket_max_queue=None, state=None, graceful_shutdown_timeout=15.0): """Start asynchronous HTTP Server on an individual process. @@ -519,8 +519,9 @@ def serve(host, port, request_handler, error_handler, before_start=None, try: loop.add_signal_handler(_signal, loop.stop) except NotImplementedError: - logger.warn('Sanic tried to use loop.add_signal_handler but it is' - ' not implemented on this platform.') + logger.warn( + 'Sanic tried to use loop.add_signal_handler but it is' + ' not implemented on this platform.') pid = os.getpid() try: logger.info('Starting worker [{}]'.format(pid)) From 5ee7b6caeba6b39ac0400c59ed9a16c0de316427 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 13 Sep 2017 10:35:34 -0700 Subject: [PATCH 068/104] fixing small issue --- sanic/app.py | 6 ++---- sanic/log.py | 7 ++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 1d4497d0..f06e9cd4 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -570,7 +570,7 @@ class Sanic: def run(self, host=None, port=None, debug=False, ssl=None, sock=None, workers=1, protocol=None, backlog=100, stop_event=None, register_sys_signals=True, - access_log=True, log_config=None): + access_log=True): """Run the HTTP Server and listen until keyboard interrupt or term signal. On termination, drain connections before closing. @@ -588,7 +588,6 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ - logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) if sock is None: host, port = host or "127.0.0.1", port or 8000 @@ -632,13 +631,12 @@ class Sanic: async def create_server(self, host=None, port=None, debug=False, ssl=None, sock=None, protocol=None, backlog=100, stop_event=None, - access_log=True, log_config=None): + access_log=True): """Asynchronous version of `run`. NOTE: This does not support multiprocessing and is not the preferred way to run a Sanic application. """ - logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) if sock is None: host, port = host or "127.0.0.1", port or 8000 diff --git a/sanic/log.py b/sanic/log.py index f9f96005..9c6d868d 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,4 +1,5 @@ import logging +import sys LOGGING_CONFIG_DEFAULTS = dict( @@ -28,17 +29,17 @@ LOGGING_CONFIG_DEFAULTS = dict( "console": { "class": "logging.StreamHandler", "formatter": "generic", - "stream": "sys.stdout" + "stream": sys.stdout }, "error_console": { "class": "logging.StreamHandler", "formatter": "generic", - "stream": "sys.stderr" + "stream": sys.stderr }, "access_console": { "class": "logging.StreamHandler", "formatter": "access", - "stream": "sys.stdout" + "stream": sys.stdout }, }, formatters={ From ddc039ed2e0df68908f2a9c673c6440f2704fa8d Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 13 Sep 2017 18:14:46 -0700 Subject: [PATCH 069/104] update doc --- docs/sanic/logging.md | 65 ++++++++----------------------------------- 1 file changed, 12 insertions(+), 53 deletions(-) diff --git a/docs/sanic/logging.md b/docs/sanic/logging.md index eb807388..092df5c7 100644 --- a/docs/sanic/logging.md +++ b/docs/sanic/logging.md @@ -9,12 +9,6 @@ A simple example using default settings would be like this: ```python from sanic import Sanic -from sanic.config import LOGGING - -# The default logging handlers are ['accessStream', 'errorStream'] -# but we change it to use other handlers here for demo purpose -LOGGING['loggers']['network']['handlers'] = [ - 'accessSysLog', 'errorSysLog'] app = Sanic('test') @@ -23,14 +17,14 @@ async def test(request): return response.text('Hello World!') if __name__ == "__main__": - app.run(log_config=LOGGING) + app.run(debug=True, access_log=True) ``` -And to close logging, simply assign log_config=None: +And to close logging, simply assign access_log=False: ```python if __name__ == "__main__": - app.run(log_config=None) + app.run(access_log=False) ``` This would skip calling logging functions when handling requests. @@ -38,59 +32,24 @@ And you could even do further in production to gain extra speed: ```python if __name__ == "__main__": - # disable internal messages - app.run(debug=False, log_config=None) + # disable debug messages + app.run(debug=False, access_log=False) ``` ### Configuration -By default, log_config parameter is set to use sanic.config.LOGGING dictionary for configuration. The default configuration provides several predefined `handlers`: +By default, log_config parameter is set to use sanic.log.LOGGING_CONFIG_DEFAULTS dictionary for configuration. -- internal (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))
- For internal information console outputs. +There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**: - -- accessStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))
- For requests information logging in console - - -- errorStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))
- For error message and traceback logging in console. - - -- accessSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))
- For requests information logging to syslog. - Currently supports Windows (via localhost:514), Darwin (/var/run/syslog), - Linux (/dev/log) and FreeBSD (/dev/log).
- You would not be able to access this property if the directory doesn't exist. - (Notice that in Docker you have to enable everything by yourself) - - -- errorSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))
- For error message and traceback logging to syslog. - Currently supports Windows (via localhost:514), Darwin (/var/run/syslog), - Linux (/dev/log) and FreeBSD (/dev/log).
- You would not be able to access this property if the directory doesn't exist. - (Notice that in Docker you have to enable everything by yourself) - - -And `filters`: - -- accessFilter (using sanic.log.DefaultFilter)
- The filter that allows only levels in `DEBUG`, `INFO`, and `NONE(0)` - - -- errorFilter (using sanic.log.DefaultFilter)
- The filter that allows only levels in `WARNING`, `ERROR`, and `CRITICAL` - -There are two `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**: - -- sanic:
+- root:
Used to log internal messages. +- sanic.error:
+ Used to log error logs. -- network:
- Used to log requests from network, and any information from those requests. +- sanic.access:
+ Used to log access logs. #### Log format: From 5cabc9cff2dc023db0e0667cba232e30cc1dbd96 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 13 Sep 2017 18:16:58 -0700 Subject: [PATCH 070/104] update doc --- docs/sanic/logging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sanic/logging.md b/docs/sanic/logging.md index 092df5c7..286282f0 100644 --- a/docs/sanic/logging.md +++ b/docs/sanic/logging.md @@ -54,7 +54,7 @@ There are three `loggers` used in sanic, and **must be defined if you want to cr #### Log format: In addition to default parameters provided by python (asctime, levelname, message), -Sanic provides additional parameters for network logger with accessFilter: +Sanic provides additional parameters for access logger with: - host (str)
request.ip From 730f7c5e41635d5ab7a78102f4c534acab758392 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 13 Sep 2017 18:30:38 -0700 Subject: [PATCH 071/104] add doc for customizing logging config --- docs/sanic/logging.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/sanic/logging.md b/docs/sanic/logging.md index 286282f0..49805d0e 100644 --- a/docs/sanic/logging.md +++ b/docs/sanic/logging.md @@ -20,6 +20,13 @@ if __name__ == "__main__": app.run(debug=True, access_log=True) ``` +To use your own logging config, simply use `logging.config.dictConfig`, or +pass `log_config` when you initialize `Sanic` app: + +```python +app = Sanic('test', log_config=LOGGING_CONFIG) +``` + And to close logging, simply assign access_log=False: ```python From eb1146c6b66827b6178980353e35e68c83f9619f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?W=C3=A8i=20C=C5=8Dngru=C3=AC?= Date: Thu, 14 Sep 2017 18:40:20 +0800 Subject: [PATCH 072/104] fix #763, sanic can't decode latin1 encoded header value --- sanic/server.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index f62ba654..4dedc650 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -189,10 +189,12 @@ class HttpProtocol(asyncio.Protocol): and int(value) > self.request_max_size: exception = PayloadTooLarge('Payload Too Large') self.write_error(exception) - + try: + value = value.decode() + except UnicodeDecodeError: + value = value.decode('latin_1') self.headers.append( - (self._header_fragment.decode().casefold(), - value.decode())) + (self._header_fragment.decode().casefold(), value)) self._header_fragment = b'' From 12dafd07b87e3412f48cfb3672c1804b3879464c Mon Sep 17 00:00:00 2001 From: huyuhan Date: Fri, 15 Sep 2017 18:34:56 +0800 Subject: [PATCH 073/104] add __repr__ for sanic request --- sanic/request.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sanic/request.py b/sanic/request.py index 27ff011e..4e8a2e07 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -68,6 +68,11 @@ class Request(dict): self._cookies = None self.stream = None + def __repr__(self): + if self.method is None or not self._parsed_url: + return '<%s>' % self.__class__.__name__ + return '<%s: %s %r>' % (self.__class__.__name__, self.method, self.path) + @property def json(self): if self.parsed_json is None: From 77f70a0792eef25841039dcd1e2266f2d78e79f2 Mon Sep 17 00:00:00 2001 From: huyuhan Date: Fri, 15 Sep 2017 20:56:44 +0800 Subject: [PATCH 074/104] add __repr__ for sanic request --- sanic/request.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 4e8a2e07..fa80b47c 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -19,8 +19,9 @@ except ImportError: from sanic.exceptions import InvalidUsage from sanic.log import log - DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" + + # HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1 # > If the media type remains unknown, the recipient SHOULD treat it # > as type "application/octet-stream" @@ -69,9 +70,9 @@ class Request(dict): self.stream = None def __repr__(self): - if self.method is None or not self._parsed_url: + if self.method is None or not self.path: return '<%s>' % self.__class__.__name__ - return '<%s: %s %r>' % (self.__class__.__name__, self.method, self.path) + return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.path) @property def json(self): @@ -175,8 +176,8 @@ class Request(dict): remote_addrs = [ addr for addr in [ addr.strip() for addr in forwarded_for - ] if addr - ] + ] if addr + ] if len(remote_addrs) > 0: self._remote_addr = remote_addrs[0] else: From f6eb35f67d6637fdd4b13e4d38ba17d4d21f1fb3 Mon Sep 17 00:00:00 2001 From: huyuhan Date: Fri, 15 Sep 2017 21:05:25 +0800 Subject: [PATCH 075/104] add __repr__ for sanic request --- sanic/request.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index fa80b47c..ea5c071e 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -71,8 +71,10 @@ class Request(dict): def __repr__(self): if self.method is None or not self.path: - return '<%s>' % self.__class__.__name__ - return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.path) + return '<{class_name}>'.format(class_name=self.__class__.__name__) + return '<{class_name}: {method} {path}>'.format(class_name=self.__class__.__name__, + method=self.method, + path=self.path) @property def json(self): From 074d36eeba5a793b0c87becfda81603fcc5cb824 Mon Sep 17 00:00:00 2001 From: huyuhan Date: Fri, 15 Sep 2017 21:15:05 +0800 Subject: [PATCH 076/104] add __repr__ for sanic request --- sanic/request.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index ea5c071e..26b14fd9 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -71,10 +71,10 @@ class Request(dict): def __repr__(self): if self.method is None or not self.path: - return '<{class_name}>'.format(class_name=self.__class__.__name__) - return '<{class_name}: {method} {path}>'.format(class_name=self.__class__.__name__, - method=self.method, - path=self.path) + return '<{0}>'.format(self.__class__.__name__) + return '<{0}: {1} {2}>'.format(self.__class__.__name__, + self.method, + self.path) @property def json(self): From c836441a75eada7c50faa70f8ae682fc5c3d8704 Mon Sep 17 00:00:00 2001 From: Kuzma Leshakov Date: Mon, 18 Sep 2017 11:37:32 +0300 Subject: [PATCH 077/104] Update getting_started.md Hello World example at the main Readme file (https://github.com/channelcat/sanic/blob/master/README.rst) is different, it returns json. Here is returned text. In the following examples, such as Routing (http://sanic.readthedocs.io/en/latest/sanic/routing.html) is again used json. Therefore I suggest to make examples the same, having json as output --- docs/sanic/getting_started.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/sanic/getting_started.md b/docs/sanic/getting_started.md index 04d22248..3e89cc3e 100644 --- a/docs/sanic/getting_started.md +++ b/docs/sanic/getting_started.md @@ -9,15 +9,16 @@ syntax, so earlier versions of python won't work. ```python from sanic import Sanic - from sanic.response import text + from sanic.response import json - app = Sanic(__name__) + app = Sanic() @app.route("/") async def test(request): - return text('Hello world!') + return json({"hello": "world"}) - app.run(host="0.0.0.0", port=8000, debug=True) + if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) ``` 3. Run the server: `python3 main.py` From d8cebe1188bf03d424639b4f944c15f01dad668c Mon Sep 17 00:00:00 2001 From: lanf0n Date: Tue, 19 Sep 2017 18:14:25 +0800 Subject: [PATCH 078/104] to fix if platform is windows. --- sanic/config.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index 0c2cc701..ec2dd5ed 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,6 +1,5 @@ import os import sys -import syslog import platform import types @@ -8,6 +7,11 @@ from sanic.log import DefaultFilter SANIC_PREFIX = 'SANIC_' +try: + from syslog import LOG_DAEMON +except ImportError: + LOG_DAEMON = 24 + _address_dict = { 'Windows': ('localhost', 514), 'Darwin': '/var/run/syslog', @@ -66,7 +70,7 @@ LOGGING = { 'class': 'logging.handlers.SysLogHandler', 'address': _address_dict.get(platform.system(), ('localhost', 514)), - 'facility': syslog.LOG_DAEMON, + 'facility': LOG_DAEMON, 'filters': ['accessFilter'], 'formatter': 'access' }, @@ -74,7 +78,7 @@ LOGGING = { 'class': 'logging.handlers.SysLogHandler', 'address': _address_dict.get(platform.system(), ('localhost', 514)), - 'facility': syslog.LOG_DAEMON, + 'facility': LOG_DAEMON, 'filters': ['errorFilter'], 'formatter': 'simple' }, From 1d719252cbc6ce79943e76d317cecd57b1872fe4 Mon Sep 17 00:00:00 2001 From: Hugh McNamara Date: Tue, 19 Sep 2017 14:58:49 +0100 Subject: [PATCH 079/104] use dependency injection to allow alternative json parser or encoder --- sanic/request.py | 4 ++-- sanic/response.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 27ff011e..aa778a8d 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -69,10 +69,10 @@ class Request(dict): self.stream = None @property - def json(self): + def json(self, loads=json_loads): if self.parsed_json is None: try: - self.parsed_json = json_loads(self.body) + self.parsed_json = loads(self.body) except Exception: if not self.body: return None diff --git a/sanic/response.py b/sanic/response.py index 3b0ef449..f661758b 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -237,7 +237,8 @@ class HTTPResponse(BaseHTTPResponse): def json(body, status=200, headers=None, - content_type="application/json", **kwargs): + content_type="application/json", dumps=json_dumps, + **kwargs): """ Returns response object with body in json format. @@ -246,7 +247,7 @@ def json(body, status=200, headers=None, :param headers: Custom Headers. :param kwargs: Remaining arguments that are passed to the json encoder. """ - return HTTPResponse(json_dumps(body, **kwargs), headers=headers, + return HTTPResponse(dumps(body, **kwargs), headers=headers, status=status, content_type=content_type) From a8f764c1612ab17eb99924ac2363c05110a1911d Mon Sep 17 00:00:00 2001 From: Hugh McNamara Date: Tue, 19 Sep 2017 18:12:53 +0100 Subject: [PATCH 080/104] make method instead of property for alternative json decoding of request --- sanic/request.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index aa778a8d..74200fe0 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -69,10 +69,10 @@ class Request(dict): self.stream = None @property - def json(self, loads=json_loads): + def json(self): if self.parsed_json is None: try: - self.parsed_json = loads(self.body) + self.parsed_json = json_loads(self.body) except Exception: if not self.body: return None @@ -80,6 +80,16 @@ class Request(dict): return self.parsed_json + def load_json(self, loads=json_loads): + try: + self.parsed_json = loads(self.body) + except Exception: + if not self.body: + return None + raise InvalidUsage("Failed when parsing body as json") + + return self.parsed_json + @property def token(self): """Attempt to return the auth header token. From 5cef1634edd029e914d0184627e98bcb63198173 Mon Sep 17 00:00:00 2001 From: Hugh McNamara Date: Fri, 22 Sep 2017 10:19:15 +0100 Subject: [PATCH 081/104] use json_loads function in json property of request --- sanic/request.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 74200fe0..43ecc511 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -71,12 +71,7 @@ class Request(dict): @property def json(self): if self.parsed_json is None: - try: - self.parsed_json = json_loads(self.body) - except Exception: - if not self.body: - return None - raise InvalidUsage("Failed when parsing body as json") + self.load_json() return self.parsed_json From f96ab027677d525d1a690223d217c27cc05517e9 Mon Sep 17 00:00:00 2001 From: lixxu Date: Wed, 27 Sep 2017 09:59:49 +0800 Subject: [PATCH 082/104] set scheme to http if not provided --- sanic/app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/app.py b/sanic/app.py index 3776c915..5d80b360 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -460,7 +460,10 @@ class Sanic: if external: if not scheme: - scheme = netloc[:8].split(':', 1)[0] + if ':' in netloc[:8]: + scheme = netloc[:8].split(':', 1)[0] + else: + scheme = 'http' if '://' in netloc[:8]: netloc = netloc.split('://', 1)[-1] From 91b2167ebac836b54205e7e9a031597bd3ea6ee6 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 27 Sep 2017 11:07:06 +0300 Subject: [PATCH 083/104] Update extensions.md Add - [JWT](https://github.com/ahopkins/sanic-jwt): Authentication extension for JSON Web Tokens (JWT) extension package. --- docs/sanic/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index 5643f4fc..ad9b8156 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -7,6 +7,7 @@ A list of Sanic extensions created by the community. - [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors. - [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress. - [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template. +- [JWT](https://github.com/ahopkins/sanic-jwt): Authentication extension for JSON Web Tokens (JWT). - [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI. - [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support. - [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper. From 9aec5febb8fc33972f0ff13bed9c11e6865ec4ab Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 27 Sep 2017 01:24:43 -0700 Subject: [PATCH 084/104] support vhosts in static routes --- sanic/app.py | 4 ++-- sanic/static.py | 4 ++-- tests/test_static.py | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 5d80b360..d553f09d 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -354,13 +354,13 @@ class Sanic: # Static Files def static(self, uri, file_or_directory, pattern=r'/?.+', use_modified_since=True, use_content_range=False, - stream_large_files=False, name='static'): + stream_large_files=False, name='static', host=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) + stream_large_files, name, host) def blueprint(self, blueprint, **options): """Register a blueprint on the application. diff --git a/sanic/static.py b/sanic/static.py index a9683b27..1ebd7291 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -18,7 +18,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'): + stream_large_files, name='static', host=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 @@ -122,4 +122,4 @@ def register(app, uri, file_or_directory, pattern, if not name.startswith('_static_'): name = '_static_{}'.format(name) - app.route(uri, methods=['GET', 'HEAD'], name=name)(_handler) + app.route(uri, methods=['GET', 'HEAD'], name=name, host=host)(_handler) diff --git a/tests/test_static.py b/tests/test_static.py index 091d63a4..6252b1c1 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -161,3 +161,20 @@ def test_static_content_range_error(file_name, static_file_directory): assert 'Content-Range' in response.headers assert response.headers['Content-Range'] == "bytes */%s" % ( len(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') + app.static( + '/testing.file', + get_file_path(static_file_directory, file_name), + host="www.example.com" + ) + + headers = {"Host": "www.example.com"} + request, response = app.test_client.get('/testing.file', headers=headers) + assert response.status == 200 + assert response.body == get_file_content(static_file_directory, file_name) + request, response = app.test_client.get('/testing.file') + assert response.status == 404 From 62871ec9b31345863f5c61e5b52c21c98cdabd32 Mon Sep 17 00:00:00 2001 From: lanf0n Date: Sat, 30 Sep 2017 01:16:26 +0800 Subject: [PATCH 085/104] add sphinx extension to add asyncio-specific markups --- docs/conf.py | 2 +- requirements-docs.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index e254c183..7dd7462c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ import sanic # -- General configuration ------------------------------------------------ -extensions = ['sphinx.ext.autodoc'] +extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.asyncio'] templates_path = ['_templates'] diff --git a/requirements-docs.txt b/requirements-docs.txt index efa74079..e12c1846 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,3 +1,4 @@ sphinx sphinx_rtd_theme recommonmark +sphinxcontrib-asyncio From e3852ceeca94317b11e0c24841d1678abf8806e1 Mon Sep 17 00:00:00 2001 From: Piotr Bulinski Date: Wed, 4 Oct 2017 12:50:57 +0200 Subject: [PATCH 086/104] Refactor access log for server --- sanic/server.py | 63 +++++++++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 12efc180..e66afcf8 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -300,6 +300,28 @@ class HttpProtocol(asyncio.Protocol): # -------------------------------------------- # # Responding # -------------------------------------------- # + def log_response(self, response): + if self.has_log: + extra = { + 'status': getattr(response, 'status', 0), + } + + if isinstance(response, HTTPResponse): + extra['byte'] = len(response.body) + else: + extra['byte'] = -1 + + if self.request: + extra['host'] = '{0}:{1}'.format(self.request.ip[0], + self.request.ip[1]) + extra['request'] = '{0} {1}'.format(self.request.method, + self.request.url) + else: + extra['host'] = 'UNKNOWN' + extra['request'] = 'nil' + + netlog.info('', extra=extra) + def write_response(self, response): """ Writes response content synchronously to the transport. @@ -313,15 +335,7 @@ class HttpProtocol(asyncio.Protocol): response.output( self.request.version, keep_alive, self.keep_alive_timeout)) - if self.has_log: - netlog.info('', extra={ - 'status': response.status, - 'byte': len(response.body), - 'host': '{0}:{1}'.format(self.request.ip[0], - self.request.ip[1]), - 'request': '{0} {1}'.format(self.request.method, - self.request.url) - }) + self.log_response(response) except AttributeError: log.error( ('Invalid response object for url {}, ' @@ -360,15 +374,7 @@ class HttpProtocol(asyncio.Protocol): response.transport = self.transport await response.stream( self.request.version, keep_alive, self.keep_alive_timeout) - if self.has_log: - netlog.info('', extra={ - 'status': response.status, - 'byte': -1, - 'host': '{0}:{1}'.format(self.request.ip[0], - self.request.ip[1]), - 'request': '{0} {1}'.format(self.request.method, - self.request.url) - }) + self.log_response(response) except AttributeError: log.error( ('Invalid response object for url {}, ' @@ -414,24 +420,9 @@ class HttpProtocol(asyncio.Protocol): repr(e)), from_error=True ) finally: - if self.has_log: - extra = dict() - if isinstance(response, HTTPResponse): - extra['status'] = response.status - extra['byte'] = len(response.body) - else: - extra['status'] = 0 - extra['byte'] = -1 - if self.request: - extra['host'] = '%s:%d' % self.request.ip, - extra['request'] = '%s %s' % (self.request.method, - self.url) - else: - extra['host'] = 'UNKNOWN' - extra['request'] = 'nil' - if self.parser and not (self.keep_alive - and extra['status'] == 408): - netlog.info('', extra=extra) + if self.parser and (self.keep_alive + or getattr(response, 'status', 0) == 408): + self.log_response(response) self.transport.close() def bail_out(self, message, from_error=False): From 8ce749e339509e30e4e87bc854da4200cc2409c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Buli=C5=84ski?= Date: Thu, 5 Oct 2017 09:27:18 +0200 Subject: [PATCH 087/104] Update server.py --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index a17e479b..8f67901d 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -320,7 +320,7 @@ class HttpProtocol(asyncio.Protocol): extra['host'] = 'UNKNOWN' extra['request'] = 'nil' - netlog.info('', extra=extra) + access_logger.info('', extra=extra) def write_response(self, response): """ From 4b877e3f6bab0a754fa1907970e1b0842e824995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Buli=C5=84ski?= Date: Thu, 5 Oct 2017 09:28:13 +0200 Subject: [PATCH 088/104] Update server.py --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index 8f67901d..e151c54f 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -301,7 +301,7 @@ class HttpProtocol(asyncio.Protocol): # Responding # -------------------------------------------- # def log_response(self, response): - if self.has_log: + if self.access_log: extra = { 'status': getattr(response, 'status', 0), } From d876e3ed5c95116f65b3f244e4ce76ca4149dc29 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 5 Oct 2017 22:20:36 -0700 Subject: [PATCH 089/104] fix false cookie encoding and output --- sanic/cookies.py | 3 ++- tests/test_cookies.py | 23 +++++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/sanic/cookies.py b/sanic/cookies.py index 16b798df..8ad8cbfc 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -98,7 +98,8 @@ class Cookie(dict): def __setitem__(self, key, value): if key not in self._keys: raise KeyError("Unknown cookie property") - return super().__setitem__(key, value) + if value is not False: + return super().__setitem__(key, value) def encode(self, encoding): output = ['%s=%s' % (self.key, _quote(self.value))] diff --git a/tests/test_cookies.py b/tests/test_cookies.py index d88288ee..84b493cb 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -25,6 +25,25 @@ 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') + + @app.route('/') + def handler(request): + response = text('hello cookies') + response.cookies['hello'] = 'world' + response.cookies['hello']['httponly'] = httponly + return text(response.cookies['hello'].encode('utf8')) + + request, response = app.test_client.get('/') + + assert ('HttpOnly' in response.text) == expected + + @pytest.mark.parametrize("httponly,expected", [ (False, False), (True, True), @@ -34,7 +53,7 @@ def test_false_cookies(httponly, expected): @app.route('/') def handler(request): - response = text('Cookies are: {}'.format(request.cookies['test'])) + response = text('hello cookies') response.cookies['right_back'] = 'at you' response.cookies['right_back']['httponly'] = httponly return response @@ -43,7 +62,7 @@ def test_false_cookies(httponly, expected): response_cookies = SimpleCookie() response_cookies.load(response.headers.get('Set-Cookie', {})) - 'HttpOnly' in response_cookies == expected + assert ('HttpOnly' in response_cookies['right_back'].output()) == expected def test_http2_cookies(): app = Sanic('test_http2_cookies') From 4b3920daba560b6b318a525be437f16bf59fbe16 Mon Sep 17 00:00:00 2001 From: Max Murashov Date: Fri, 6 Oct 2017 16:53:30 +0300 Subject: [PATCH 090/104] Fix logs --- sanic/handlers.py | 20 +++++++++---------- sanic/server.py | 51 +++++++++++++++++++---------------------------- sanic/worker.py | 5 ++--- 3 files changed, 33 insertions(+), 43 deletions(-) diff --git a/sanic/handlers.py b/sanic/handlers.py index 2e335b09..9afcfb94 100644 --- a/sanic/handlers.py +++ b/sanic/handlers.py @@ -86,12 +86,13 @@ class ErrorHandler: self.log(format_exc()) if self.debug: url = getattr(request, 'url', 'unknown') - response_message = ( - 'Exception raised in exception handler "{}" ' - 'for uri: "{}"\n{}').format( - handler.__name__, url, format_exc()) - logger.error(response_message) - return text(response_message, 500) + response_message = ('Exception raised in exception handler ' + '"%s" for uri: "%s"\n%s') + logger.error(response_message, + handler.__name__, url, format_exc()) + + return text(response_message % ( + handler.__name__, url, format_exc()), 500) else: return text('An error occurred while handling an error', 500) return response @@ -114,10 +115,9 @@ class ErrorHandler: elif self.debug: html_output = self._render_traceback_html(exception, request) - response_message = ( - 'Exception occurred while handling uri: "{}"\n{}'.format( - request.url, format_exc())) - logger.error(response_message) + response_message = ('Exception occurred while handling uri: ' + '"%s"\n%s') + logger.error(response_message, request.url, format_exc()) return html(html_output, status=500) else: return html(INTERNAL_SERVER_ERROR_HTML, status=500) diff --git a/sanic/server.py b/sanic/server.py index e151c54f..8f60a864 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -337,15 +337,13 @@ class HttpProtocol(asyncio.Protocol): self.keep_alive_timeout)) self.log_response(response) except AttributeError: - logger.error( - ('Invalid response object for url {}, ' - 'Expected Type: HTTPResponse, Actual Type: {}').format( - self.url, type(response))) + logger.error('Invalid response object for url %s, ' + 'Expected Type: HTTPResponse, Actual Type: %s', + self.url, type(response)) self.write_error(ServerError('Invalid response type')) except RuntimeError: - logger.error( - 'Connection lost before response written @ {}'.format( - self.request.ip)) + logger.error('Connection lost before response written @ %s', + self.request.ip) except Exception as e: self.bail_out( "Writing response failed, connection closed {}".format( @@ -376,15 +374,13 @@ class HttpProtocol(asyncio.Protocol): self.request.version, keep_alive, self.keep_alive_timeout) self.log_response(response) except AttributeError: - logger.error( - ('Invalid response object for url {}, ' - 'Expected Type: HTTPResponse, Actual Type: {}').format( - self.url, type(response))) + logger.error('Invalid response object for url %s, ' + 'Expected Type: HTTPResponse, Actual Type: %s', + self.url, type(response)) self.write_error(ServerError('Invalid response type')) except RuntimeError: - logger.error( - 'Connection lost before response written @ {}'.format( - self.request.ip)) + logger.error('Connection lost before response written @ %s', + self.request.ip) except Exception as e: self.bail_out( "Writing response failed, connection closed {}".format( @@ -411,9 +407,8 @@ class HttpProtocol(asyncio.Protocol): version = self.request.version if self.request else '1.1' self.transport.write(response.output(version)) except RuntimeError: - logger.error( - 'Connection lost before error written @ {}'.format( - self.request.ip if self.request else 'Unknown')) + logger.error('Connection lost before error written @ %s', + self.request.ip if self.request else 'Unknown') except Exception as e: self.bail_out( "Writing error failed, connection closed {}".format( @@ -427,12 +422,10 @@ class HttpProtocol(asyncio.Protocol): def bail_out(self, message, from_error=False): if from_error or self.transport.is_closing(): - logger.error( - ("Transport closed @ {} and exception " - "experienced during error handling").format( - self.transport.get_extra_info('peername'))) - logger.debug( - 'Exception:\n{}'.format(traceback.format_exc())) + logger.error("Transport closed @ %s and exception " + "experienced during error handling", + self.transport.get_extra_info('peername')) + logger.debug('Exception:\n%s', traceback.format_exc()) else: exception = ServerError(message) self.write_error(exception) @@ -597,15 +590,14 @@ def serve(host, port, request_handler, error_handler, before_start=None, try: loop.add_signal_handler(_signal, loop.stop) except NotImplementedError: - logger.warn( - 'Sanic tried to use loop.add_signal_handler but it is' - ' not implemented on this platform.') + logger.warning('Sanic tried to use loop.add_signal_handler ' + 'but it is not implemented on this platform.') pid = os.getpid() try: - logger.info('Starting worker [{}]'.format(pid)) + logger.info('Starting worker [%s]', pid) loop.run_forever() finally: - logger.info("Stopping worker [{}]".format(pid)) + logger.info("Stopping worker [%s]", pid) # Run the on_stop function if provided trigger_events(before_stop, loop) @@ -667,8 +659,7 @@ def serve_multiple(server_settings, workers): server_settings['port'] = None def sig_handler(signal, frame): - logger.info("Received signal {}. Shutting down.".format( - Signals(signal).name)) + logger.info("Received signal %s. Shutting down.", Signals(signal).name) for process in processes: os.kill(process.pid, SIGINT) diff --git a/sanic/worker.py b/sanic/worker.py index 9f950c34..811c7e5c 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -142,9 +142,8 @@ class GunicornWorker(base.Worker): ) if self.max_requests and req_count > self.max_requests: self.alive = False - self.log.info( - "Max requests exceeded, shutting down: %s", self - ) + self.log.info("Max requests exceeded, shutting down: %s", + self) elif pid == os.getpid() and self.ppid != os.getppid(): self.alive = False self.log.info("Parent changed, shutting down: %s", self) From 7610c0fb2e0ec76b254ecb11dffa1c5161da61bf Mon Sep 17 00:00:00 2001 From: Maks Skorokhod Date: Mon, 9 Oct 2017 15:50:36 +0300 Subject: [PATCH 091/104] :wrench: log Connection lost only if debug --- sanic/server.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 8f60a864..06c749a5 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -342,8 +342,10 @@ class HttpProtocol(asyncio.Protocol): self.url, type(response)) self.write_error(ServerError('Invalid response type')) except RuntimeError: - logger.error('Connection lost before response written @ %s', - self.request.ip) + if self._debug: + logger.error('Connection lost before response written @ %s', + self.request.ip) + keep_alive = False except Exception as e: self.bail_out( "Writing response failed, connection closed {}".format( @@ -379,8 +381,10 @@ class HttpProtocol(asyncio.Protocol): self.url, type(response)) self.write_error(ServerError('Invalid response type')) except RuntimeError: - logger.error('Connection lost before response written @ %s', - self.request.ip) + if self._debug: + logger.error('Connection lost before response written @ %s', + self.request.ip) + keep_alive = False except Exception as e: self.bail_out( "Writing response failed, connection closed {}".format( @@ -407,8 +411,9 @@ class HttpProtocol(asyncio.Protocol): version = self.request.version if self.request else '1.1' self.transport.write(response.output(version)) except RuntimeError: - logger.error('Connection lost before error written @ %s', - self.request.ip if self.request else 'Unknown') + if self._debug: + logger.error('Connection lost before error written @ %s', + self.request.ip if self.request else 'Unknown') except Exception as e: self.bail_out( "Writing error failed, connection closed {}".format( From 64edf7ad9c539fc05e70fd8b09f1c485a53472aa Mon Sep 17 00:00:00 2001 From: Maks Skorokhod Date: Mon, 9 Oct 2017 15:50:57 +0300 Subject: [PATCH 092/104] :white_check_mark: upd test for connection lost error --- tests/test_logging.py | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/tests/test_logging.py b/tests/test_logging.py index 112c94a0..4580d6b6 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,11 +1,17 @@ import uuid +import logging + +from io import StringIO from importlib import reload +import pytest +from unittest.mock import Mock + +import sanic from sanic.response import text from sanic.log import LOGGING_CONFIG_DEFAULTS from sanic import Sanic -from io import StringIO -import logging + logging_format = '''module: %(module)s; \ function: %(funcName)s(); \ @@ -71,3 +77,31 @@ def test_logging_pass_customer_logconfig(): for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]: assert fmt._fmt == modified_config['formatters']['access']['format'] + + +@pytest.mark.parametrize('debug', (True, False, )) +def test_log_connection_lost(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)) + monkeypatch.setattr(sanic.server, 'logger', root) + + @app.route('/conn_lost') + async def conn_lost(request): + response = text('Ok') + response.output = Mock(side_effect=RuntimeError) + return response + + with pytest.raises(ValueError): + # catch ValueError: Exception during request + app.test_client.get('/conn_lost', debug=debug) + + log = stream.getvalue() + + if debug: + assert log.startswith( + 'Connection lost before response written @') + else: + 'Connection lost before response written @' not in log From 4d515b05f3d8e7d3f3074b2f85146a34dc4d875d Mon Sep 17 00:00:00 2001 From: Maks Skorokhod Date: Mon, 9 Oct 2017 17:18:04 +0300 Subject: [PATCH 093/104] :white_check_mark: fix missed assertion --- tests/test_logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_logging.py b/tests/test_logging.py index 4580d6b6..e95b7ce5 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -104,4 +104,4 @@ def test_log_connection_lost(debug, monkeypatch): assert log.startswith( 'Connection lost before response written @') else: - 'Connection lost before response written @' not in log + assert 'Connection lost before response written @' not in log From 07e95dba4f5983afc1e673df14bdd278817288aa Mon Sep 17 00:00:00 2001 From: Maks Skorokhod Date: Mon, 9 Oct 2017 17:45:22 +0300 Subject: [PATCH 094/104] :repeat: customize filename in file response --- sanic/response.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index f661758b..c05be0fd 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -292,15 +292,21 @@ def html(body, status=200, headers=None): content_type="text/html; charset=utf-8") -async def file(location, mime_type=None, headers=None, _range=None): +async def file( + location, mime_type=None, headers=None, filename=None, _range=None): """Return a response object with file data. :param location: Location of file on system. :param mime_type: Specific mime_type. :param headers: Custom Headers. + :param filename: Override filename. :param _range: """ - filename = path.split(location)[-1] + headers = headers or {} + if filename: + headers.setdefault( + 'Content-Disposition', f'attachment; filename="{filename}"') + filename = filename or path.split(location)[-1] async with open_async(location, mode='rb') as _file: if _range: @@ -312,24 +318,29 @@ async def file(location, mime_type=None, headers=None, _range=None): out_stream = await _file.read() mime_type = mime_type or guess_type(filename)[0] or 'text/plain' - return HTTPResponse(status=200, headers=headers, content_type=mime_type, body_bytes=out_stream) -async def file_stream(location, chunk_size=4096, mime_type=None, headers=None, - _range=None): +async def file_stream( + location, 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. :param chunk_size: The size of each chunk in the stream (in bytes) :param mime_type: Specific mime_type. :param headers: Custom Headers. + :param filename: Override filename. :param _range: """ - filename = path.split(location)[-1] + headers = headers or {} + if filename: + headers.setdefault( + 'Content-Disposition', f'attachment; filename="{filename}"') + filename = filename or path.split(location)[-1] _file = await open_async(location, mode='rb') From c4e3a98ea794892bbda14d07c92ab61c868fc7cb Mon Sep 17 00:00:00 2001 From: Maks Skorokhod Date: Mon, 9 Oct 2017 17:45:42 +0300 Subject: [PATCH 095/104] :white_check_mark: add test for custom filename --- tests/test_response.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_response.py b/tests/test_response.py index fb213b56..910c4e80 100644 --- a/tests/test_response.py +++ b/tests/test_response.py @@ -149,7 +149,22 @@ def test_file_response(file_name, static_file_directory): request, response = app.test_client.get('/files/{}'.format(file_name)) assert response.status == 200 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') + @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, filename=dest) + + request, response = app.test_client.get('/files/{}'.format(source)) + assert response.status == 200 + assert response.body == get_file_content(static_file_directory, source) + assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest) @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) def test_file_head_response(file_name, static_file_directory): @@ -191,7 +206,22 @@ def test_file_stream_response(file_name, static_file_directory): request, response = app.test_client.get('/files/{}'.format(file_name)) assert response.status == 200 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_stream_response_custom_filename(source, dest, static_file_directory): + app = Sanic('test_file_helper') + @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_stream(file_path, chunk_size=32, filename=dest) + + request, response = app.test_client.get('/files/{}'.format(source)) + assert response.status == 200 + assert response.body == get_file_content(static_file_directory, source) + assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest) @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) def test_file_stream_head_response(file_name, static_file_directory): From 770a8fb28889a4bdcffaed5b3e6902ebff800bf6 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Mon, 9 Oct 2017 07:54:39 -0700 Subject: [PATCH 096/104] raise exception for invalid param syntax --- sanic/router.py | 2 ++ tests/test_routes.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/sanic/router.py b/sanic/router.py index f943bc19..c57d808f 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -93,6 +93,8 @@ class Router: pattern = 'string' if ':' in parameter_string: name, pattern = parameter_string.split(':', 1) + if not name: + raise ValueError("Invalid parameter syntax: {}".format(parameter_string)) default = (str, pattern) # Pull from pre-configured types diff --git a/tests/test_routes.py b/tests/test_routes.py index b7228d29..b4ed7cf3 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -71,6 +71,16 @@ def test_route_strict_slash(): request, response = app.test_client.post('/post') assert response.status == 404 +def test_route_invalid_parameter_syntax(): + with pytest.raises(ValueError): + app = Sanic('test_route_invalid_param_syntax') + + @app.get('/get/<:string>', strict_slashes=True) + def handler(request): + return text('OK') + + request, response = app.test_client.get('/get') + def test_route_strict_slash_default_value(): app = Sanic('test_route_strict_slash', strict_slashes=True) From 86f87cf4ace6cd41a192a53ca4b4c47851f30c78 Mon Sep 17 00:00:00 2001 From: Maks Skorokhod Date: Mon, 9 Oct 2017 17:55:35 +0300 Subject: [PATCH 097/104] :wrench: no use f'string' --- sanic/response.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index c05be0fd..582e11cf 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -305,7 +305,8 @@ async def file( headers = headers or {} if filename: headers.setdefault( - 'Content-Disposition', f'attachment; filename="{filename}"') + 'Content-Disposition', + 'attachment; filename="{}"'.format(filename)) filename = filename or path.split(location)[-1] async with open_async(location, mode='rb') as _file: @@ -339,7 +340,8 @@ async def file_stream( headers = headers or {} if filename: headers.setdefault( - 'Content-Disposition', f'attachment; filename="{filename}"') + 'Content-Disposition', + 'attachment; filename="{}"'.format(filename)) filename = filename or path.split(location)[-1] _file = await open_async(location, mode='rb') From c96df8611171e48d4b34f0b7dceabe93d431ef96 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Mon, 9 Oct 2017 07:58:04 -0700 Subject: [PATCH 098/104] make flake8 happy --- sanic/router.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sanic/router.py b/sanic/router.py index c57d808f..21c98766 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -94,7 +94,9 @@ class Router: if ':' in parameter_string: name, pattern = parameter_string.split(':', 1) if not name: - raise ValueError("Invalid parameter syntax: {}".format(parameter_string)) + raise ValueError( + "Invalid parameter syntax: {}".format(parameter_string) + ) default = (str, pattern) # Pull from pre-configured types From 6d2f5da5063ca0285d879d8b8fada42023f9786c Mon Sep 17 00:00:00 2001 From: pcinkh Date: Wed, 11 Oct 2017 14:02:26 +0300 Subject: [PATCH 099/104] Speedup websocket disconnects. --- sanic/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 8f1e0b90..8f70b6e7 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -54,7 +54,7 @@ class Sanic: self.is_running = False self.is_request_stream = False self.websocket_enabled = False - self.websocket_tasks = [] + self.websocket_tasks = set() # Register alternative method names self.go_fast = self.run @@ -259,7 +259,7 @@ class Sanic: # its future is kept in self.websocket_tasks in case it # needs to be cancelled due to the server being stopped fut = ensure_future(handler(request, ws, *args, **kwargs)) - self.websocket_tasks.append(fut) + self.websocket_tasks.add(fut) try: await fut except (CancelledError, ConnectionClosed): From 4578f6016b04a6e850bfe6076f12dd3757164e96 Mon Sep 17 00:00:00 2001 From: lanf0n Date: Fri, 13 Oct 2017 16:48:02 +0800 Subject: [PATCH 100/104] to fix condition error that used in `log_response` `request` class is derived from `dict`, so it will never be `True`. --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index 8f60a864..2d47dc4f 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -311,7 +311,7 @@ class HttpProtocol(asyncio.Protocol): else: extra['byte'] = -1 - if self.request: + if self.request is not None: extra['host'] = '{0}:{1}'.format(self.request.ip[0], self.request.ip[1]) extra['request'] = '{0} {1}'.format(self.request.method, From 477e6b86639a8f57c93dae96ff7df4b9092a1e9b Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Mon, 16 Oct 2017 10:53:45 +1000 Subject: [PATCH 101/104] Add documentation for REQUEST_TIMEOUT, RESPONSE_TIMEOUT and KEEP_ALIVE_TIMEOUT config values. Fixed some inconsistent default values. --- docs/sanic/config.md | 37 ++++++++++++++++++++++++++++++++----- sanic/config.py | 6 ------ sanic/server.py | 9 +++++++-- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/docs/sanic/config.md b/docs/sanic/config.md index 01ee7fc3..5d0dc95a 100644 --- a/docs/sanic/config.md +++ b/docs/sanic/config.md @@ -85,8 +85,35 @@ DB_USER = 'appuser' Out of the box there are just a few predefined values which can be overwritten when creating the application. - | Variable | Default | Description | - | ----------------- | --------- | --------------------------------- | - | REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) | - | REQUEST_TIMEOUT | 60 | How long a request can take (sec) | - | KEEP_ALIVE | True | Disables keep-alive when False | + | Variable | Default | Description | + | ------------------ | --------- | --------------------------------------------- | + | REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) | + | REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) | + | RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) | + | KEEP_ALIVE | True | Disables keep-alive when False | + | KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) | + +### The different Timeout variables: + +A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the `REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates a HTTP 408 response and sends that to the client. Adjust this value higher if your clients routinely pass very large request payloads or upload requests very slowly. + +A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT` value (in seconds), this is considered a Server Error so Sanic generates a HTTP 503 response and sets that to the client. Adjust this value higher if your application is likely to have long-running process that delay the generation of a response. + +### What is Keep Alive? And what does the Keep Alive Timeout value do? + +Keep-Alive is a HTTP feature indroduced in HTTP 1.1. When sending a HTTP request, the client (usually a web browser application) can set a Keep-Alive header to indicate for the http server (Sanic) to not close the TCP connection after it has send the response. This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient network traffic for both the client and the server. + +The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application, set it to `False` to cause all client connections to close immediately after a response is sent, regardless of the Keep-Alive header on the request. + +The amount of time the server holds the TCP connection open is decided by the server itself. In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds, this is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless you know your clients are using a browser which supports TCP connections held open for that long. + +For reference: +``` +Apache httpd server default keepalive timeout = 5 seconds +Nginx server default keepalive timeout = 75 seconds +Nginx performance tuning guidelines uses keepalive = 15 seconds +IE (5-9) client hard keepalive limit = 60 seconds +Firefox client hard keepalive limit = 115 seconds +Opera 11 client hard keepalive limit = 120 seconds +Chrome 13+ client keepalive limit > 300+ seconds +``` diff --git a/sanic/config.py b/sanic/config.py index 741da019..922a9874 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -33,12 +33,6 @@ class Config(dict): self.REQUEST_TIMEOUT = 60 # 60 seconds self.RESPONSE_TIMEOUT = 60 # 60 seconds self.KEEP_ALIVE = keep_alive - # Apache httpd server default keepalive timeout = 5 seconds - # Nginx server default keepalive timeout = 75 seconds - # Nginx performance tuning guidelines uses keepalive = 15 seconds - # IE client hard keepalive limit = 60 seconds - # Firefox client hard keepalive limit = 115 seconds - self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes self.WEBSOCKET_MAX_QUEUE = 32 diff --git a/sanic/server.py b/sanic/server.py index 06c749a5..fbe02ff1 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -76,7 +76,7 @@ class HttpProtocol(asyncio.Protocol): def __init__(self, *, loop, request_handler, error_handler, signal=Signal(), connections=set(), request_timeout=60, - response_timeout=60, keep_alive_timeout=15, + response_timeout=60, keep_alive_timeout=5, request_max_size=None, request_class=None, access_log=True, keep_alive=True, is_request_stream=False, router=None, state=None, debug=False, **kwargs): @@ -192,6 +192,7 @@ class HttpProtocol(asyncio.Protocol): else: logger.info('KeepAlive Timeout. Closing connection.') self.transport.close() + self.transport = None # -------------------------------------------- # # Parsing @@ -353,6 +354,7 @@ class HttpProtocol(asyncio.Protocol): finally: if not keep_alive: self.transport.close() + self.transport = None else: self._keep_alive_timeout_handler = self.loop.call_later( self.keep_alive_timeout, @@ -392,6 +394,7 @@ class HttpProtocol(asyncio.Protocol): finally: if not keep_alive: self.transport.close() + self.transport = None else: self._keep_alive_timeout_handler = self.loop.call_later( self.keep_alive_timeout, @@ -494,7 +497,7 @@ def trigger_events(events, loop): def serve(host, port, request_handler, error_handler, before_start=None, after_start=None, before_stop=None, after_stop=None, debug=False, - request_timeout=60, response_timeout=60, keep_alive_timeout=60, + request_timeout=60, response_timeout=60, keep_alive_timeout=5, ssl=None, sock=None, request_max_size=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, register_sys_signals=True, run_async=False, connections=None, @@ -520,6 +523,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, `app` instance and `loop` :param debug: enables debug output (slows server) :param request_timeout: time in seconds + :param response_timeout: time in seconds + :param keep_alive_timeout: time in seconds :param ssl: SSLContext :param sock: Socket for the server to accept connections from :param request_max_size: size in bytes, `None` for no limit From ea5b07f636451822cbe923a75e6275118f594006 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Mon, 16 Oct 2017 11:05:01 +1000 Subject: [PATCH 102/104] Update websocket protocol to accomodate changes in HTTP protocol from https://github.com/channelcat/sanic/pull/939 Fixes https://github.com/channelcat/sanic/issues/969 --- sanic/websocket.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/sanic/websocket.py b/sanic/websocket.py index e8e9922f..37b13f3c 100644 --- a/sanic/websocket.py +++ b/sanic/websocket.py @@ -13,10 +13,18 @@ class WebSocketProtocol(HttpProtocol): self.websocket_max_size = websocket_max_size self.websocket_max_queue = websocket_max_queue - def connection_timeout(self): - # timeouts make no sense for websocket routes + # timeouts make no sense for websocket routes + def request_timeout_callback(self): if self.websocket is None: - super().connection_timeout() + super().request_timeout_callback() + + def response_timeout_callback(self): + if self.websocket is None: + super().response_timeout_callback() + + def keep_alive_timeout_callback(self): + if self.websocket is None: + super().keep_alive_timeout_callback() def connection_lost(self, exc): if self.websocket is not None: From e6be3b2313eb7b18fd329eada093ed117906afd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kriszti=C3=A1n=20Sz=C5=B1cs?= Date: Tue, 17 Oct 2017 16:05:24 +0200 Subject: [PATCH 103/104] include LICENSE file in manifest --- MANIFEST.in | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 7682c1b6..e52d6670 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,7 @@ include README.rst +include MANIFEST.in +include LICENSE +include setup.py recursive-exclude * __pycache__ recursive-exclude * *.py[co] From 49f3ba39f94c6ef7a6a4cdf0da6459e2cc5b4d3e Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Wed, 18 Oct 2017 17:52:03 +1000 Subject: [PATCH 104/104] Add Sanic-Plugins-Framework library to Extensions doc I made a new tool for devs to use for easily and quickly creating Sanic Plugins (extensions), and for application builders to easily use those plugins in their app. --- docs/sanic/extensions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index ad9b8156..03feb90c 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -1,7 +1,7 @@ # Extensions A list of Sanic extensions created by the community. - +- [Sanic-Plugins-Framework](https://github.com/ashleysommer/sanicpluginsframework): Library for easily creating and using Sanic plugins. - [Sessions](https://github.com/subyraman/sanic_session): Support for sessions. Allows using redis, memcache or an in memory store. - [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.