Compare commits

..

1 Commits

Author SHA1 Message Date
Adam Hopkins
8673021ad4 Release 21.9.3 (#2318)
* Allow non-conforming ErrorHandlers (#2259)

* Allow non-conforming ErrorHandlers

* Rename to legacy lookup

* Updated depnotice

* Bump version

* Fix formatting

* Remove unused import

* Fix error messages

* Add error format commit and merge conflicts

* Make HTTP connections start in IDLE stage, avoiding delays and error messages (#2268)

* Make all new connections start in IDLE stage, and switch to REQUEST stage only once any bytes are received from client. This makes new connections without any request obey keepalive timeout rather than request timeout like they currently do.

* Revert typo

* Remove request timeout endpoint test which is no longer working (still tested by mocking). Fix mock timeout test setup.

Co-authored-by: L. Karkkainen <tronic@users.noreply.github.com>

* Bump version

* Add error format from config replacement objects

* Cleanup mistaken print statement

* Cleanup reversions

* Bump version

Co-authored-by: L. Kärkkäinen <98187+Tronic@users.noreply.github.com>
Co-authored-by: L. Karkkainen <tronic@users.noreply.github.com>
2021-11-21 14:26:30 +02:00
158 changed files with 2142 additions and 5713 deletions

View File

@@ -1,7 +1,5 @@
exclude_patterns:
- "sanic/__main__.py"
- "sanic/application/logo.py"
- "sanic/application/motd.py"
- "sanic/reloader_helpers.py"
- "sanic/simple.py"
- "sanic/utils.py"
@@ -10,6 +8,7 @@ exclude_patterns:
- "docker/"
- "docs/"
- "examples/"
- "hack/"
- "scripts/"
- "tests/"
checks:
@@ -23,6 +22,3 @@ checks:
threshold: 40
complex-logic:
enabled: false
method-complexity:
config:
threshold: 10

View File

@@ -3,9 +3,6 @@ branch = True
source = sanic
omit =
site-packages
sanic/application/logo.py
sanic/application/motd.py
sanic/cli
sanic/__main__.py
sanic/reloader_helpers.py
sanic/simple.py

View File

@@ -2,20 +2,14 @@ name: "CodeQL"
on:
push:
branches:
- main
- "*LTS"
branches: [ main ]
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
branches: [ main ]
schedule:
- cron: '25 16 * * 0'
jobs:
analyze:
if: github.event.pull_request.draft == false
name: Analyze
runs-on: ubuntu-latest

View File

@@ -1,16 +1,19 @@
name: Coverage check
on:
push:
branches:
- main
- "*LTS"
tags:
- "!*" # Do not execute on tags
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
# on:
# push:
# branches:
# - main
# tags:
# - "!*" # Do not execute on tags
# paths:
# - sanic/*
# - tests/*
# pull_request:
# paths:
# - "!*.MD"
on: [push, pull_request]
jobs:
test:
if: github.event.pull_request.draft == false
runs-on: ${{ matrix.os }}
strategy:
matrix:

View File

@@ -3,12 +3,9 @@ on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
bandit:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }}
strategy:
@@ -19,7 +16,6 @@ jobs:
- { python-version: 3.7, tox-env: security}
- { python-version: 3.8, tox-env: security}
- { python-version: 3.9, tox-env: security}
- { python-version: "3.10", tox-env: security}
steps:
- name: Checkout the repository
uses: actions/checkout@v2

View File

@@ -3,12 +3,9 @@ on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
docsLinter:
if: github.event.pull_request.draft == false
name: Lint Documentation
runs-on: ubuntu-latest
strategy:

View File

@@ -3,12 +3,9 @@ on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
linter:
if: github.event.pull_request.draft == false
name: lint
runs-on: ${{ matrix.os }}
strategy:

View File

@@ -1,47 +0,0 @@
name: Python 3.10 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy310:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- {
python-version: "3.10",
tox-env: py310,
ignore-error-flake: "false",
command-timeout: "0",
}
- {
python-version: "3.10",
tox-env: py310-no-ext,
ignore-error-flake: "true",
command-timeout: "600000",
}
steps:
- name: Checkout the Repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Unit Tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }},-vv=''"
experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}"
command-timeout: "${{ matrix.config.command-timeout }}"
test-failure-retry: "3"

View File

@@ -3,16 +3,19 @@ on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
push:
branches:
- main
paths:
- sanic/*
- tests/*
jobs:
testPy37:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
fail-fast: false
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]

View File

@@ -3,16 +3,19 @@ on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
push:
branches:
- main
paths:
- sanic/*
- tests/*
jobs:
testPy38:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
fail-fast: false
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]

View File

@@ -3,16 +3,19 @@ on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
push:
branches:
- main
paths:
- sanic/*
- tests/*
jobs:
testPy39:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
fail-fast: false
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]

View File

@@ -3,12 +3,9 @@ on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
typeChecking:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }}
strategy:
@@ -16,10 +13,9 @@ jobs:
matrix:
os: [ubuntu-latest]
config:
# - { python-version: 3.7, tox-env: type-checking}
- { python-version: 3.7, tox-env: type-checking}
- { python-version: 3.8, tox-env: type-checking}
- { python-version: 3.9, tox-env: type-checking}
- { python-version: "3.10", tox-env: type-checking}
steps:
- name: Checkout the repository
uses: actions/checkout@v2

View File

@@ -3,12 +3,9 @@ on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testsOnWindows:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}
runs-on: windows-latest
strategy:
@@ -18,7 +15,6 @@ jobs:
- { python-version: 3.7, tox-env: py37-no-ext }
- { python-version: 3.8, tox-env: py38-no-ext }
- { python-version: 3.9, tox-env: py39-no-ext }
- { python-version: "3.10", tox-env: py310-no-ext }
- { python-version: pypy-3.7, tox-env: pypy37-no-ext }
steps:

View File

@@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: true
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
python-version: ["3.7", "3.8", "3.9"]
steps:
- name: Checkout repository

View File

@@ -1,12 +1,12 @@
.. note::
CHANGELOG files are maintained in ``./docs/sanic/releases``. To view the full CHANGELOG, please visit https://sanic.readthedocs.io/en/stable/sanic/changelog.html.
From v21.9, CHANGELOG files are maintained in ``./docs/sanic/releases``
Version 21.6.1
--------------
**Bugfixes**
Bugfixes
********
* `#2178 <https://github.com/sanic-org/sanic/pull/2178>`_
Update sanic-routing to allow for better splitting of complex URI templates
@@ -20,7 +20,8 @@ Version 21.6.1
Version 21.6.0
--------------
**Features**
Features
********
* `#2094 <https://github.com/sanic-org/sanic/pull/2094>`_
Add ``response.eof()`` method for closing a stream in a handler
@@ -67,7 +68,8 @@ Version 21.6.0
* `#2170 <https://github.com/sanic-org/sanic/pull/2170>`_
Additional methods for attaching ``HTTPMethodView``
**Bugfixes**
Bugfixes
********
* `#2091 <https://github.com/sanic-org/sanic/pull/2091>`_
Fix ``UserWarning`` in ASGI mode for missing ``__slots__``
@@ -83,7 +85,8 @@ Version 21.6.0
Fix issue where Blueprint exception handlers do not consistently route to proper handler
**Deprecations and Removals**
Deprecations and Removals
*************************
* `#2156 <https://github.com/sanic-org/sanic/pull/2156>`_
Remove config value ``REQUEST_BUFFER_QUEUE_SIZE``
@@ -92,12 +95,14 @@ Version 21.6.0
* `#2172 <https://github.com/sanic-org/sanic/pull/2170>`_
Deprecate StreamingHTTPResponse
**Developer infrastructure**
Developer infrastructure
************************
* `#2149 <https://github.com/sanic-org/sanic/pull/2149>`_
Remove Travis CI in favor of GitHub Actions
**Improved Documentation**
Improved Documentation
**********************
* `#2164 <https://github.com/sanic-org/sanic/pull/2164>`_
Fix typo in documentation
@@ -107,7 +112,8 @@ Version 21.6.0
Version 21.3.2
--------------
**Bugfixes**
Bugfixes
********
* `#2081 <https://github.com/sanic-org/sanic/pull/2081>`_
Disable response timeout on websocket connections
@@ -118,7 +124,8 @@ Version 21.3.2
Version 21.3.1
--------------
**Bugfixes**
Bugfixes
********
* `#2076 <https://github.com/sanic-org/sanic/pull/2076>`_
Static files inside subfolders are not accessible (404)
@@ -128,7 +135,8 @@ Version 21.3.0
`Release Notes <https://sanicframework.org/en/guide/release-notes/v21.3.html>`_
**Features**
Features
********
*
`#1876 <https://github.com/sanic-org/sanic/pull/1876>`_
@@ -181,7 +189,8 @@ Version 21.3.0
`#2063 <https://github.com/sanic-org/sanic/pull/2063>`_
App and connection level context objects
**Bugfixes**
Bugfixes and issues resolved
****************************
* Resolve `#1420 <https://github.com/sanic-org/sanic/pull/1420>`_
``url_for`` where ``strict_slashes`` are on for a path ending in ``/``
@@ -211,7 +220,8 @@ Version 21.3.0
`#2001 <https://github.com/sanic-org/sanic/pull/2001>`_
Raise ValueError when cookie max-age is not an integer
**Deprecations and Removals**
Deprecations and Removals
*************************
*
`#2007 <https://github.com/sanic-org/sanic/pull/2007>`_
@@ -230,7 +240,8 @@ Version 21.3.0
* ``Request.endpoint`` deprecated in favor of ``Request.name``
* handler type name prefixes removed (static, websocket, etc)
**Developer infrastructure**
Developer infrastructure
************************
*
`#1995 <https://github.com/sanic-org/sanic/pull/1995>`_
@@ -248,7 +259,8 @@ Version 21.3.0
`#2049 <https://github.com/sanic-org/sanic/pull/2049>`_
Updated setup.py to use ``find_packages``
**Improved Documentation**
Improved Documentation
**********************
*
`#1218 <https://github.com/sanic-org/sanic/pull/1218>`_
@@ -270,7 +282,8 @@ Version 21.3.0
`#2052 <https://github.com/sanic-org/sanic/pull/2052>`_
Fix some examples and docs
**Miscellaneous**
Miscellaneous
*************
* ``Request.route`` property
* Better websocket subprotocols support
@@ -316,7 +329,8 @@ Version 21.3.0
Version 20.12.3
---------------
**Bugfixes**
Bugfixes
********
*
`#2021 <https://github.com/sanic-org/sanic/pull/2021>`_
@@ -325,7 +339,8 @@ Version 20.12.3
Version 20.12.2
---------------
**Dependencies**
Dependencies
************
*
`#2026 <https://github.com/sanic-org/sanic/pull/2026>`_
@@ -338,7 +353,8 @@ Version 20.12.2
Version 19.12.5
---------------
**Dependencies**
Dependencies
************
*
`#2025 <https://github.com/sanic-org/sanic/pull/2025>`_
@@ -351,7 +367,8 @@ Version 19.12.5
Version 20.12.0
---------------
**Features**
Features
********
*
`#1993 <https://github.com/sanic-org/sanic/pull/1993>`_
@@ -360,7 +377,8 @@ Version 20.12.0
Version 20.12.0
---------------
**Features**
Features
********
*
`#1945 <https://github.com/sanic-org/sanic/pull/1945>`_
@@ -398,19 +416,22 @@ Version 20.12.0
`#1979 <https://github.com/sanic-org/sanic/pull/1979>`_
Add app registry and Sanic class level app retrieval
**Bugfixes**
Bugfixes
********
*
`#1965 <https://github.com/sanic-org/sanic/pull/1965>`_
Fix Chunked Transport-Encoding in ASGI streaming response
**Deprecations and Removals**
Deprecations and Removals
*************************
*
`#1981 <https://github.com/sanic-org/sanic/pull/1981>`_
Cleanup and remove deprecated code
**Developer infrastructure**
Developer infrastructure
************************
*
`#1956 <https://github.com/sanic-org/sanic/pull/1956>`_
@@ -424,7 +445,8 @@ Version 20.12.0
`#1986 <https://github.com/sanic-org/sanic/pull/1986>`_
Update tox requirements
**Improved Documentation**
Improved Documentation
**********************
*
`#1951 <https://github.com/sanic-org/sanic/pull/1951>`_
@@ -442,7 +464,8 @@ Version 20.12.0
Version 20.9.1
---------------
**Bugfixes**
Bugfixes
********
*
`#1954 <https://github.com/sanic-org/sanic/pull/1954>`_
@@ -455,7 +478,8 @@ Version 20.9.1
Version 19.12.3
---------------
**Bugfixes**
Bugfixes
********
*
`#1959 <https://github.com/sanic-org/sanic/pull/1959>`_
@@ -466,7 +490,8 @@ Version 20.9.0
---------------
**Features**
Features
********
*
`#1887 <https://github.com/sanic-org/sanic/pull/1887>`_
@@ -493,19 +518,22 @@ Version 20.9.0
`#1937 <https://github.com/sanic-org/sanic/pull/1937>`_
Added auto, text, and json fallback error handlers (in v21.3, the default will change form html to auto)
**Bugfixes**
Bugfixes
********
*
`#1897 <https://github.com/sanic-org/sanic/pull/1897>`_
Resolves exception from unread bytes in stream
**Deprecations and Removals**
Deprecations and Removals
*************************
*
`#1903 <https://github.com/sanic-org/sanic/pull/1903>`_
config.from_envar, config.from_pyfile, and config.from_object are deprecated and set to be removed in v21.3
**Developer infrastructure**
Developer infrastructure
************************
*
`#1890 <https://github.com/sanic-org/sanic/pull/1890>`_,
@@ -520,7 +548,8 @@ Version 20.9.0
`#1924 <https://github.com/sanic-org/sanic/pull/1924>`_
Adding --strict-markers for pytest
**Improved Documentation**
Improved Documentation
**********************
*
`#1922 <https://github.com/sanic-org/sanic/pull/1922>`_
@@ -530,7 +559,8 @@ Version 20.9.0
Version 20.6.3
---------------
**Bugfixes**
Bugfixes
********
*
`#1884 <https://github.com/sanic-org/sanic/pull/1884>`_
@@ -540,7 +570,8 @@ Version 20.6.3
Version 20.6.2
---------------
**Features**
Features
********
*
`#1641 <https://github.com/sanic-org/sanic/pull/1641>`_
@@ -550,7 +581,8 @@ Version 20.6.2
Version 20.6.1
---------------
**Features**
Features
********
*
`#1760 <https://github.com/sanic-org/sanic/pull/1760>`_
@@ -564,7 +596,8 @@ Version 20.6.1
`#1880 <https://github.com/sanic-org/sanic/pull/1880>`_
Add handler names for websockets for url_for usage
**Bugfixes**
Bugfixes
********
*
`#1776 <https://github.com/sanic-org/sanic/pull/1776>`_
@@ -586,13 +619,15 @@ Version 20.6.1
`#1853 <https://github.com/sanic-org/sanic/pull/1853>`_
Fix pickle error when attempting to pickle an application which contains websocket routes
**Deprecations and Removals**
Deprecations and Removals
*************************
*
`#1739 <https://github.com/sanic-org/sanic/pull/1739>`_
Deprecate body_bytes to merge into body
**Developer infrastructure**
Developer infrastructure
************************
*
`#1852 <https://github.com/sanic-org/sanic/pull/1852>`_
@@ -607,7 +642,8 @@ Version 20.6.1
Wrap run()'s "protocol" type annotation in Optional[]
**Improved Documentation**
Improved Documentation
**********************
*
`#1846 <https://github.com/sanic-org/sanic/pull/1846>`_
@@ -621,13 +657,14 @@ Version 20.6.1
Version 20.6.0
---------------
*Released, but unintentionally omitting PR #1880, so was replaced by 20.6.1*
*Released, but unintentionally ommitting PR #1880, so was replaced by 20.6.1*
Version 20.3.0
---------------
**Features**
Features
********
*
`#1762 <https://github.com/sanic-org/sanic/pull/1762>`_
@@ -658,7 +695,8 @@ Version 20.3.0
`#1820 <https://github.com/sanic-org/sanic/pull/1820>`_
Do not set content-type and content-length headers in exceptions
**Bugfixes**
Bugfixes
********
*
`#1748 <https://github.com/sanic-org/sanic/pull/1748>`_
@@ -676,7 +714,8 @@ Version 20.3.0
`#1808 <https://github.com/sanic-org/sanic/pull/1808>`_
Fix Ctrl+C and tests on Windows
**Deprecations and Removals**
Deprecations and Removals
*************************
*
`#1800 <https://github.com/sanic-org/sanic/pull/1800>`_
@@ -694,7 +733,8 @@ Version 20.3.0
`#1818 <https://github.com/sanic-org/sanic/pull/1818>`_
Complete deprecation of ``app.remove_route`` and ``request.raw_args``
**Dependencies**
Dependencies
************
*
`#1794 <https://github.com/sanic-org/sanic/pull/1794>`_
@@ -704,13 +744,15 @@ Version 20.3.0
`#1806 <https://github.com/sanic-org/sanic/pull/1806>`_
Import ``ASGIDispatch`` from top-level ``httpx`` (from third-party deprecation)
**Developer infrastructure**
Developer infrastructure
************************
*
`#1833 <https://github.com/sanic-org/sanic/pull/1833>`_
Resolve broken documentation builds
**Improved Documentation**
Improved Documentation
**********************
*
`#1755 <https://github.com/sanic-org/sanic/pull/1755>`_
@@ -752,7 +794,8 @@ Version 20.3.0
Version 19.12.0
---------------
**Bugfixes**
Bugfixes
********
- Fix blueprint middleware application
@@ -771,7 +814,8 @@ Version 19.12.0
due to an `AttributeError`. This fix makes the availability of `SERVER_NAME` on our `app.config` an optional behavior. (`#1707 <https://github.com/sanic-org/sanic/issues/1707>`__)
**Improved Documentation**
Improved Documentation
**********************
- Move docs from MD to RST
@@ -785,7 +829,8 @@ Version 19.12.0
Version 19.6.3
--------------
**Features**
Features
********
- Enable Towncrier Support
@@ -793,7 +838,8 @@ Version 19.6.3
of generating and managing change logs as part of each of pull requests. (`#1631 <https://github.com/sanic-org/sanic/issues/1631>`__)
**Improved Documentation**
Improved Documentation
**********************
- Documentation infrastructure changes
@@ -806,7 +852,8 @@ Version 19.6.3
Version 19.6.2
--------------
**Features**
Features
********
*
`#1562 <https://github.com/sanic-org/sanic/pull/1562>`_
@@ -822,7 +869,8 @@ Version 19.6.2
Add Configure support from object string
**Bugfixes**
Bugfixes
********
*
`#1587 <https://github.com/sanic-org/sanic/pull/1587>`_
@@ -840,7 +888,8 @@ Version 19.6.2
`#1594 <https://github.com/sanic-org/sanic/pull/1594>`_
Strict Slashes behavior fix
**Deprecations and Removals**
Deprecations and Removals
*************************
*
`#1544 <https://github.com/sanic-org/sanic/pull/1544>`_
@@ -864,7 +913,8 @@ Version 19.6.2
Version 19.3
------------
**Features**
Features
********
*
`#1497 <https://github.com/sanic-org/sanic/pull/1497>`_
@@ -932,7 +982,8 @@ Version 19.3
This is a breaking change.
**Bugfixes**
Bugfixes
********
*
@@ -968,7 +1019,8 @@ Version 19.3
This allows the access log to be disabled for example when running via
gunicorn.
**Developer infrastructure**
Developer infrastructure
************************
* `#1529 <https://github.com/sanic-org/sanic/pull/1529>`_ Update project PyPI credentials
* `#1515 <https://github.com/sanic-org/sanic/pull/1515>`_ fix linter issue causing travis build failures (fix #1514)
@@ -976,7 +1028,8 @@ Version 19.3
* `#1478 <https://github.com/sanic-org/sanic/pull/1478>`_ Upgrade setuptools version and use native docutils in doc build
* `#1464 <https://github.com/sanic-org/sanic/pull/1464>`_ Upgrade pytest, and fix caplog unit tests
**Improved Documentation**
Improved Documentation
**********************
* `#1516 <https://github.com/sanic-org/sanic/pull/1516>`_ Fix typo at the exception documentation
* `#1510 <https://github.com/sanic-org/sanic/pull/1510>`_ fix typo in Asyncio example
@@ -1037,19 +1090,21 @@ Version 18.12
* Fix Range header handling for static files (#1402)
* Fix the logger and make it work (#1397)
* Fix type pikcle->pickle in multiprocessing test
* Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirement of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
* Fix pickling blueprints Change the string passed in the "name" section of the namedtuples in Blueprint to match the name of the Blueprint module attribute name. This allows blueprints to be pickled and unpickled, without errors, which is a requirment of running Sanic in multiprocessing mode in Windows. Added a test for pickling and unpickling blueprints Added a test for pickling and unpickling sanic itself Added a test for enabling multiprocessing on an app with a blueprint (only useful to catch this bug if the tests are run on Windows).
* Fix document for logging
Version 0.8
-----------
**0.8.3**
0.8.3
*****
* Changes:
* Ownership changed to org 'sanic-org'
**0.8.0**
0.8.0
*****
* Changes:
@@ -1074,7 +1129,7 @@ Version 0.8
* Content-length header on 204/304 responses (Arnulfo Solís)
* Extend WebSocketProtocol arguments and add docs (Bob Olde Hampsink, yunstanford)
* Update development status from pre-alpha to beta (Maksim Anisenkov)
* KeepAlive Timeout log level changed to debug (Arnulfo Solís)
* KeepAlive Timout log level changed to debug (Arnulfo Solís)
* Pin pytest to 3.3.2 because of pytest-dev/pytest#3170 (Maksim Aniskenov)
* Install Python 3.5 and 3.6 on docker container for tests (Shahin Azad)
* Add support for blueprint groups and nesting (Elias Tarhini)
@@ -1129,16 +1184,19 @@ Version 0.1
-----------
**0.1.7**
0.1.7
*****
* Reversed static url and directory arguments to meet spec
**0.1.6**
0.1.6
*****
* Static files
* Lazy Cookie Loading
**0.1.5**
0.1.5
*****
* Cookies
* Blueprint listeners and ordering
@@ -1146,19 +1204,23 @@ Version 0.1
* Fix: Incomplete file reads on medium+ sized post requests
* Breaking: after_start and before_stop now pass sanic as their first argument
**0.1.4**
0.1.4
*****
* Multiprocessing
**0.1.3**
0.1.3
*****
* Blueprint support
* Faster Response processing
**0.1.1 - 0.1.2**
0.1.1 - 0.1.2
*************
* Struggling to update pypi via CI
**0.1.0**
0.1.0
*****
* Released to public

View File

@@ -11,7 +11,7 @@ Sanic | Build fast. Run fast.
:stub-columns: 1
* - Build
- | |Py310Test| |Py39Test| |Py38Test| |Py37Test|
- | |Py39Test| |Py38Test| |Py37Test| |Codecov|
* - Docs
- | |UserGuide| |Documentation|
* - Package
@@ -27,8 +27,8 @@ Sanic | Build fast. Run fast.
:target: https://community.sanicframework.org/
.. |Discord| image:: https://img.shields.io/discord/812221182594121728?logo=discord
:target: https://discord.gg/FARQzAEMAA
.. |Py310Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python310.yml
.. |Codecov| image:: https://codecov.io/gh/sanic-org/sanic/branch/master/graph/badge.svg
:target: https://codecov.io/gh/sanic-org/sanic
.. |Py39Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml/badge.svg?branch=main
:target: https://github.com/sanic-org/sanic/actions/workflows/pr-python39.yml
.. |Py38Test| image:: https://github.com/sanic-org/sanic/actions/workflows/pr-python38.yml/badge.svg?branch=main
@@ -77,11 +77,7 @@ The goal of the project is to provide a simple way to get up and running a highl
Sponsor
-------
Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
Thanks to `Linode <https://www.linode.com>`_ for their contribution towards the development and community of Sanic.
|Linode|
Check out `open collective <https://opencollective.com/sanic-org>`_ to learn more about helping to fund Sanic.
Installation
------------
@@ -166,8 +162,3 @@ Contribution
------------
We are always happy to have new contributions. We have `marked issues good for anyone looking to get started <https://github.com/sanic-org/sanic/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner>`_, and welcome `questions on the forums <https://community.sanicframework.org/>`_. Please take a look at our `Contribution guidelines <https://github.com/sanic-org/sanic/blob/master/CONTRIBUTING.rst>`_.
.. |Linode| image:: https://www.linode.com/wp-content/uploads/2021/01/Linode-Logo-Black.svg
:alt: Linode
:target: https://www.linode.com
:width: 200px

View File

@@ -38,3 +38,10 @@ sanic.views
.. automodule:: sanic.views
:members:
:show-inheritance:
sanic.websocket
---------------
.. automodule:: sanic.websocket
:members:
:show-inheritance:

View File

@@ -1,6 +1,6 @@
📜 Changelog
============
.. mdinclude:: ./releases/21/21.12.md
.. mdinclude:: ./releases/21/21.9.md
.. mdinclude:: ./releases/21.9.md
.. include:: ../../CHANGELOG.rst

View File

@@ -1,14 +1,4 @@
## Version 21.9.3
*Rerelease of v21.9.2 with some cleanup*
## Version 21.9.2
- [#2268](https://github.com/sanic-org/sanic/pull/2268) Make HTTP connections start in IDLE stage, avoiding delays and error messages
- [#2310](https://github.com/sanic-org/sanic/pull/2310) More consistent config setting with post-FALLBACK_ERROR_FORMAT apply
## Version 21.9.1
- [#2259](https://github.com/sanic-org/sanic/pull/2259) Allow non-conforming ErrorHandlers
## Version 21.9.0
## Version 21.9
### Features
- [#2158](https://github.com/sanic-org/sanic/pull/2158), [#2248](https://github.com/sanic-org/sanic/pull/2248) Complete overhaul of I/O to websockets

View File

@@ -1,64 +0,0 @@
## Version 21.12.1
- [#2349](https://github.com/sanic-org/sanic/pull/2349) Only display MOTD on startup
- [#2354](https://github.com/sanic-org/sanic/pull/2354) Ignore name argument in Python 3.7
- [#2355](https://github.com/sanic-org/sanic/pull/2355) Add config.update support for all config values
## Version 21.12.0
### Features
- [#2260](https://github.com/sanic-org/sanic/pull/2260) Allow early Blueprint registrations to still apply later added objects
- [#2262](https://github.com/sanic-org/sanic/pull/2262) Noisy exceptions - force logging of all exceptions
- [#2264](https://github.com/sanic-org/sanic/pull/2264) Optional `uvloop` by configuration
- [#2270](https://github.com/sanic-org/sanic/pull/2270) Vhost support using multiple TLS certificates
- [#2277](https://github.com/sanic-org/sanic/pull/2277) Change signal routing for increased consistency
- *BREAKING CHANGE*: If you were manually routing signals there is a breaking change. The signal router's `get` is no longer 100% determinative. There is now an additional step to loop thru the returned signals for proper matching on the requirements. If signals are being dispatched using `app.dispatch` or `bp.dispatch`, there is no change.
- [#2290](https://github.com/sanic-org/sanic/pull/2290) Add contextual exceptions
- [#2291](https://github.com/sanic-org/sanic/pull/2291) Increase join concat performance
- [#2295](https://github.com/sanic-org/sanic/pull/2295), [#2316](https://github.com/sanic-org/sanic/pull/2316), [#2331](https://github.com/sanic-org/sanic/pull/2331) Restructure of CLI and application state with new displays and more command parity with `app.run`
- [#2302](https://github.com/sanic-org/sanic/pull/2302) Add route context at definition time
- [#2304](https://github.com/sanic-org/sanic/pull/2304) Named tasks and new API for managing background tasks
- [#2307](https://github.com/sanic-org/sanic/pull/2307) On app auto-reload, provide insight of changed files
- [#2308](https://github.com/sanic-org/sanic/pull/2308) Auto extend application with [Sanic Extensions](https://sanicframework.org/en/plugins/sanic-ext/getting-started.html) if it is installed, and provide first class support for accessing the extensions
- [#2309](https://github.com/sanic-org/sanic/pull/2309) Builtin signals changed to `Enum`
- [#2313](https://github.com/sanic-org/sanic/pull/2313) Support additional config implementation use case
- [#2321](https://github.com/sanic-org/sanic/pull/2321) Refactor environment variable hydration logic
- [#2327](https://github.com/sanic-org/sanic/pull/2327) Prevent sending multiple or mixed responses on a single request
- [#2330](https://github.com/sanic-org/sanic/pull/2330) Custom type casting on environment variables
- [#2332](https://github.com/sanic-org/sanic/pull/2332) Make all deprecation notices consistent
- [#2335](https://github.com/sanic-org/sanic/pull/2335) Allow underscore to start instance names
### Bugfixes
- [#2273](https://github.com/sanic-org/sanic/pull/2273) Replace assignation by typing for `websocket_handshake`
- [#2285](https://github.com/sanic-org/sanic/pull/2285) Fix IPv6 display in startup logs
- [#2299](https://github.com/sanic-org/sanic/pull/2299) Dispatch `http.lifecyle.response` from exception handler
### Deprecations and Removals
- [#2306](https://github.com/sanic-org/sanic/pull/2306) Removal of deprecated items
- `Sanic` and `Blueprint` may no longer have arbitrary properties attached to them
- `Sanic` and `Blueprint` forced to have compliant names
- alphanumeric + `_` + `-`
- must start with letter or `_`
- `load_env` keyword argument of `Sanic`
- `sanic.exceptions.abort`
- `sanic.views.CompositionView`
- `sanic.response.StreamingHTTPResponse`
- *NOTE:* the `stream()` response method (where you pass a callable streaming function) has been deprecated and will be removed in v22.6. You should upgrade all streaming responses to the new style: https://sanicframework.org/en/guide/advanced/streaming.html#response-streaming
- [#2320](https://github.com/sanic-org/sanic/pull/2320) Remove app instance from Config for error handler setting
### Developer infrastructure
- [#2251](https://github.com/sanic-org/sanic/pull/2251) Change dev install command
- [#2286](https://github.com/sanic-org/sanic/pull/2286) Change codeclimate complexity threshold from 5 to 10
- [#2287](https://github.com/sanic-org/sanic/pull/2287) Update host test function names so they are not overwritten
- [#2292](https://github.com/sanic-org/sanic/pull/2292) Fail CI on error
- [#2311](https://github.com/sanic-org/sanic/pull/2311), [#2324](https://github.com/sanic-org/sanic/pull/2324) Do not run tests for draft PRs
- [#2336](https://github.com/sanic-org/sanic/pull/2336) Remove paths from coverage checks
- [#2338](https://github.com/sanic-org/sanic/pull/2338) Cleanup ports on tests
### Improved Documentation
- [#2269](https://github.com/sanic-org/sanic/pull/2269), [#2329](https://github.com/sanic-org/sanic/pull/2329), [#2333](https://github.com/sanic-org/sanic/pull/2333) Cleanup typos and fix language
### Miscellaneous
- [#2257](https://github.com/sanic-org/sanic/pull/2257), [#2294](https://github.com/sanic-org/sanic/pull/2294), [#2341](https://github.com/sanic-org/sanic/pull/2341) Add Python 3.10 support
- [#2279](https://github.com/sanic-org/sanic/pull/2279), [#2317](https://github.com/sanic-org/sanic/pull/2317), [#2322](https://github.com/sanic-org/sanic/pull/2322) Add/correct missing type annotations
- [#2305](https://github.com/sanic-org/sanic/pull/2305) Fix examples to use modern implementations

View File

@@ -4,14 +4,12 @@ import asyncio
from sanic import Sanic
app = Sanic("Example")
app = Sanic()
async def notify_server_started_after_five_seconds():
await asyncio.sleep(5)
print("Server successfully started!")
print('Server successfully started!')
app.add_task(notify_server_started_after_five_seconds())

View File

@@ -1,29 +1,30 @@
from random import randint
from sanic import Sanic
from sanic.response import text
from random import randint
app = Sanic()
app = Sanic("Example")
@app.middleware("request")
@app.middleware('request')
def append_request(request):
request.ctx.num = randint(0, 100)
# Add new key with random value
request['num'] = randint(0, 100)
@app.get("/pop")
@app.get('/pop')
def pop_handler(request):
return text(request.ctx.num)
# Pop key from request object
num = request.pop('num')
return text(num)
@app.get("/key_exist")
@app.get('/key_exist')
def key_exist_handler(request):
# Check the key is exist or not
if hasattr(request.ctx, "num"):
return text("num exist in request")
if 'num' in request:
return text('num exist in request')
return text("num does not exist in request")
return text('num does not exist in reqeust')
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,12 +1,10 @@
# -*- coding: utf-8 -*-
from functools import wraps
from sanic import Sanic
from functools import wraps
from sanic.response import json
app = Sanic("Example")
app = Sanic()
def check_request_for_authorization_status(request):
@@ -29,16 +27,14 @@ def authorized(f):
return response
else:
# the user is not authorized.
return json({"status": "not_authorized"}, 403)
return json({'status': 'not_authorized'}, 403)
return decorated_function
@app.route("/")
@authorized
async def test(request):
return json({"status": "authorized"})
return json({'status': 'authorized'})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,53 +1,43 @@
from sanic import Blueprint, Sanic
from sanic import Sanic, Blueprint
from sanic.response import text
"""
Demonstrates that blueprint request middleware are executed in the order they
'''
Demonstrates that blueprint request middleware are executed in the order they
are added. And blueprint response middleware are executed in _reverse_ order.
On a valid request, it should print "1 2 3 6 5 4" to terminal
"""
'''
app = Sanic("Example")
app = Sanic(__name__)
bp = Blueprint("bp_example")
bp = Blueprint("bp_"+__name__)
@bp.on_request
@bp.middleware('request')
def request_middleware_1(request):
print("1")
print('1')
@bp.on_request
@bp.middleware('request')
def request_middleware_2(request):
print("2")
print('2')
@bp.on_request
@bp.middleware('request')
def request_middleware_3(request):
print("3")
print('3')
@bp.on_response
@bp.middleware('response')
def resp_middleware_4(request, response):
print("4")
print('4')
@bp.on_response
@bp.middleware('response')
def resp_middleware_5(request, response):
print("5")
print('5')
@bp.on_response
@bp.middleware('response')
def resp_middleware_6(request, response):
print("6")
print('6')
@bp.route("/")
@bp.route('/')
def pop_handler(request):
return text("hello world")
return text('hello world')
app.blueprint(bp, url_prefix="/bp")
app.blueprint(bp, url_prefix='/bp')
app.run(host="0.0.0.0", port=8000, debug=True, auto_reload=False)

View File

@@ -1,11 +1,10 @@
from sanic import Blueprint, Sanic
from sanic.response import file, json
app = Sanic("Example")
blueprint = Blueprint("bp_example", url_prefix="/my_blueprint")
blueprint2 = Blueprint("bp_example2", url_prefix="/my_blueprint2")
blueprint3 = Blueprint("bp_example3", url_prefix="/my_blueprint3")
app = Sanic(__name__)
blueprint = Blueprint("name", url_prefix="/my_blueprint")
blueprint2 = Blueprint("name2", url_prefix="/my_blueprint2")
blueprint3 = Blueprint("name3", url_prefix="/my_blueprint3")
@blueprint.route("/foo")

View File

@@ -2,20 +2,17 @@ from asyncio import sleep
from sanic import Sanic, response
app = Sanic("DelayedResponseApp", strict_slashes=True)
app = Sanic(__name__, strict_slashes=True)
@app.get("/")
async def handler(request):
return response.redirect("/sleep/3")
@app.get("/sleep/<t:number>")
async def handler2(request, t=0.3):
await sleep(t)
return response.text(f"Slept {t:.1f} seconds.\n")
if __name__ == "__main__":
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@@ -7,10 +7,8 @@ and pass in an instance of it when we create our Sanic instance. Inside this
class' default handler, we can do anything including sending exceptions to
an external service.
"""
from sanic.exceptions import SanicException
from sanic.handlers import ErrorHandler
from sanic.exceptions import SanicException
"""
Imports and code relevant for our CustomHandler class
(Ordinarily this would be in a separate file)
@@ -18,6 +16,7 @@ Imports and code relevant for our CustomHandler class
class CustomHandler(ErrorHandler):
def default(self, request, exception):
# Here, we have access to the exception object
# and can do anything with it (log, send to external service, etc)
@@ -39,17 +38,17 @@ server's error_handler to an instance of our CustomHandler
from sanic import Sanic
app = Sanic(__name__)
handler = CustomHandler()
app = Sanic("Example", error_handler=handler)
app.error_handler = handler
@app.route("/")
async def test(request):
# Here, something occurs which causes an unexpected exception
# This exception will flow to our custom handler.
raise SanicException("You Broke It!")
raise SanicException('You Broke It!')
if __name__ == "__main__":
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,6 +1,4 @@
from sanic import Sanic, response, text
from sanic.handlers import ErrorHandler
from sanic.server.async_server import AsyncioServer
HTTP_PORT = 9999
@@ -34,40 +32,20 @@ def proxy(request, path):
return response.redirect(url)
@https.main_process_start
@https.listener("main_process_start")
async def start(app, _):
http_server = await http.create_server(
global http
app.http_server = await http.create_server(
port=HTTP_PORT, return_asyncio_server=True
)
app.add_task(runner(http, http_server))
app.ctx.http_server = http_server
app.ctx.http = http
app.http_server.after_start()
@https.main_process_stop
@https.listener("main_process_stop")
async def stop(app, _):
await app.ctx.http_server.before_stop()
await app.ctx.http_server.close()
for connection in app.ctx.http_server.connections:
connection.close_if_idle()
await app.ctx.http_server.after_stop()
app.ctx.http = False
async def runner(app: Sanic, app_server: AsyncioServer):
app.is_running = True
try:
app.signalize()
app.finalize()
ErrorHandler.finalize(app.error_handler)
app_server.init = True
await app_server.before_start()
await app_server.after_start()
await app_server.serve_forever()
finally:
app.is_running = False
app.is_stopping = True
app.http_server.before_stop()
await app.http_server.close()
app.http_server.after_stop()
https.run(port=HTTPS_PORT, debug=True)

View File

@@ -1,30 +1,26 @@
import asyncio
import httpx
from sanic import Sanic
from sanic.response import json
import asyncio
import aiohttp
app = Sanic("Example")
app = Sanic(__name__)
sem = None
@app.before_server_start
def init(sanic, _):
@app.listener('before_server_start')
def init(sanic, loop):
global sem
concurrency_per_worker = 4
sem = asyncio.Semaphore(concurrency_per_worker)
sem = asyncio.Semaphore(concurrency_per_worker, loop=loop)
async def bounded_fetch(session, url):
"""
Use session object to perform 'get' request on url
"""
async with sem:
response = await session.get(url)
return response.json()
async with sem, session.get(url) as response:
return await response.json()
@app.route("/")
@@ -32,9 +28,9 @@ async def test(request):
"""
Download and serve example JSON
"""
url = "https://api.github.com/repos/sanic-org/sanic"
url = "https://api.github.com/repos/channelcat/sanic"
async with httpx.AsyncClient() as session:
async with aiohttp.ClientSession() as session:
response = await bounded_fetch(session, url)
return json(response)

View File

@@ -1,6 +1,6 @@
import logging
from contextvars import ContextVar
import aiotask_context as context
from sanic import Sanic, response
@@ -11,8 +11,8 @@ log = logging.getLogger(__name__)
class RequestIdFilter(logging.Filter):
def filter(self, record):
try:
record.request_id = app.ctx.request_id.get(None) or "n/a"
except AttributeError:
record.request_id = context.get("X-Request-ID")
except ValueError:
record.request_id = "n/a"
return True
@@ -44,12 +44,13 @@ LOG_SETTINGS = {
}
app = Sanic("Example", log_config=LOG_SETTINGS)
app = Sanic(__name__, log_config=LOG_SETTINGS)
@app.on_request
async def set_request_id(request):
request.app.ctx.request_id.set(request.id)
request_id = request.id
context.set("X-Request-ID", request_id)
log.info(f"Setting {request.id=}")
@@ -60,14 +61,14 @@ async def set_request_header(request, response):
@app.route("/")
async def test(request):
log.debug("X-Request-ID: %s", request.id)
log.debug("X-Request-ID: %s", context.get("X-Request-ID"))
log.info("Hello from test!")
return response.json({"test": True})
@app.before_server_start
def setup(app, loop):
app.ctx.request_id = ContextVar("request_id")
loop.set_task_factory(context.task_factory)
if __name__ == "__main__":

View File

@@ -1,6 +1,5 @@
import logging
import socket
from os import getenv
from platform import node
from uuid import getnode as get_mac
@@ -8,11 +7,10 @@ from uuid import getnode as get_mac
from logdna import LogDNAHandler
from sanic import Sanic
from sanic.request import Request
from sanic.response import json
from sanic.request import Request
log = logging.getLogger("logdna")
log = logging.getLogger('logdna')
log.setLevel(logging.INFO)
@@ -32,18 +30,16 @@ logdna_options = {
"index_meta": True,
"hostname": node(),
"ip": get_my_ip_address(),
"mac": get_mac_address(),
"mac": get_mac_address()
}
logdna_handler = LogDNAHandler(
getenv("LOGDNA_API_KEY"), options=logdna_options
)
logdna_handler = LogDNAHandler(getenv("LOGDNA_API_KEY"), options=logdna_options)
logdna = logging.getLogger(__name__)
logdna.setLevel(logging.INFO)
logdna.addHandler(logdna_handler)
app = Sanic("Example")
app = Sanic(__name__)
@app.middleware
@@ -53,8 +49,13 @@ def log_request(request: Request):
@app.route("/")
def default(request):
return json({"response": "I was here"})
return json({
"response": "I was here"
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@@ -2,29 +2,27 @@
Modify header or status in response
"""
from sanic import Sanic, response
from sanic import Sanic
from sanic import response
app = Sanic(__name__)
app = Sanic("Example")
@app.route("/")
@app.route('/')
def handle_request(request):
return response.json(
{"message": "Hello world!"},
headers={"X-Served-By": "sanic"},
status=200,
{'message': 'Hello world!'},
headers={'X-Served-By': 'sanic'},
status=200
)
@app.route("/unauthorized")
@app.route('/unauthorized')
def handle_request(request):
return response.json(
{"message": "You are not authorized"},
headers={"X-Served-By": "sanic"},
status=404,
{'message': 'You are not authorized'},
headers={'X-Served-By': 'sanic'},
status=404
)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=True)
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -32,7 +32,7 @@ def test_port(worker_id):
@pytest.fixture(scope="session")
def app():
app = Sanic("Example")
app = Sanic()
@app.route("/")
async def index(request):

View File

@@ -8,6 +8,7 @@ from sanic.handlers import ErrorHandler
class RaygunExceptionReporter(ErrorHandler):
def __init__(self, raygun_api_key=None):
super().__init__()
if raygun_api_key is None:
@@ -21,13 +22,16 @@ class RaygunExceptionReporter(ErrorHandler):
raygun_error_reporter = RaygunExceptionReporter()
app = Sanic("Example", error_handler=raygun_error_reporter)
app = Sanic(__name__, error_handler=raygun_error_reporter)
@app.route("/raise")
async def test(request):
raise SanicException("You Broke It!")
raise SanicException('You Broke It!')
if __name__ == "__main__":
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
if __name__ == '__main__':
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@@ -1,18 +1,18 @@
from sanic import Sanic, response
from sanic import Sanic
from sanic import response
app = Sanic(__name__)
app = Sanic("Example")
@app.route("/")
@app.route('/')
def handle_request(request):
return response.redirect("/redirect")
return response.redirect('/redirect')
@app.route("/redirect")
@app.route('/redirect')
async def test(request):
return response.json({"Redirected": True})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,63 +1,65 @@
from sanic import Sanic
from sanic.blueprints import Blueprint
from sanic.response import stream, text
from sanic.views import CompositionView
from sanic.views import HTTPMethodView
from sanic.views import stream as stream_decorator
from sanic.blueprints import Blueprint
from sanic.response import stream, text
bp = Blueprint("bp_example")
app = Sanic("Example")
bp = Blueprint('blueprint_request_stream')
app = Sanic('request_stream')
class SimpleView(HTTPMethodView):
@stream_decorator
async def post(self, request):
result = ""
result = ''
while True:
body = await request.stream.get()
if body is None:
break
result += body.decode("utf-8")
result += body.decode('utf-8')
return text(result)
@app.post("/stream", stream=True)
@app.post('/stream', stream=True)
async def handler(request):
async def streaming(response):
while True:
body = await request.stream.get()
if body is None:
break
body = body.decode("utf-8").replace("1", "A")
body = body.decode('utf-8').replace('1', 'A')
await response.write(body)
return stream(streaming)
@bp.put("/bp_stream", stream=True)
@bp.put('/bp_stream', stream=True)
async def bp_handler(request):
result = ""
result = ''
while True:
body = await request.stream.get()
if body is None:
break
result += body.decode("utf-8").replace("1", "A")
result += body.decode('utf-8').replace('1', 'A')
return text(result)
async def post_handler(request):
result = ""
result = ''
while True:
body = await request.stream.get()
if body is None:
break
result += body.decode("utf-8")
result += body.decode('utf-8')
return text(result)
app.blueprint(bp)
app.add_route(SimpleView.as_view(), "/method_view")
app.add_route(SimpleView.as_view(), '/method_view')
view = CompositionView()
view.add(['POST'], post_handler, stream=True)
app.add_route(view, '/composition_view')
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000)

View File

@@ -1,23 +1,21 @@
import asyncio
from sanic import Sanic, response
from sanic import Sanic
from sanic import response
from sanic.config import Config
from sanic.exceptions import RequestTimeout
Config.REQUEST_TIMEOUT = 1
app = Sanic("Example")
app = Sanic(__name__)
@app.route("/")
@app.route('/')
async def test(request):
await asyncio.sleep(3)
return response.text("Hello, world!")
return response.text('Hello, world!')
@app.exception(RequestTimeout)
def timeout(request, exception):
return response.text("RequestTimeout from error_handler.", 408)
return response.text('RequestTimeout from error_handler.', 408)
app.run(host="0.0.0.0", port=8000)
app.run(host='0.0.0.0', port=8000)

View File

@@ -1,22 +1,21 @@
from os import getenv
import rollbar
from sanic.handlers import ErrorHandler
from sanic import Sanic
from sanic.exceptions import SanicException
from sanic.handlers import ErrorHandler
from os import getenv
rollbar.init(getenv("ROLLBAR_API_KEY"))
class RollbarExceptionHandler(ErrorHandler):
def default(self, request, exception):
rollbar.report_message(str(exception))
return super().default(request, exception)
app = Sanic("Example", error_handler=RollbarExceptionHandler())
app = Sanic(__name__, error_handler=RollbarExceptionHandler())
@app.route("/raise")
@@ -25,4 +24,7 @@ def create_error(request):
if __name__ == "__main__":
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@@ -11,7 +11,7 @@ from pathlib import Path
from sanic import Sanic, response
app = Sanic("Example")
app = Sanic(__name__)
@app.route("/text")
@@ -59,31 +59,31 @@ async def handler_stream(request):
return response.stream(body)
@app.before_server_start
@app.listener("before_server_start")
async def listener_before_server_start(*args, **kwargs):
print("before_server_start")
@app.after_server_start
@app.listener("after_server_start")
async def listener_after_server_start(*args, **kwargs):
print("after_server_start")
@app.before_server_stop
@app.listener("before_server_stop")
async def listener_before_server_stop(*args, **kwargs):
print("before_server_stop")
@app.after_server_stop
@app.listener("after_server_stop")
async def listener_after_server_stop(*args, **kwargs):
print("after_server_stop")
@app.on_request
@app.middleware("request")
async def print_on_request(request):
print("print_on_request")
@app.on_response
@app.middleware("response")
async def print_on_response(request, response):
print("print_on_response")

View File

@@ -1,30 +1,22 @@
from sanic import Sanic
from sanic import response
from signal import signal, SIGINT
import asyncio
import uvloop
from sanic import Sanic, response
app = Sanic("Example")
app = Sanic(__name__)
@app.route("/")
async def test(request):
return response.json({"answer": "42"})
async def main():
server = await app.create_server(
port=8000, host="0.0.0.0", return_asyncio_server=True
)
if server is None:
return
await server.startup()
await server.serve_forever()
if __name__ == "__main__":
asyncio.set_event_loop(uvloop.new_event_loop())
asyncio.run(main())
asyncio.set_event_loop(uvloop.new_event_loop())
server = app.create_server(host="0.0.0.0", port=8000, return_asyncio_server=True)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(server)
signal(SIGINT, lambda s, f: loop.stop())
try:
loop.run_forever()
except:
loop.stop()

View File

@@ -8,27 +8,12 @@ from sanic import Sanic, response
from sanic.server import AsyncioServer
app = Sanic("Example")
app = Sanic(__name__)
@app.before_server_start
async def before_server_start(app, loop):
print("Async Server starting")
@app.after_server_start
async def after_server_start(app, loop):
print("Async Server started")
@app.before_server_stop
async def before_server_stop(app, loop):
print("Async Server stopping")
@app.after_server_stop
async def after_server_stop(app, loop):
print("Async Server stopped")
@app.listener("after_server_start")
async def after_start_test(app, loop):
print("Async Server Started!")
@app.route("/")
@@ -43,20 +28,20 @@ serv_coro = app.create_server(
loop = asyncio.get_event_loop()
serv_task = asyncio.ensure_future(serv_coro, loop=loop)
signal(SIGINT, lambda s, f: loop.stop())
server: AsyncioServer = loop.run_until_complete(serv_task)
loop.run_until_complete(server.startup())
server: AsyncioServer = loop.run_until_complete(serv_task) # type: ignore
server.startup()
# When using app.run(), this actually triggers before the serv_coro.
# But, in this example, we are using the convenience method, even if it is
# out of order.
loop.run_until_complete(server.before_start())
loop.run_until_complete(server.after_start())
server.before_start()
server.after_start()
try:
loop.run_forever()
except KeyboardInterrupt:
loop.stop()
finally:
loop.run_until_complete(server.before_stop())
server.before_stop()
# Wait for server to close
close_task = server.close()
@@ -65,4 +50,4 @@ finally:
# Complete all tasks on the loop
for connection in server.connections:
connection.close_if_idle()
loop.run_until_complete(server.after_stop())
server.after_stop()

View File

@@ -6,19 +6,20 @@ from sentry_sdk.integrations.sanic import SanicIntegration
from sanic import Sanic
from sanic.response import json
sentry_init(
dsn=getenv("SENTRY_DSN"),
integrations=[SanicIntegration()],
)
app = Sanic("Example")
app = Sanic(__name__)
# noinspection PyUnusedLocal
@app.route("/working")
async def working_path(request):
return json({"response": "Working API Response"})
return json({
"response": "Working API Response"
})
# noinspection PyUnusedLocal
@@ -27,5 +28,8 @@ async def raise_error(request):
raise Exception("Testing Sentry Integration")
if __name__ == "__main__":
app.run(host="0.0.0.0", port=getenv("PORT", 8080))
if __name__ == '__main__':
app.run(
host="0.0.0.0",
port=getenv("PORT", 8080)
)

View File

@@ -1,41 +1,42 @@
from sanic import Sanic
from sanic.response import text
from sanic.views import HTTPMethodView
from sanic.response import text
app = Sanic("some_name")
app = Sanic('some_name')
class SimpleView(HTTPMethodView):
def get(self, request):
return text("I am get method")
return text('I am get method')
def post(self, request):
return text("I am post method")
return text('I am post method')
def put(self, request):
return text("I am put method")
return text('I am put method')
def patch(self, request):
return text("I am patch method")
return text('I am patch method')
def delete(self, request):
return text("I am delete method")
return text('I am delete method')
class SimpleAsyncView(HTTPMethodView):
async def get(self, request):
return text("I am async get method")
return text('I am async get method')
async def post(self, request):
return text("I am async post method")
return text('I am async post method')
async def put(self, request):
return text("I am async put method")
return text('I am async put method')
app.add_route(SimpleView.as_view(), "/")
app.add_route(SimpleAsyncView.as_view(), "/async")
app.add_route(SimpleView.as_view(), '/')
app.add_route(SimpleAsyncView.as_view(), '/async')
if __name__ == "__main__":
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,7 +1,7 @@
from sanic import Sanic, response
from sanic import Sanic
from sanic import response
app = Sanic("Example")
app = Sanic(__name__)
@app.route("/")
@@ -9,5 +9,5 @@ async def test(request):
return response.json({"test": True})
if __name__ == "__main__":
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,6 +1,6 @@
from sanic import Sanic
app = Sanic("Example")
app = Sanic(__name__)
app.static("/", "./static")

View File

@@ -1,14 +1,13 @@
from sanic import Sanic
from sanic import response as res
app = Sanic("Example")
app = Sanic(__name__)
@app.route("/")
async def test(req):
return res.text("I'm a teapot", status=418)
return res.text("I\'m a teapot", status=418)
if __name__ == "__main__":
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@@ -1,11 +1,11 @@
import os
from sanic import Sanic, response
from sanic.exceptions import ServerError
from sanic import Sanic
from sanic.log import logger as log
from sanic import response
from sanic.exceptions import ServerError
app = Sanic("Example")
app = Sanic(__name__)
@app.route("/")
@@ -13,7 +13,7 @@ async def test_async(request):
return response.json({"test": True})
@app.route("/sync", methods=["GET", "POST"])
@app.route("/sync", methods=['GET', 'POST'])
def test_sync(request):
return response.json({"test": True})
@@ -31,7 +31,6 @@ def exception(request):
@app.route("/await")
async def test_await(request):
import asyncio
await asyncio.sleep(5)
return response.text("I'm feeling sleepy")
@@ -43,10 +42,8 @@ async def test_file(request):
@app.route("/file_stream")
async def test_file_stream(request):
return await response.file_stream(
os.path.abspath("setup.py"), chunk_size=1024
)
return await response.file_stream(os.path.abspath("setup.py"),
chunk_size=1024)
# ----------------------------------------------- #
# Exceptions
@@ -55,17 +52,14 @@ async def test_file_stream(request):
@app.exception(ServerError)
async def test(request, exception):
return response.json(
{"exception": str(exception), "status": exception.status_code},
status=exception.status_code,
)
return response.json({"exception": "{}".format(exception), "status": exception.status_code},
status=exception.status_code)
# ----------------------------------------------- #
# Read from request
# ----------------------------------------------- #
@app.route("/json")
def post_json(request):
return response.json({"received": True, "message": request.json})
@@ -73,51 +67,38 @@ def post_json(request):
@app.route("/form")
def post_form_json(request):
return response.json(
{
"received": True,
"form_data": request.form,
"test": request.form.get("test"),
}
)
return response.json({"received": True, "form_data": request.form, "test": request.form.get('test')})
@app.route("/query_string")
def query_string(request):
return response.json(
{
"parsed": True,
"args": request.args,
"url": request.url,
"query_string": request.query_string,
}
)
return response.json({"parsed": True, "args": request.args, "url": request.url,
"query_string": request.query_string})
# ----------------------------------------------- #
# Run Server
# ----------------------------------------------- #
@app.before_server_start
@app.listener('before_server_start')
def before_start(app, loop):
log.info("SERVER STARTING")
@app.after_server_start
@app.listener('after_server_start')
def after_start(app, loop):
log.info("OH OH OH OH OHHHHHHHH")
@app.before_server_stop
@app.listener('before_server_stop')
def before_stop(app, loop):
log.info("SERVER STOPPING")
@app.after_server_stop
@app.listener('after_server_stop')
def after_stop(app, loop):
log.info("TRIED EVERYTHING")
if __name__ == "__main__":
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -1,25 +1,23 @@
import os
from sanic import Sanic
from sanic import response
import socket
import os
from sanic import Sanic, response
app = Sanic("Example")
app = Sanic(__name__)
@app.route("/test")
async def test(request):
return response.text("OK")
if __name__ == "__main__":
server_address = "./uds_socket"
if __name__ == '__main__':
server_address = './uds_socket'
# Make sure the socket does not already exist
try:
os.unlink(server_address)
os.unlink(server_address)
except OSError:
if os.path.exists(server_address):
raise
if os.path.exists(server_address):
raise
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.bind(server_address)
app.run(sock=sock)

View File

@@ -1,21 +1,20 @@
from sanic import Sanic, response
from sanic import Sanic
from sanic import response
app = Sanic(__name__)
app = Sanic("Example")
@app.route("/")
@app.route('/')
async def index(request):
# generate a URL for the endpoint `post_handler`
url = app.url_for("post_handler", post_id=5)
url = app.url_for('post_handler', post_id=5)
# the URL is `/posts/5`, redirect to it
return response.redirect(url)
@app.route("/posts/<post_id>")
@app.route('/posts/<post_id>')
async def post_handler(request, post_id):
return response.text("Post - {}".format(post_id))
if __name__ == "__main__":
return response.text('Post - {}'.format(post_id))
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True)

View File

@@ -8,9 +8,7 @@ app = Sanic(name="blue-print-group-version-example")
bp1 = Blueprint(name="ultron", url_prefix="/ultron")
bp2 = Blueprint(name="vision", url_prefix="/vision", strict_slashes=None)
bpg = Blueprint.group(
bp1, bp2, url_prefix="/sentient/robot", version=1, strict_slashes=True
)
bpg = Blueprint.group([bp1, bp2], url_prefix="/sentient/robot", version=1, strict_slashes=True)
@bp1.get("/name")
@@ -33,5 +31,5 @@ async def bp2_revised_name(request):
app.blueprint(bpg)
if __name__ == "__main__":
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)

View File

@@ -8,7 +8,7 @@ from sanic.blueprints import Blueprint
# curl -H "Host: bp.example.com" localhost:8000/question
# curl -H "Host: bp.example.com" localhost:8000/answer
app = Sanic("Example")
app = Sanic(__name__)
bp = Blueprint("bp", host="bp.example.com")

View File

@@ -1,27 +1,25 @@
from sanic import Sanic
from sanic.response import redirect
app = Sanic("Example")
app = Sanic(__name__)
app.static("index.html", "websocket.html")
app.static('index.html', "websocket.html")
@app.route("/")
@app.route('/')
def index(request):
return redirect("index.html")
@app.websocket("/feed")
@app.websocket('/feed')
async def feed(request, ws):
while True:
data = "hello!"
print("Sending: " + data)
data = 'hello!'
print('Sending: ' + data)
await ws.send(data)
data = await ws.recv()
print("Received: " + data)
print('Received: ' + data)
if __name__ == "__main__":
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000, debug=True)

6
hack/Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM catthehacker/ubuntu:act-latest
SHELL [ "/bin/bash", "-c" ]
ENTRYPOINT []
RUN apt-get update
RUN apt-get install gcc -y
RUN apt-get install -y --no-install-recommends g++

View File

@@ -1,15 +1,196 @@
from sanic.cli.app import SanicCLI
from sanic.compat import OS_IS_WINDOWS, enable_windows_color_support
import os
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from importlib import import_module
from pathlib import Path
from typing import Any, Dict, Optional
from sanic_routing import __version__ as __routing_version__ # type: ignore
from sanic import __version__
from sanic.app import Sanic
from sanic.config import BASE_LOGO
from sanic.log import error_logger
from sanic.simple import create_simple_server
if OS_IS_WINDOWS:
enable_windows_color_support()
class SanicArgumentParser(ArgumentParser):
def add_bool_arguments(self, *args, **kwargs):
group = self.add_mutually_exclusive_group()
group.add_argument(*args, action="store_true", **kwargs)
kwargs["help"] = f"no {kwargs['help']}\n "
group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
)
def main():
cli = SanicCLI()
cli.attach()
cli.run()
parser = SanicArgumentParser(
prog="sanic",
description=BASE_LOGO,
formatter_class=lambda prog: RawTextHelpFormatter(
prog, max_help_position=33
),
)
parser.add_argument(
"-v",
"--version",
action="version",
version=f"Sanic {__version__}; Routing {__routing_version__}",
)
parser.add_argument(
"--factory",
action="store_true",
help=(
"Treat app as an application factory, "
"i.e. a () -> <Sanic app> callable"
),
)
parser.add_argument(
"-s",
"--simple",
dest="simple",
action="store_true",
help="Run Sanic as a Simple Server (module arg should be a path)\n ",
)
parser.add_argument(
"-H",
"--host",
dest="host",
type=str,
default="127.0.0.1",
help="Host address [default 127.0.0.1]",
)
parser.add_argument(
"-p",
"--port",
dest="port",
type=int,
default=8000,
help="Port to serve on [default 8000]",
)
parser.add_argument(
"-u",
"--unix",
dest="unix",
type=str,
default="",
help="location of unix socket\n ",
)
parser.add_argument(
"--cert", dest="cert", type=str, help="Location of certificate for SSL"
)
parser.add_argument(
"--key", dest="key", type=str, help="location of keyfile for SSL\n "
)
parser.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
)
parser.add_argument(
"-w",
"--workers",
dest="workers",
type=int,
default=1,
help="number of worker processes [default 1]\n ",
)
parser.add_argument("-d", "--debug", dest="debug", action="store_true")
parser.add_argument(
"-r",
"--reload",
"--auto-reload",
dest="auto_reload",
action="store_true",
help="Watch source directory for file changes and reload on changes",
)
parser.add_argument(
"-R",
"--reload-dir",
dest="path",
action="append",
help="Extra directories to watch and reload on changes\n ",
)
parser.add_argument(
"module",
help=(
"Path to your Sanic app. Example: path.to.server:app\n"
"If running a Simple Server, path to directory to serve. "
"Example: ./\n"
),
)
args = parser.parse_args()
try:
module_path = os.path.abspath(os.getcwd())
if module_path not in sys.path:
sys.path.append(module_path)
if args.simple:
path = Path(args.module)
app = create_simple_server(path)
else:
delimiter = ":" if ":" in args.module else "."
module_name, app_name = args.module.rsplit(delimiter, 1)
if app_name.endswith("()"):
args.factory = True
app_name = app_name[:-2]
module = import_module(module_name)
app = getattr(module, app_name, None)
if args.factory:
app = app()
app_type_name = type(app).__name__
if not isinstance(app, Sanic):
raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}. "
f"Perhaps you meant {args.module}.app?"
)
if args.cert is not None or args.key is not None:
ssl: Optional[Dict[str, Any]] = {
"cert": args.cert,
"key": args.key,
}
else:
ssl = None
kwargs = {
"host": args.host,
"port": args.port,
"unix": args.unix,
"workers": args.workers,
"debug": args.debug,
"access_log": args.access_log,
"ssl": ssl,
}
if args.auto_reload:
kwargs["auto_reload"] = True
if args.path:
if args.auto_reload or args.debug:
kwargs["reload_dir"] = args.path
else:
error_logger.warning(
"Ignoring '--reload-dir' since auto reloading was not "
"enabled. If you would like to watch directories for "
"changes, consider using --debug or --auto-reload."
)
app.run(**kwargs)
except ImportError as e:
if module_name.startswith(e.name):
error_logger.error(
f"No module named {e.name} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
)
else:
raise e
except ValueError:
error_logger.exception("Failed to run app")
if __name__ == "__main__":

View File

@@ -1 +1 @@
__version__ = "21.12.1"
__version__ = "21.9.3"

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +0,0 @@
from __future__ import annotations
from contextlib import suppress
from importlib import import_module
from typing import TYPE_CHECKING
if TYPE_CHECKING: # no cov
from sanic import Sanic
try:
from sanic_ext import Extend # type: ignore
except ImportError:
...
def setup_ext(app: Sanic, *, fail: bool = False, **kwargs):
if not app.config.AUTO_EXTEND:
return
sanic_ext = None
with suppress(ModuleNotFoundError):
sanic_ext = import_module("sanic_ext")
if not sanic_ext:
if fail:
raise RuntimeError(
"Sanic Extensions is not installed. You can add it to your "
"environment using:\n$ pip install sanic[ext]\nor\n$ pip "
"install sanic-ext"
)
return
if not getattr(app, "_ext", None):
Ext: Extend = getattr(sanic_ext, "Extend")
app._ext = Ext(app, **kwargs)
return app.ext

View File

@@ -1,57 +0,0 @@
import re
import sys
from os import environ
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
COFFEE_LOGO = """\033[48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ▄████████▄ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ██ ██▀▀▄ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ███████████ █ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ███████████▄▄▀ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ▀███████▀ \033[0m
\033[48;2;255;13;104m \033[0m
Dark roast. No sugar."""
COLOR_LOGO = """\033[48;2;255;13;104m \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ▄███ █████ ██ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ▀███████ ███▄ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ██ \033[0m
\033[38;2;255;255;255;48;2;255;13;104m ████ ████████▀ \033[0m
\033[48;2;255;13;104m \033[0m
Build Fast. Run Fast."""
FULL_COLOR_LOGO = """
\033[38;2;255;13;104m ▄███ █████ ██ \033[0m ▄█▄ ██ █ █ ▄██████████
\033[38;2;255;13;104m ██ \033[0m █ █ █ ██ █ █ ██
\033[38;2;255;13;104m ▀███████ ███▄ \033[0m ▀ █ █ ██ ▄ █ ██
\033[38;2;255;13;104m ██\033[0m █████████ █ ██ █ █ ▄▄
\033[38;2;255;13;104m ████ ████████▀ \033[0m █ █ █ ██ █ ▀██ ███████
""" # noqa
ansi_pattern = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
def get_logo(full=False, coffee=False):
logo = (
(FULL_COLOR_LOGO if full else (COFFEE_LOGO if coffee else COLOR_LOGO))
if sys.stdout.isatty()
else BASE_LOGO
)
if (
sys.platform == "darwin"
and environ.get("TERM_PROGRAM") == "Apple_Terminal"
):
logo = ansi_pattern.sub("", logo)
return logo

View File

@@ -1,143 +0,0 @@
import sys
from abc import ABC, abstractmethod
from shutil import get_terminal_size
from textwrap import indent, wrap
from typing import Dict, Optional
from sanic import __version__
from sanic.log import logger
class MOTD(ABC):
def __init__(
self,
logo: Optional[str],
serve_location: str,
data: Dict[str, str],
extra: Dict[str, str],
) -> None:
self.logo = logo
self.serve_location = serve_location
self.data = data
self.extra = extra
self.key_width = 0
self.value_width = 0
@abstractmethod
def display(self):
... # noqa
@classmethod
def output(
cls,
logo: Optional[str],
serve_location: str,
data: Dict[str, str],
extra: Dict[str, str],
) -> None:
motd_class = MOTDTTY if sys.stdout.isatty() else MOTDBasic
motd_class(logo, serve_location, data, extra).display()
class MOTDBasic(MOTD):
def display(self):
if self.logo:
logger.debug(self.logo)
lines = [f"Sanic v{__version__}"]
if self.serve_location:
lines.append(f"Goin' Fast @ {self.serve_location}")
lines += [
*(f"{key}: {value}" for key, value in self.data.items()),
*(f"{key}: {value}" for key, value in self.extra.items()),
]
for line in lines:
logger.info(line)
class MOTDTTY(MOTD):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.set_variables()
def set_variables(self): # no cov
fallback = (108, 24)
terminal_width = max(
get_terminal_size(fallback=fallback).columns, fallback[0]
)
self.max_value_width = terminal_width - fallback[0] + 36
self.key_width = 4
self.value_width = self.max_value_width
if self.data:
self.key_width = max(map(len, self.data.keys()))
self.value_width = min(
max(map(len, self.data.values())), self.max_value_width
)
self.logo_lines = self.logo.split("\n") if self.logo else []
self.logo_line_length = 24
self.centering_length = (
self.key_width + self.value_width + 2 + self.logo_line_length
)
self.display_length = self.key_width + self.value_width + 2
def display(self):
version = f"Sanic v{__version__}".center(self.centering_length)
running = (
f"Goin' Fast @ {self.serve_location}"
if self.serve_location
else ""
).center(self.centering_length)
length = len(version) + 2 - self.logo_line_length
first_filler = "" * (self.logo_line_length - 1)
second_filler = "" * length
display_filler = "" * (self.display_length + 2)
lines = [
f"\n{first_filler}{second_filler}",
f"{version}",
f"{running}",
f"{first_filler}{second_filler}",
]
self._render_data(lines, self.data, 0)
if self.extra:
logo_part = self._get_logo_part(len(lines) - 4)
lines.append(f"| {logo_part}{display_filler}")
self._render_data(lines, self.extra, len(lines) - 4)
self._render_fill(lines)
lines.append(f"{first_filler}{second_filler}\n")
logger.info(indent("\n".join(lines), " "))
def _render_data(self, lines, data, start):
offset = 0
for idx, (key, value) in enumerate(data.items(), start=start):
key = key.rjust(self.key_width)
wrapped = wrap(value, self.max_value_width, break_on_hyphens=False)
for wrap_index, part in enumerate(wrapped):
part = part.ljust(self.value_width)
logo_part = self._get_logo_part(idx + offset + wrap_index)
display = (
f"{key}: {part}"
if wrap_index == 0
else (" " * len(key) + f" {part}")
)
lines.append(f"{logo_part}{display}")
if wrap_index:
offset += 1
def _render_fill(self, lines):
filler = " " * self.display_length
idx = len(lines) - 5
for i in range(1, len(self.logo_lines) - idx):
logo_part = self.logo_lines[idx + i]
lines.append(f"{logo_part}{filler}")
def _get_logo_part(self, idx):
try:
logo_part = self.logo_lines[idx]
except IndexError:
logo_part = " " * (self.logo_line_length - 3)
return logo_part

View File

@@ -1,79 +0,0 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from enum import Enum, auto
from pathlib import Path
from socket import socket
from ssl import SSLContext
from typing import TYPE_CHECKING, Any, Optional, Set, Union
from sanic.log import logger
if TYPE_CHECKING:
from sanic import Sanic
class StrEnum(str, Enum):
def _generate_next_value_(name: str, *args) -> str: # type: ignore
return name.lower()
class Server(StrEnum):
SANIC = auto()
ASGI = auto()
GUNICORN = auto()
class Mode(StrEnum):
PRODUCTION = auto()
DEBUG = auto()
@dataclass
class ApplicationState:
app: Sanic
asgi: bool = field(default=False)
coffee: bool = field(default=False)
fast: bool = field(default=False)
host: str = field(default="")
port: int = field(default=0)
ssl: Optional[SSLContext] = field(default=None)
sock: Optional[socket] = field(default=None)
unix: Optional[str] = field(default=None)
mode: Mode = field(default=Mode.PRODUCTION)
reload_dirs: Set[Path] = field(default_factory=set)
server: Server = field(default=Server.SANIC)
is_running: bool = field(default=False)
is_started: bool = field(default=False)
is_stopping: bool = field(default=False)
verbosity: int = field(default=0)
workers: int = field(default=0)
# This property relates to the ApplicationState instance and should
# not be changed except in the __post_init__ method
_init: bool = field(default=False)
def __post_init__(self) -> None:
self._init = True
def __setattr__(self, name: str, value: Any) -> None:
if self._init and name == "_init":
raise RuntimeError(
"Cannot change the value of _init after instantiation"
)
super().__setattr__(name, value)
if self._init and hasattr(self, f"set_{name}"):
getattr(self, f"set_{name}")(value)
def set_mode(self, value: Union[str, Mode]):
if hasattr(self.app, "error_handler"):
self.app.error_handler.debug = self.app.debug
if getattr(self.app, "configure_logging", False) and self.app.debug:
logger.setLevel(logging.DEBUG)
@property
def is_debug(self):
return self.mode is Mode.DEBUG

View File

@@ -7,11 +7,8 @@ import sanic.app # noqa
from sanic.compat import Header
from sanic.exceptions import ServerError
from sanic.helpers import _default
from sanic.http import Stage
from sanic.models.asgi import ASGIReceive, ASGIScope, ASGISend, MockTransport
from sanic.request import Request
from sanic.response import BaseHTTPResponse
from sanic.server import ConnInfo
from sanic.server.websockets.connection import WebSocketConnection
@@ -54,13 +51,6 @@ class Lifespan:
await self.asgi_app.sanic_app._server_event("init", "before")
await self.asgi_app.sanic_app._server_event("init", "after")
if self.asgi_app.sanic_app.config.USE_UVLOOP is not _default:
warnings.warn(
"You have set the USE_UVLOOP configuration option, but Sanic "
"cannot control the event loop when running in ASGI mode."
"This option will be ignored."
)
async def shutdown(self) -> None:
"""
Gather the listeners to fire on server stop.
@@ -93,8 +83,6 @@ class ASGIApp:
transport: MockTransport
lifespan: Lifespan
ws: Optional[WebSocketConnection]
stage: Stage
response: Optional[BaseHTTPResponse]
def __init__(self) -> None:
self.ws = None
@@ -107,8 +95,6 @@ class ASGIApp:
instance.sanic_app = sanic_app
instance.transport = MockTransport(scope, receive, send)
instance.transport.loop = sanic_app.loop
instance.stage = Stage.IDLE
instance.response = None
setattr(instance.transport, "add_task", sanic_app.loop.create_task)
headers = Header(
@@ -163,8 +149,6 @@ class ASGIApp:
"""
Read and stream the body in chunks from an incoming ASGI message.
"""
if self.stage is Stage.IDLE:
self.stage = Stage.REQUEST
message = await self.transport.receive()
body = message.get("body", b"")
if not message.get("more_body", False):
@@ -179,17 +163,11 @@ class ASGIApp:
if data:
yield data
def respond(self, response: BaseHTTPResponse):
if self.stage is not Stage.HANDLER:
self.stage = Stage.FAILED
raise RuntimeError("Response already started")
if self.response is not None:
self.response.stream = None
def respond(self, response):
response.stream, self.response = self, response
return response
async def send(self, data, end_stream):
self.stage = Stage.IDLE if end_stream else Stage.RESPONSE
if self.response:
response, self.response = self.response, None
await self.transport.send(
@@ -217,7 +195,6 @@ class ASGIApp:
Handle the incoming request.
"""
try:
self.stage = Stage.HANDLER
await self.sanic_app.handle_request(self.request)
except Exception as e:
await self.sanic_app.handle_exception(self.request, e)

View File

@@ -1,8 +1,8 @@
import re
from typing import Any
from typing import Any, Tuple
from warnings import warn
from sanic.base.meta import SanicMeta
from sanic.exceptions import SanicException
from sanic.mixins.exceptions import ExceptionMixin
from sanic.mixins.listeners import ListenerMixin
@@ -11,7 +11,7 @@ from sanic.mixins.routes import RouteMixin
from sanic.mixins.signals import SignalMixin
VALID_NAME = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_\-]*$")
VALID_NAME = re.compile(r"^[a-zA-Z][a-zA-Z0-9_\-]*$")
class BaseSanic(
@@ -20,11 +20,10 @@ class BaseSanic(
ListenerMixin,
ExceptionMixin,
SignalMixin,
metaclass=SanicMeta,
):
__slots__ = ("name",)
__fake_slots__: Tuple[str, ...]
def __init__(self, name: str = None, *args: Any, **kwargs: Any) -> None:
def __init__(self, name: str = None, *args, **kwargs) -> None:
class_name = self.__class__.__name__
if name is None:
@@ -34,10 +33,11 @@ class BaseSanic(
)
if not VALID_NAME.match(name):
raise SanicException(
f"{class_name} instance named '{name}' uses an invalid "
"format. Names must begin with a character and may only "
"contain alphanumeric characters, _, or -."
warn(
f"{class_name} instance named '{name}' uses a format that is"
f"deprecated. Starting in version 21.12, {class_name} objects "
"must be named only using alphanumeric characters, _, or -.",
DeprecationWarning,
)
self.name = name
@@ -52,12 +52,15 @@ class BaseSanic(
return f'{self.__class__.__name__}(name="{self.name}")'
def __setattr__(self, name: str, value: Any) -> None:
try:
super().__setattr__(name, value)
except AttributeError as e:
raise AttributeError(
# This is a temporary compat layer so we can raise a warning until
# setting attributes on the app instance can be removed and deprecated
# with a proper implementation of __slots__
if name not in self.__fake_slots__:
warn(
f"Setting variables on {self.__class__.__name__} instances is "
"not allowed. You should change your "
f"{self.__class__.__name__} instance to use "
"deprecated and will be removed in version 21.12. You should "
f"change your {self.__class__.__name__} instance to use "
f"instance.ctx.{name} instead.",
) from e
DeprecationWarning,
)
super().__setattr__(name, value)

View File

View File

@@ -1,6 +0,0 @@
class SanicMeta(type):
@classmethod
def __prepare__(metaclass, name, bases, **kwds):
cls = super().__prepare__(metaclass, name, bases, **kwds)
cls["__slots__"] = ()
return cls

View File

@@ -5,7 +5,7 @@ from functools import partial
from typing import TYPE_CHECKING, List, Optional, Union
if TYPE_CHECKING: # no cov
if TYPE_CHECKING:
from sanic.blueprints import Blueprint
@@ -208,7 +208,7 @@ class BlueprintGroup(MutableSequence):
:param args: List of Python exceptions to be caught by the handler
:param kwargs: Additional optional arguments to be passed to the
exception handler
:return: a decorated method to handle global exceptions for any
:return a decorated method to handle global exceptions for any
blueprint registered under this group.
"""

View File

@@ -4,27 +4,13 @@ import asyncio
from collections import defaultdict
from copy import deepcopy
from functools import wraps
from inspect import isfunction
from itertools import chain
from types import SimpleNamespace
from typing import (
TYPE_CHECKING,
Any,
Dict,
Iterable,
List,
Optional,
Sequence,
Set,
Tuple,
Union,
)
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Union
from sanic_routing.exceptions import NotFound # type: ignore
from sanic_routing.route import Route # type: ignore
from sanic.base.root import BaseSanic
from sanic.base import BaseSanic
from sanic.blueprint_group import BlueprintGroup
from sanic.exceptions import SanicException
from sanic.helpers import Default, _default
@@ -36,34 +22,8 @@ from sanic.models.handler_types import (
)
if TYPE_CHECKING: # no cov
from sanic import Sanic
def lazy(func, as_decorator=True):
@wraps(func)
def decorator(bp, *args, **kwargs):
nonlocal as_decorator
kwargs["apply"] = False
pass_handler = None
if args and isfunction(args[0]):
as_decorator = False
def wrapper(handler):
future = func(bp, *args, **kwargs)
if as_decorator:
future = future(handler)
if bp.registered:
for app in bp.apps:
bp.register(app, {})
return future
return wrapper if as_decorator else wrapper(pass_handler)
return decorator
if TYPE_CHECKING:
from sanic import Sanic # noqa
class Blueprint(BaseSanic):
@@ -79,13 +39,13 @@ class Blueprint(BaseSanic):
:param name: unique name of the blueprint
:param url_prefix: URL to be prefixed before all route URLs
:param host: IP Address or FQDN for the sanic server to use.
:param host: IP Address of FQDN for the sanic server to use.
:param version: Blueprint Version
:param strict_slashes: Enforce the API urls are requested with a
trailing */*
"""
__slots__ = (
__fake_slots__ = (
"_apps",
"_future_routes",
"_future_statics",
@@ -98,6 +58,7 @@ class Blueprint(BaseSanic):
"host",
"listeners",
"middlewares",
"name",
"routes",
"statics",
"strict_slashes",
@@ -111,7 +72,7 @@ class Blueprint(BaseSanic):
self,
name: str = None,
url_prefix: Optional[str] = None,
host: Optional[Union[List[str], str]] = None,
host: Optional[str] = None,
version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None,
version_prefix: str = "/v",
@@ -154,21 +115,34 @@ class Blueprint(BaseSanic):
)
return self._apps
@property
def registered(self) -> bool:
return bool(self._apps)
def route(self, *args, **kwargs):
kwargs["apply"] = False
return super().route(*args, **kwargs)
exception = lazy(BaseSanic.exception)
listener = lazy(BaseSanic.listener)
middleware = lazy(BaseSanic.middleware)
route = lazy(BaseSanic.route)
signal = lazy(BaseSanic.signal)
static = lazy(BaseSanic.static, as_decorator=False)
def static(self, *args, **kwargs):
kwargs["apply"] = False
return super().static(*args, **kwargs)
def middleware(self, *args, **kwargs):
kwargs["apply"] = False
return super().middleware(*args, **kwargs)
def listener(self, *args, **kwargs):
kwargs["apply"] = False
return super().listener(*args, **kwargs)
def exception(self, *args, **kwargs):
kwargs["apply"] = False
return super().exception(*args, **kwargs)
def signal(self, event: str, *args, **kwargs):
kwargs["apply"] = False
return super().signal(event, *args, **kwargs)
def reset(self):
self._apps: Set[Sanic] = set()
self.exceptions: List[RouteHandler] = []
self.listeners: Dict[str, List[ListenerType[Any]]] = {}
self.listeners: Dict[str, List[ListenerType]] = {}
self.middlewares: List[MiddlewareType] = []
self.routes: List[Route] = []
self.statics: List[RouteHandler] = []
@@ -247,7 +221,7 @@ class Blueprint(BaseSanic):
version: Optional[Union[int, str, float]] = None,
strict_slashes: Optional[bool] = None,
version_prefix: str = "/v",
) -> BlueprintGroup:
):
"""
Create a list of blueprints, optionally grouping them under a
general URL prefix.
@@ -300,7 +274,6 @@ class Blueprint(BaseSanic):
middleware = []
exception_handlers = []
listeners = defaultdict(list)
registered = set()
# Routes
for future in self._future_routes:
@@ -327,15 +300,12 @@ class Blueprint(BaseSanic):
)
name = app._generate_name(future.name)
host = future.host or self.host
if isinstance(host, list):
host = tuple(host)
apply_route = FutureRoute(
future.handler,
uri[1:] if uri.startswith("//") else uri,
future.methods,
host,
future.host or self.host,
strict_slashes,
future.stream,
version,
@@ -347,13 +317,8 @@ class Blueprint(BaseSanic):
future.static,
version_prefix,
error_format,
future.route_context,
)
if (self, apply_route) in app._future_registry:
continue
registered.add(apply_route)
route = app._apply_route(apply_route)
operation = (
routes.extend if isinstance(route, list) else routes.append
@@ -365,11 +330,6 @@ class Blueprint(BaseSanic):
# Prepend the blueprint URI prefix if available
uri = url_prefix + future.uri if url_prefix else future.uri
apply_route = FutureStatic(uri, *future[1:])
if (self, apply_route) in app._future_registry:
continue
registered.add(apply_route)
route = app._apply_static(apply_route)
routes.append(route)
@@ -378,56 +338,34 @@ class Blueprint(BaseSanic):
if route_names:
# Middleware
for future in self._future_middleware:
if (self, future) in app._future_registry:
continue
middleware.append(app._apply_middleware(future, route_names))
# Exceptions
for future in self._future_exceptions:
if (self, future) in app._future_registry:
continue
exception_handlers.append(
app._apply_exception_handler(future, route_names)
)
# Event listeners
for future in self._future_listeners:
if (self, future) in app._future_registry:
continue
listeners[future.event].append(app._apply_listener(future))
for listener in self._future_listeners:
listeners[listener.event].append(app._apply_listener(listener))
# Signals
for future in self._future_signals:
if (self, future) in app._future_registry:
continue
future.condition.update({"__blueprint__": self.name})
# Force exclusive to be False
app._apply_signal(tuple((*future[:-1], False)))
for signal in self._future_signals:
signal.condition.update({"blueprint": self.name})
app._apply_signal(signal)
self.routes += [route for route in routes if isinstance(route, Route)]
self.websocket_routes += [
self.routes = [route for route in routes if isinstance(route, Route)]
self.websocket_routes = [
route for route in self.routes if route.ctx.websocket
]
self.middlewares += middleware
self.exceptions += exception_handlers
self.listeners.update(dict(listeners))
if self.registered:
self.register_futures(
self.apps,
self,
chain(
registered,
self._future_middleware,
self._future_exceptions,
self._future_listeners,
self._future_signals,
),
)
self.middlewares = middleware
self.exceptions = exception_handlers
self.listeners = dict(listeners)
async def dispatch(self, *args, **kwargs):
condition = kwargs.pop("condition", {})
condition.update({"__blueprint__": self.name})
condition.update({"blueprint": self.name})
kwargs["condition"] = condition
await asyncio.gather(
*[app.dispatch(*args, **kwargs) for app in self.apps]
@@ -455,10 +393,3 @@ class Blueprint(BaseSanic):
value = v
break
return value
@staticmethod
def register_futures(
apps: Set[Sanic], bp: Blueprint, futures: Sequence[Tuple[Any, ...]]
):
for app in apps:
app._future_registry.update(set((bp, item) for item in futures))

View File

View File

@@ -1,189 +0,0 @@
import os
import shutil
import sys
from argparse import ArgumentParser, RawTextHelpFormatter
from importlib import import_module
from pathlib import Path
from textwrap import indent
from typing import Any, List, Union
from sanic.app import Sanic
from sanic.application.logo import get_logo
from sanic.cli.arguments import Group
from sanic.log import error_logger
from sanic.simple import create_simple_server
class SanicArgumentParser(ArgumentParser):
...
class SanicCLI:
DESCRIPTION = indent(
f"""
{get_logo(True)}
To start running a Sanic application, provide a path to the module, where
app is a Sanic() instance:
$ sanic path.to.server:app
Or, a path to a callable that returns a Sanic() instance:
$ sanic path.to.factory:create_app --factory
Or, a path to a directory to run as a simple HTTP server:
$ sanic ./path/to/static --simple
""",
prefix=" ",
)
def __init__(self) -> None:
width = shutil.get_terminal_size().columns
self.parser = SanicArgumentParser(
prog="sanic",
description=self.DESCRIPTION,
formatter_class=lambda prog: RawTextHelpFormatter(
prog,
max_help_position=36 if width > 96 else 24,
indent_increment=4,
width=None,
),
)
self.parser._positionals.title = "Required\n========\n Positional"
self.parser._optionals.title = "Optional\n========\n General"
self.main_process = (
os.environ.get("SANIC_RELOADER_PROCESS", "") != "true"
)
self.args: List[Any] = []
def attach(self):
for group in Group._registry:
group.create(self.parser).attach()
def run(self):
# This is to provide backwards compat -v to display version
legacy_version = len(sys.argv) == 2 and sys.argv[-1] == "-v"
parse_args = ["--version"] if legacy_version else None
self.args = self.parser.parse_args(args=parse_args)
self._precheck()
try:
app = self._get_app()
kwargs = self._build_run_kwargs()
app.run(**kwargs)
except ValueError:
error_logger.exception("Failed to run app")
def _precheck(self):
if self.args.debug and self.main_process:
error_logger.warning(
"Starting in v22.3, --debug will no "
"longer automatically run the auto-reloader.\n Switch to "
"--dev to continue using that functionality."
)
# # Custom TLS mismatch handling for better diagnostics
if self.main_process and (
# one of cert/key missing
bool(self.args.cert) != bool(self.args.key)
# new and old style self.args used together
or self.args.tls
and self.args.cert
# strict host checking without certs would always fail
or self.args.tlshost
and not self.args.tls
and not self.args.cert
):
self.parser.print_usage(sys.stderr)
message = (
"TLS certificates must be specified by either of:\n"
" --cert certdir/fullchain.pem --key certdir/privkey.pem\n"
" --tls certdir (equivalent to the above)"
)
error_logger.error(message)
sys.exit(1)
def _get_app(self):
try:
module_path = os.path.abspath(os.getcwd())
if module_path not in sys.path:
sys.path.append(module_path)
if self.args.simple:
path = Path(self.args.module)
app = create_simple_server(path)
else:
delimiter = ":" if ":" in self.args.module else "."
module_name, app_name = self.args.module.rsplit(delimiter, 1)
if app_name.endswith("()"):
self.args.factory = True
app_name = app_name[:-2]
module = import_module(module_name)
app = getattr(module, app_name, None)
if self.args.factory:
app = app()
app_type_name = type(app).__name__
if not isinstance(app, Sanic):
raise ValueError(
f"Module is not a Sanic app, it is a {app_type_name}\n"
f" Perhaps you meant {self.args.module}.app?"
)
except ImportError as e:
if module_name.startswith(e.name):
error_logger.error(
f"No module named {e.name} found.\n"
" Example File: project/sanic_server.py -> app\n"
" Example Module: project.sanic_server.app"
)
else:
raise e
return app
def _build_run_kwargs(self):
ssl: Union[None, dict, str, list] = []
if self.args.tlshost:
ssl.append(None)
if self.args.cert is not None or self.args.key is not None:
ssl.append(dict(cert=self.args.cert, key=self.args.key))
if self.args.tls:
ssl += self.args.tls
if not ssl:
ssl = None
elif len(ssl) == 1 and ssl[0] is not None:
# Use only one cert, no TLSSelector.
ssl = ssl[0]
kwargs = {
"access_log": self.args.access_log,
"debug": self.args.debug,
"fast": self.args.fast,
"host": self.args.host,
"motd": self.args.motd,
"noisy_exceptions": self.args.noisy_exceptions,
"port": self.args.port,
"ssl": ssl,
"unix": self.args.unix,
"verbosity": self.args.verbosity or 0,
"workers": self.args.workers,
}
if self.args.auto_reload:
kwargs["auto_reload"] = True
if self.args.path:
if self.args.auto_reload or self.args.debug:
kwargs["reload_dir"] = self.args.path
else:
error_logger.warning(
"Ignoring '--reload-dir' since auto reloading was not "
"enabled. If you would like to watch directories for "
"changes, consider using --debug or --auto-reload."
)
return kwargs

View File

@@ -1,237 +0,0 @@
from __future__ import annotations
from argparse import ArgumentParser, _ArgumentGroup
from typing import List, Optional, Type, Union
from sanic_routing import __version__ as __routing_version__ # type: ignore
from sanic import __version__
class Group:
name: Optional[str]
container: Union[ArgumentParser, _ArgumentGroup]
_registry: List[Type[Group]] = []
def __init_subclass__(cls) -> None:
Group._registry.append(cls)
def __init__(self, parser: ArgumentParser, title: Optional[str]):
self.parser = parser
if title:
self.container = self.parser.add_argument_group(title=f" {title}")
else:
self.container = self.parser
@classmethod
def create(cls, parser: ArgumentParser):
instance = cls(parser, cls.name)
return instance
def add_bool_arguments(self, *args, **kwargs):
group = self.container.add_mutually_exclusive_group()
kwargs["help"] = kwargs["help"].capitalize()
group.add_argument(*args, action="store_true", **kwargs)
kwargs["help"] = f"no {kwargs['help'].lower()}".capitalize()
group.add_argument(
"--no-" + args[0][2:], *args[1:], action="store_false", **kwargs
)
class GeneralGroup(Group):
name = None
def attach(self):
self.container.add_argument(
"--version",
action="version",
version=f"Sanic {__version__}; Routing {__routing_version__}",
)
self.container.add_argument(
"module",
help=(
"Path to your Sanic app. Example: path.to.server:app\n"
"If running a Simple Server, path to directory to serve. "
"Example: ./\n"
),
)
class ApplicationGroup(Group):
name = "Application"
def attach(self):
self.container.add_argument(
"--factory",
action="store_true",
help=(
"Treat app as an application factory, "
"i.e. a () -> <Sanic app> callable"
),
)
self.container.add_argument(
"-s",
"--simple",
dest="simple",
action="store_true",
help=(
"Run Sanic as a Simple Server, and serve the contents of "
"a directory\n(module arg should be a path)"
),
)
class SocketGroup(Group):
name = "Socket binding"
def attach(self):
self.container.add_argument(
"-H",
"--host",
dest="host",
type=str,
default="127.0.0.1",
help="Host address [default 127.0.0.1]",
)
self.container.add_argument(
"-p",
"--port",
dest="port",
type=int,
default=8000,
help="Port to serve on [default 8000]",
)
self.container.add_argument(
"-u",
"--unix",
dest="unix",
type=str,
default="",
help="location of unix socket",
)
class TLSGroup(Group):
name = "TLS certificate"
def attach(self):
self.container.add_argument(
"--cert",
dest="cert",
type=str,
help="Location of fullchain.pem, bundle.crt or equivalent",
)
self.container.add_argument(
"--key",
dest="key",
type=str,
help="Location of privkey.pem or equivalent .key file",
)
self.container.add_argument(
"--tls",
metavar="DIR",
type=str,
action="append",
help=(
"TLS certificate folder with fullchain.pem and privkey.pem\n"
"May be specified multiple times to choose multiple "
"certificates"
),
)
self.container.add_argument(
"--tls-strict-host",
dest="tlshost",
action="store_true",
help="Only allow clients that send an SNI matching server certs",
)
class WorkerGroup(Group):
name = "Worker"
def attach(self):
group = self.container.add_mutually_exclusive_group()
group.add_argument(
"-w",
"--workers",
dest="workers",
type=int,
default=1,
help="Number of worker processes [default 1]",
)
group.add_argument(
"--fast",
dest="fast",
action="store_true",
help="Set the number of workers to max allowed",
)
self.add_bool_arguments(
"--access-logs", dest="access_log", help="display access logs"
)
class DevelopmentGroup(Group):
name = "Development"
def attach(self):
self.container.add_argument(
"--debug",
dest="debug",
action="store_true",
help="Run the server in debug mode",
)
self.container.add_argument(
"-d",
"--dev",
dest="debug",
action="store_true",
help=(
"Currently is an alias for --debug. But starting in v22.3, \n"
"--debug will no longer automatically trigger auto_restart. \n"
"However, --dev will continue, effectively making it the \n"
"same as debug + auto_reload."
),
)
self.container.add_argument(
"-r",
"--reload",
"--auto-reload",
dest="auto_reload",
action="store_true",
help=(
"Watch source directory for file changes and reload on "
"changes"
),
)
self.container.add_argument(
"-R",
"--reload-dir",
dest="path",
action="append",
help="Extra directories to watch and reload on changes",
)
class OutputGroup(Group):
name = "Output"
def attach(self):
self.add_bool_arguments(
"--motd",
dest="motd",
default=True,
help="Show the startup display",
)
self.container.add_argument(
"-v",
"--verbosity",
action="count",
help="Control logging noise, eg. -vv or --verbosity=2 [default 0]",
)
self.add_bool_arguments(
"--noisy-exceptions",
dest="noisy_exceptions",
help="Output stack traces for all exceptions",
)

View File

@@ -8,21 +8,6 @@ from multidict import CIMultiDict # type: ignore
OS_IS_WINDOWS = os.name == "nt"
UVLOOP_INSTALLED = False
try:
import uvloop # type: ignore # noqa
UVLOOP_INSTALLED = True
except ImportError:
pass
def enable_windows_color_support():
import ctypes
kernel = ctypes.windll.kernel32
kernel.SetConsoleMode(kernel.GetStdHandle(-11), 7)
class Header(CIMultiDict):

View File

@@ -1,34 +1,38 @@
from __future__ import annotations
from inspect import getmembers, isclass, isdatadescriptor
from inspect import isclass
from os import environ
from pathlib import Path
from typing import Any, Callable, Dict, Optional, Sequence, Union
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from warnings import warn
from sanic.errorpages import DEFAULT_FORMAT, check_error_format
from sanic.helpers import Default, _default
from sanic.errorpages import check_error_format
from sanic.http import Http
from sanic.log import deprecation, error_logger
from sanic.utils import load_module_from_file_location, str_to_bool
from .utils import load_module_from_file_location, str_to_bool
if TYPE_CHECKING: # no cov
from sanic import Sanic
SANIC_PREFIX = "SANIC_"
BASE_LOGO = """
Sanic
Build Fast. Run Fast.
"""
DEFAULT_CONFIG = {
"_FALLBACK_ERROR_FORMAT": _default,
"ACCESS_LOG": True,
"AUTO_EXTEND": True,
"AUTO_RELOAD": False,
"EVENT_AUTOREGISTER": False,
"FALLBACK_ERROR_FORMAT": "auto",
"FORWARDED_FOR_HEADER": "X-Forwarded-For",
"FORWARDED_SECRET": None,
"GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec
"KEEP_ALIVE_TIMEOUT": 5, # 5 seconds
"KEEP_ALIVE": True,
"MOTD": True,
"MOTD_DISPLAY": {},
"NOISY_EXCEPTIONS": False,
"PROXIES_COUNT": None,
"REAL_IP_HEADER": None,
"REGISTER": True,
@@ -38,39 +42,21 @@ DEFAULT_CONFIG = {
"REQUEST_MAX_SIZE": 100000000, # 100 megabytes
"REQUEST_TIMEOUT": 60, # 60 seconds
"RESPONSE_TIMEOUT": 60, # 60 seconds
"USE_UVLOOP": _default,
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
"WEBSOCKET_PING_INTERVAL": 20,
"WEBSOCKET_PING_TIMEOUT": 20,
}
# These values will be removed from the Config object in v22.6 and moved
# to the application state
DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES")
class DescriptorMeta(type):
def __init__(cls, *_):
cls.__setters__ = {name for name, _ in getmembers(cls, cls._is_setter)}
@staticmethod
def _is_setter(member: object):
return isdatadescriptor(member) and hasattr(member, "setter")
class Config(dict, metaclass=DescriptorMeta):
class Config(dict):
ACCESS_LOG: bool
AUTO_EXTEND: bool
AUTO_RELOAD: bool
EVENT_AUTOREGISTER: bool
FALLBACK_ERROR_FORMAT: str
FORWARDED_FOR_HEADER: str
FORWARDED_SECRET: Optional[str]
GRACEFUL_SHUTDOWN_TIMEOUT: float
KEEP_ALIVE_TIMEOUT: int
KEEP_ALIVE: bool
NOISY_EXCEPTIONS: bool
MOTD: bool
MOTD_DISPLAY: Dict[str, str]
PROXIES_COUNT: Optional[int]
REAL_IP_HEADER: Optional[str]
REGISTER: bool
@@ -81,7 +67,6 @@ class Config(dict, metaclass=DescriptorMeta):
REQUEST_TIMEOUT: int
RESPONSE_TIMEOUT: int
SERVER_NAME: str
USE_UVLOOP: Union[Default, bool]
WEBSOCKET_MAX_SIZE: int
WEBSOCKET_PING_INTERVAL: int
WEBSOCKET_PING_TIMEOUT: int
@@ -89,20 +74,17 @@ class Config(dict, metaclass=DescriptorMeta):
def __init__(
self,
defaults: Dict[str, Union[str, bool, int, float, None]] = None,
load_env: Optional[Union[bool, str]] = True,
env_prefix: Optional[str] = SANIC_PREFIX,
keep_alive: Optional[bool] = None,
*,
converters: Optional[Sequence[Callable[[str], Any]]] = None,
app: Optional[Sanic] = None,
):
defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults})
self._converters = [str, str_to_bool, float, int]
self._LOGO = ""
if converters:
for converter in converters:
self.register_type(converter)
self._app = app
self._LOGO = BASE_LOGO
if keep_alive is not None:
self.KEEP_ALIVE = keep_alive
@@ -110,6 +92,15 @@ class Config(dict, metaclass=DescriptorMeta):
if env_prefix != SANIC_PREFIX:
if env_prefix:
self.load_environment_vars(env_prefix)
elif load_env is not True:
if load_env:
self.load_environment_vars(prefix=load_env)
warn(
"Use of load_env is deprecated and will be removed in "
"21.12. Modify the configuration prefix by passing "
"env_prefix instead.",
DeprecationWarning,
)
else:
self.load_environment_vars(SANIC_PREFIX)
@@ -130,21 +121,9 @@ class Config(dict, metaclass=DescriptorMeta):
self.update({attr: value})
def update(self, *other, **kwargs) -> None:
kwargs.update({k: v for item in other for k, v in dict(item).items()})
setters: Dict[str, Any] = {
k: kwargs.pop(k)
for k in {**kwargs}.keys()
if k in self.__class__.__setters__
}
for key, value in setters.items():
try:
super().__setattr__(key, value)
except AttributeError:
...
super().update(**kwargs)
for attr, value in {**setters, **kwargs}.items():
other_mapping = {k: v for item in other for k, v in dict(item).items()}
super().update(*other, **kwargs)
for attr, value in {**other_mapping, **kwargs}.items():
self._post_set(attr, value)
def _post_set(self, attr, value) -> None:
@@ -155,37 +134,32 @@ class Config(dict, metaclass=DescriptorMeta):
"REQUEST_MAX_SIZE",
):
self._configure_header_size()
elif attr == "FALLBACK_ERROR_FORMAT":
self._check_error_format()
if self.app and value != self.app.error_handler.fallback:
if self.app.error_handler.fallback != "auto":
warn(
"Overriding non-default ErrorHandler fallback "
"value. Changing from "
f"{self.app.error_handler.fallback} to {value}."
)
self.app.error_handler.fallback = value
elif attr == "LOGO":
self._LOGO = value
deprecation(
warn(
"Setting the config.LOGO is deprecated and will no longer "
"be supported starting in v22.6.",
22.6,
DeprecationWarning,
)
@property
def app(self):
return self._app
@property
def LOGO(self):
return self._LOGO
@property
def FALLBACK_ERROR_FORMAT(self) -> str:
if self._FALLBACK_ERROR_FORMAT is _default:
return DEFAULT_FORMAT
return self._FALLBACK_ERROR_FORMAT
@FALLBACK_ERROR_FORMAT.setter
def FALLBACK_ERROR_FORMAT(self, value):
self._check_error_format(value)
if (
self._FALLBACK_ERROR_FORMAT is not _default
and value != self._FALLBACK_ERROR_FORMAT
):
error_logger.warning(
"Setting config.FALLBACK_ERROR_FORMAT on an already "
"configured value may have unintended consequences."
)
self._FALLBACK_ERROR_FORMAT = value
def _configure_header_size(self):
Http.set_header_max_size(
self.REQUEST_MAX_HEADER_SIZE,
@@ -193,51 +167,36 @@ class Config(dict, metaclass=DescriptorMeta):
self.REQUEST_MAX_SIZE,
)
def _check_error_format(self, format: Optional[str] = None):
check_error_format(format or self.FALLBACK_ERROR_FORMAT)
def _check_error_format(self):
check_error_format(self.FALLBACK_ERROR_FORMAT)
def load_environment_vars(self, prefix=SANIC_PREFIX):
"""
Looks for prefixed environment variables and applies them to the
configuration if present. This is called automatically when Sanic
starts up to load environment variables into config.
Looks for prefixed environment variables and applies
them to the configuration if present. This is called automatically when
Sanic starts up to load environment variables into config.
It will automatically hydrate the following types:
It will automatically hyrdate the following types:
- ``int``
- ``float``
- ``bool``
Anything else will be imported as a ``str``. If you would like to add
additional types to this list, you can use
:meth:`sanic.config.Config.register_type`. Just make sure that they
are registered before you instantiate your application.
.. code-block:: python
class Foo:
def __init__(self, name) -> None:
self.name = name
config = Config(converters=[Foo])
app = Sanic(__name__, config=config)
`See user guide re: config
<https://sanicframework.org/guide/deployment/configuration.html>`__
Anything else will be imported as a ``str``.
"""
for key, value in environ.items():
if not key.startswith(prefix):
continue
_, config_key = key.split(prefix, 1)
for converter in reversed(self._converters):
for k, v in environ.items():
if k.startswith(prefix):
_, config_key = k.split(prefix, 1)
try:
self[config_key] = converter(value)
break
self[config_key] = int(v)
except ValueError:
pass
try:
self[config_key] = float(v)
except ValueError:
try:
self[config_key] = str_to_bool(v)
except ValueError:
self[config_key] = v
def update_config(self, config: Union[bytes, str, dict, Any]):
"""
@@ -307,17 +266,3 @@ class Config(dict, metaclass=DescriptorMeta):
self.update(config)
load = update_config
def register_type(self, converter: Callable[[str], Any]) -> None:
"""
Allows for adding custom function to cast from a string value to any
other type. The function should raise ValueError if it is not the
correct type.
"""
if converter in self._converters:
error_logger.warning(
f"Configuration value converter '{converter.__name__}' has "
"already been registered"
)
return
self._converters.append(converter)

View File

@@ -25,16 +25,14 @@ from sanic.request import Request
from sanic.response import HTTPResponse, html, json, text
dumps: t.Callable[..., str]
try:
from ujson import dumps
dumps = partial(dumps, escape_forward_slashes=False)
except ImportError: # noqa
from json import dumps
from json import dumps # type: ignore
DEFAULT_FORMAT = "auto"
FALLBACK_TEXT = (
"The server encountered an internal error and "
"cannot complete your request."
@@ -47,8 +45,6 @@ class BaseRenderer:
Base class that all renderers must inherit from.
"""
dumps = staticmethod(dumps)
def __init__(self, request, exception, debug):
self.request = request
self.exception = exception
@@ -116,16 +112,14 @@ class HTMLRenderer(BaseRenderer):
TRACEBACK_STYLE = """
html { font-family: sans-serif }
h2 { color: #888; }
.tb-wrapper p, dl, dd { margin: 0 }
.tb-wrapper p { margin: 0 }
.frame-border { margin: 1rem }
.frame-line > *, dt, dd { padding: 0.3rem 0.6rem }
.frame-line, dl { margin-bottom: 0.3rem }
.frame-code, dd { font-size: 16px; padding-left: 4ch }
.tb-wrapper, dl { border: 1px solid #eee }
.tb-header,.obj-header {
background: #eee; padding: 0.3rem; font-weight: bold
}
.frame-descriptor, dt { background: #e2eafb; font-size: 14px }
.frame-line > * { padding: 0.3rem 0.6rem }
.frame-line { margin-bottom: 0.3rem }
.frame-code { font-size: 16px; padding-left: 4ch }
.tb-wrapper { border: 1px solid #eee }
.tb-header { background: #eee; padding: 0.3rem; font-weight: bold }
.frame-descriptor { background: #e2eafb; font-size: 14px }
"""
TRACEBACK_WRAPPER_HTML = (
"<div class=tb-header>{exc_name}: {exc_value}</div>"
@@ -144,11 +138,6 @@ class HTMLRenderer(BaseRenderer):
"<p class=frame-code><code>{0.line}</code>"
"</div>"
)
OBJECT_WRAPPER_HTML = (
"<div class=obj-header>{title}</div>"
"<dl class={obj_type}>{display_html}</dl>"
)
OBJECT_DISPLAY_HTML = "<dt>{key}</dt><dd><code>{value}</code></dd>"
OUTPUT_HTML = (
"<!DOCTYPE html><html lang=en>"
"<meta charset=UTF-8><title>{title}</title>\n"
@@ -163,7 +152,7 @@ class HTMLRenderer(BaseRenderer):
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body=self._generate_body(full=True),
body=self._generate_body(),
),
status=self.status,
)
@@ -174,7 +163,7 @@ class HTMLRenderer(BaseRenderer):
title=self.title,
text=self.text,
style=self.TRACEBACK_STYLE,
body=self._generate_body(full=False),
body="",
),
status=self.status,
headers=self.headers,
@@ -188,49 +177,27 @@ class HTMLRenderer(BaseRenderer):
def title(self):
return escape(f"⚠️ {super().title}")
def _generate_body(self, *, full):
lines = []
if full:
_, exc_value, __ = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
appname = escape(self.request.app.name)
name = escape(self.exception.__class__.__name__)
value = escape(self.exception)
path = escape(self.request.path)
lines += [
f"<h2>Traceback of {appname} " "(most recent call last):</h2>",
f"{traceback_html}",
"<div class=summary><p>",
f"<b>{name}: {value}</b> "
f"while handling path <code>{path}</code>",
"</div>",
]
for attr, display in (("context", True), ("extra", bool(full))):
info = getattr(self.exception, attr, None)
if info and display:
lines.append(self._generate_object_display(info, attr))
def _generate_body(self):
_, exc_value, __ = sys.exc_info()
exceptions = []
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
traceback_html = self.TRACEBACK_BORDER.join(reversed(exceptions))
appname = escape(self.request.app.name)
name = escape(self.exception.__class__.__name__)
value = escape(self.exception)
path = escape(self.request.path)
lines = [
f"<h2>Traceback of {appname} (most recent call last):</h2>",
f"{traceback_html}",
"<div class=summary><p>",
f"<b>{name}: {value}</b> while handling path <code>{path}</code>",
"</div>",
]
return "\n".join(lines)
def _generate_object_display(
self, obj: t.Dict[str, t.Any], descriptor: str
) -> str:
display = "".join(
self.OBJECT_DISPLAY_HTML.format(key=key, value=value)
for key, value in obj.items()
)
return self.OBJECT_WRAPPER_HTML.format(
title=descriptor.title(),
display_html=display,
obj_type=descriptor.lower(),
)
def _format_exc(self, exc):
frames = extract_tb(exc.__traceback__)
frame_html = "".join(
@@ -257,7 +224,7 @@ class TextRenderer(BaseRenderer):
title=self.title,
text=self.text,
bar=("=" * len(self.title)),
body=self._generate_body(full=True),
body=self._generate_body(),
),
status=self.status,
)
@@ -268,7 +235,7 @@ class TextRenderer(BaseRenderer):
title=self.title,
text=self.text,
bar=("=" * len(self.title)),
body=self._generate_body(full=False),
body="",
),
status=self.status,
headers=self.headers,
@@ -278,31 +245,21 @@ class TextRenderer(BaseRenderer):
def title(self):
return f"⚠️ {super().title}"
def _generate_body(self, *, full):
lines = []
if full:
_, exc_value, __ = sys.exc_info()
exceptions = []
def _generate_body(self):
_, exc_value, __ = sys.exc_info()
exceptions = []
lines += [
f"{self.exception.__class__.__name__}: {self.exception} while "
f"handling path {self.request.path}",
f"Traceback of {self.request.app.name} "
"(most recent call last):\n",
]
lines = [
f"{self.exception.__class__.__name__}: {self.exception} while "
f"handling path {self.request.path}",
f"Traceback of {self.request.app.name} (most recent call last):\n",
]
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
while exc_value:
exceptions.append(self._format_exc(exc_value))
exc_value = exc_value.__cause__
lines += exceptions[::-1]
for attr, display in (("context", True), ("extra", bool(full))):
info = getattr(self.exception, attr, None)
if info and display:
lines += self._generate_object_display_list(info, attr)
return "\n".join(lines)
return "\n".join(lines + exceptions[::-1])
def _format_exc(self, exc):
frames = "\n\n".join(
@@ -315,13 +272,6 @@ class TextRenderer(BaseRenderer):
)
return f"{self.SPACER}{exc.__class__.__name__}: {exc}\n{frames}"
def _generate_object_display_list(self, obj, descriptor):
lines = [f"\n{descriptor.title()}"]
for key, value in obj.items():
display = self.dumps(value)
lines.append(f"{self.SPACER * 2}{key}: {display}")
return lines
class JSONRenderer(BaseRenderer):
"""
@@ -330,11 +280,11 @@ class JSONRenderer(BaseRenderer):
def full(self) -> HTTPResponse:
output = self._generate_output(full=True)
return json(output, status=self.status, dumps=self.dumps)
return json(output, status=self.status, dumps=dumps)
def minimal(self) -> HTTPResponse:
output = self._generate_output(full=False)
return json(output, status=self.status, dumps=self.dumps)
return json(output, status=self.status, dumps=dumps)
def _generate_output(self, *, full):
output = {
@@ -343,11 +293,6 @@ class JSONRenderer(BaseRenderer):
"message": self.text,
}
for attr, display in (("context", True), ("extra", bool(full))):
info = getattr(self.exception, attr, None)
if info and display:
output[attr] = info
if full:
_, exc_value, __ = sys.exc_info()
exceptions = []

View File

@@ -1,4 +1,4 @@
from typing import Any, Dict, Optional, Union
from typing import Optional, Union
from sanic.helpers import STATUS_CODES
@@ -11,11 +11,7 @@ class SanicException(Exception):
message: Optional[Union[str, bytes]] = None,
status_code: Optional[int] = None,
quiet: Optional[bool] = None,
context: Optional[Dict[str, Any]] = None,
extra: Optional[Dict[str, Any]] = None,
) -> None:
self.context = context
self.extra = extra
if message is None:
if self.message:
message = self.message
@@ -244,3 +240,25 @@ class InvalidSignal(SanicException):
class WebsocketClosed(SanicException):
quiet = True
message = "Client has closed the websocket connection"
def abort(status_code: int, message: Optional[Union[str, bytes]] = None):
"""
Raise an exception based on SanicException. Returns the HTTP response
message appropriate for the given status code, unless provided.
STATUS_CODES from sanic.helpers for the given status code.
:param status_code: The HTTP status code to return.
:param message: The HTTP response body. Defaults to the messages in
"""
import warnings
warnings.warn(
"sanic.exceptions.abort has been marked as deprecated, and will be "
"removed in release 21.12.\n To migrate your code, simply replace "
"abort(status_code, msg) with raise SanicException(msg, status_code), "
"or even better, raise an appropriate SanicException subclass."
)
raise SanicException(message=message, status_code=status_code)

View File

@@ -1,23 +1,13 @@
from __future__ import annotations
from inspect import signature
from typing import Dict, List, Optional, Tuple, Type, Union
from typing import Dict, List, Optional, Tuple, Type
from sanic.config import Config
from sanic.errorpages import (
DEFAULT_FORMAT,
BaseRenderer,
HTMLRenderer,
exception_response,
)
from sanic.errorpages import BaseRenderer, HTMLRenderer, exception_response
from sanic.exceptions import (
ContentRangeError,
HeaderNotFound,
InvalidRangeType,
SanicException,
)
from sanic.helpers import Default, _default
from sanic.log import deprecation, error_logger
from sanic.log import error_logger
from sanic.models.handler_types import RouteHandler
from sanic.response import text
@@ -37,91 +27,24 @@ class ErrorHandler:
# Beginning in v22.3, the base renderer will be TextRenderer
def __init__(
self,
fallback: Union[str, Default] = _default,
base: Type[BaseRenderer] = HTMLRenderer,
self, fallback: str = "auto", base: Type[BaseRenderer] = HTMLRenderer
):
self.handlers: List[Tuple[Type[BaseException], RouteHandler]] = []
self.cached_handlers: Dict[
Tuple[Type[BaseException], Optional[str]], Optional[RouteHandler]
] = {}
self.debug = False
self._fallback = fallback
self.fallback = fallback
self.base = base
if fallback is not _default:
self._warn_fallback_deprecation()
@property
def fallback(self):
# This is for backwards compat and can be removed in v22.6
if self._fallback is _default:
return DEFAULT_FORMAT
return self._fallback
@fallback.setter
def fallback(self, value: str):
self._warn_fallback_deprecation()
if not isinstance(value, str):
raise SanicException(
f"Cannot set error handler fallback to: value={value}"
)
self._fallback = value
@staticmethod
def _warn_fallback_deprecation():
deprecation(
"Setting the ErrorHandler fallback value directly is "
"deprecated and no longer supported. This feature will "
"be removed in v22.6. Instead, use "
"app.config.FALLBACK_ERROR_FORMAT.",
22.6,
)
@classmethod
def _get_fallback_value(cls, error_handler: ErrorHandler, config: Config):
if error_handler._fallback is not _default:
if config._FALLBACK_ERROR_FORMAT is _default:
return error_handler.fallback
error_logger.warning(
"Conflicting error fallback values were found in the "
"error handler and in the app.config while handling an "
"exception. Using the value from app.config."
)
return config.FALLBACK_ERROR_FORMAT
@classmethod
def finalize(
cls,
error_handler: ErrorHandler,
fallback: Optional[str] = None,
config: Optional[Config] = None,
):
if fallback:
deprecation(
"Setting the ErrorHandler fallback value via finalize() "
"is deprecated and no longer supported. This feature will "
"be removed in v22.6. Instead, use "
"app.config.FALLBACK_ERROR_FORMAT.",
22.6,
)
if config is None:
deprecation(
"Starting in v22.3, config will be a required argument "
"for ErrorHandler.finalize().",
22.3,
)
if fallback and fallback != DEFAULT_FORMAT:
if error_handler._fallback is not _default:
error_logger.warning(
f"Setting the fallback value to {fallback}. This changes "
"the current non-default value "
f"'{error_handler._fallback}'."
)
error_handler._fallback = fallback
def finalize(cls, error_handler, fallback: Optional[str] = None):
if (
fallback
and fallback != "auto"
and error_handler.fallback == "auto"
):
error_handler.fallback = fallback
if not isinstance(error_handler, cls):
error_logger.warning(
@@ -130,18 +53,18 @@ class ErrorHandler:
sig = signature(error_handler.lookup)
if len(sig.parameters) == 1:
deprecation(
"You are using a deprecated error handler. The lookup "
"method should accept two positional parameters: "
"(exception, route_name: Optional[str]). "
"Until you upgrade your ErrorHandler.lookup, Blueprint "
"specific exceptions will not work properly. Beginning "
"in v22.3, the legacy style lookup method will not "
"work at all.",
22.3,
error_logger.warning(
DeprecationWarning(
"You are using a deprecated error handler. The lookup "
"method should accept two positional parameters: "
"(exception, route_name: Optional[str]). "
"Until you upgrade your ErrorHandler.lookup, Blueprint "
"specific exceptions will not work properly. Beginning "
"in v22.3, the legacy style lookup method will not "
"work at all."
),
)
legacy_lookup = error_handler._legacy_lookup
error_handler._lookup = legacy_lookup # type: ignore
error_handler._lookup = error_handler._legacy_lookup
def _full_lookup(self, exception, route_name: Optional[str] = None):
return self.lookup(exception, route_name)
@@ -265,20 +188,18 @@ class ErrorHandler:
:return:
"""
self.log(request, exception)
fallback = ErrorHandler._get_fallback_value(self, request.app.config)
return exception_response(
request,
exception,
debug=self.debug,
base=self.base,
fallback=fallback,
fallback=self.fallback,
)
@staticmethod
def log(request, exception):
quiet = getattr(exception, "quiet", False)
noisy = getattr(request.app.config, "NOISY_EXCEPTIONS", False)
if quiet is False or noisy is True:
if quiet is False:
try:
url = repr(request.url)
except AttributeError:

View File

@@ -28,7 +28,7 @@ _host_re = re.compile(
# RFC's quoted-pair escapes are mostly ignored by browsers. Chrome, Firefox and
# curl all have different escaping, that we try to handle as well as possible,
# even though no client escapes in a way that would allow perfect handling.
# even though no client espaces in a way that would allow perfect handling.
# For more information, consult ../tests/test_requests.py

View File

@@ -144,7 +144,7 @@ def import_string(module_name, package=None):
import a module or class by string path.
:module_name: str with path of module or path to import and
instantiate a class
instanciate a class
:returns: a module object or one instance from class if
module_name is a valid path to class

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING: # no cov
if TYPE_CHECKING:
from sanic.request import Request
from sanic.response import BaseHTTPResponse
@@ -584,11 +584,6 @@ class Http(metaclass=TouchUpMeta):
self.stage = Stage.FAILED
raise RuntimeError("Response already started")
# Disconnect any earlier but unused response object
if self.response is not None:
self.response.stream = None
# Connect and return the response
self.response, response.stream = response, self
return response

View File

@@ -1,12 +1,8 @@
import logging
import sys
from enum import Enum
from typing import Any, Dict
from warnings import warn
LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
LOGGING_CONFIG_DEFAULTS = dict(
version=1,
disable_existing_loggers=False,
loggers={
@@ -57,14 +53,6 @@ LOGGING_CONFIG_DEFAULTS: Dict[str, Any] = dict(
)
class Colors(str, Enum):
END = "\033[0m"
BLUE = "\033[01;34m"
GREEN = "\033[01;32m"
YELLOW = "\033[01;33m"
RED = "\033[01;31m"
logger = logging.getLogger("sanic.root")
"""
General Sanic logger
@@ -79,11 +67,3 @@ access_logger = logging.getLogger("sanic.access")
"""
Logger used by Sanic for access logging
"""
def deprecation(message: str, version: float):
version_info = f"[DEPRECATION v{version}] "
if sys.stdout.isatty():
version_info = f"{Colors.RED}{version_info}"
message = f"{Colors.YELLOW}{message}{Colors.END}"
warn(version_info + message, DeprecationWarning)

View File

@@ -1,10 +1,9 @@
from typing import Set
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureException
class ExceptionMixin(metaclass=SanicMeta):
class ExceptionMixin:
def __init__(self, *args, **kwargs) -> None:
self._future_exceptions: Set[FutureException] = set()

View File

@@ -2,9 +2,8 @@ from enum import Enum, auto
from functools import partial
from typing import List, Optional, Union
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureListener
from sanic.models.handler_types import ListenerType, Sanic
from sanic.models.handler_types import ListenerType
class ListenerEvent(str, Enum):
@@ -19,7 +18,7 @@ class ListenerEvent(str, Enum):
MAIN_PROCESS_STOP = auto()
class ListenerMixin(metaclass=SanicMeta):
class ListenerMixin:
def __init__(self, *args, **kwargs) -> None:
self._future_listeners: List[FutureListener] = []
@@ -28,10 +27,10 @@ class ListenerMixin(metaclass=SanicMeta):
def listener(
self,
listener_or_event: Union[ListenerType[Sanic], str],
listener_or_event: Union[ListenerType, str],
event_or_none: Optional[str] = None,
apply: bool = True,
) -> ListenerType[Sanic]:
):
"""
Create a listener from a decorated function.
@@ -63,32 +62,20 @@ class ListenerMixin(metaclass=SanicMeta):
else:
return partial(register_listener, event=listener_or_event)
def main_process_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
def main_process_start(self, listener: ListenerType) -> ListenerType:
return self.listener(listener, "main_process_start")
def main_process_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
def main_process_stop(self, listener: ListenerType) -> ListenerType:
return self.listener(listener, "main_process_stop")
def before_server_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
def before_server_start(self, listener: ListenerType) -> ListenerType:
return self.listener(listener, "before_server_start")
def after_server_start(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
def after_server_start(self, listener: ListenerType) -> ListenerType:
return self.listener(listener, "after_server_start")
def before_server_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
def before_server_stop(self, listener: ListenerType) -> ListenerType:
return self.listener(listener, "before_server_stop")
def after_server_stop(
self, listener: ListenerType[Sanic]
) -> ListenerType[Sanic]:
def after_server_stop(self, listener: ListenerType) -> ListenerType:
return self.listener(listener, "after_server_stop")

View File

@@ -1,11 +1,10 @@
from functools import partial
from typing import List
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureMiddleware
class MiddlewareMixin(metaclass=SanicMeta):
class MiddlewareMixin:
def __init__(self, *args, **kwargs) -> None:
self._future_middleware: List[FutureMiddleware] = []

View File

@@ -1,5 +1,4 @@
from ast import NodeVisitor, Return, parse
from contextlib import suppress
from functools import partial, wraps
from inspect import getsource, signature
from mimetypes import guess_type
@@ -13,7 +12,6 @@ from urllib.parse import unquote
from sanic_routing.route import Route # type: ignore
from sanic.base.meta import SanicMeta
from sanic.compat import stat_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
from sanic.errorpages import RESPONSE_MAPPING
@@ -24,27 +22,19 @@ from sanic.exceptions import (
InvalidUsage,
)
from sanic.handlers import ContentRangeHandler
from sanic.log import deprecation, error_logger
from sanic.log import error_logger
from sanic.models.futures import FutureRoute, FutureStatic
from sanic.models.handler_types import RouteHandler
from sanic.response import HTTPResponse, file, file_stream
from sanic.types import HashableDict
from sanic.views import CompositionView
RouteWrapper = Callable[
[RouteHandler], Union[RouteHandler, Tuple[Route, RouteHandler]]
]
RESTRICTED_ROUTE_CONTEXT = (
"ignore_body",
"stream",
"hosts",
"static",
"error_format",
"websocket",
)
class RouteMixin(metaclass=SanicMeta):
class RouteMixin:
name: str
def __init__(self, *args, **kwargs) -> None:
@@ -62,7 +52,7 @@ class RouteMixin(metaclass=SanicMeta):
self,
uri: str,
methods: Optional[Iterable[str]] = None,
host: Optional[Union[str, List[str]]] = None,
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
stream: bool = False,
version: Optional[Union[int, str, float]] = None,
@@ -75,20 +65,10 @@ class RouteMixin(metaclass=SanicMeta):
static: bool = False,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs: Any,
) -> RouteWrapper:
"""
Decorate a function to be registered as a route
**Example using context kwargs**
.. code-block:: python
@app.route(..., ctx_foo="foobar")
async def route_handler(request: Request):
assert request.route.ctx.foo == "foobar"
:param uri: path of the URL
:param methods: list or tuple of methods allowed
:param host: the host, if required
@@ -100,8 +80,6 @@ class RouteMixin(metaclass=SanicMeta):
body (eg. GET requests)
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: tuple of routes, decorated function
"""
@@ -116,8 +94,6 @@ class RouteMixin(metaclass=SanicMeta):
if not methods and not websocket:
methods = frozenset({"GET"})
route_context = self._build_route_context(ctx_kwargs)
def decorator(handler):
nonlocal uri
nonlocal methods
@@ -176,7 +152,6 @@ class RouteMixin(metaclass=SanicMeta):
static,
version_prefix,
error_format,
route_context,
)
self._future_routes.add(route)
@@ -214,14 +189,13 @@ class RouteMixin(metaclass=SanicMeta):
handler: RouteHandler,
uri: str,
methods: Iterable[str] = frozenset({"GET"}),
host: Optional[Union[str, List[str]]] = None,
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
version: Optional[Union[int, str, float]] = None,
version: Optional[int] = None,
name: Optional[str] = None,
stream: bool = False,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteHandler:
"""A helper method to register class instance or
functions as a handler to the application url
@@ -238,8 +212,6 @@ class RouteMixin(metaclass=SanicMeta):
:param stream: boolean specifying if the handler is a stream handler
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: function or class instance
"""
# Handle HTTPMethodView differently
@@ -254,6 +226,14 @@ class RouteMixin(metaclass=SanicMeta):
if hasattr(_handler, "is_stream"):
stream = True
# handle composition view differently
if isinstance(handler, CompositionView):
methods = handler.handlers.keys()
for _handler in handler.handlers.values():
if hasattr(_handler, "is_stream"):
stream = True
break
if strict_slashes is None:
strict_slashes = self.strict_slashes
@@ -267,7 +247,6 @@ class RouteMixin(metaclass=SanicMeta):
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)(handler)
return handler
@@ -275,14 +254,13 @@ class RouteMixin(metaclass=SanicMeta):
def get(
self,
uri: str,
host: Optional[Union[str, List[str]]] = None,
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
version: Optional[Union[int, str, float]] = None,
version: Optional[int] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **GET** *HTTP* method
@@ -295,8 +273,6 @@ class RouteMixin(metaclass=SanicMeta):
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -309,20 +285,18 @@ class RouteMixin(metaclass=SanicMeta):
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def post(
self,
uri: str,
host: Optional[Union[str, List[str]]] = None,
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
stream: bool = False,
version: Optional[Union[int, str, float]] = None,
version: Optional[int] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **POST** *HTTP* method
@@ -335,8 +309,6 @@ class RouteMixin(metaclass=SanicMeta):
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -349,20 +321,18 @@ class RouteMixin(metaclass=SanicMeta):
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def put(
self,
uri: str,
host: Optional[Union[str, List[str]]] = None,
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
stream: bool = False,
version: Optional[Union[int, str, float]] = None,
version: Optional[int] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **PUT** *HTTP* method
@@ -375,8 +345,6 @@ class RouteMixin(metaclass=SanicMeta):
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -389,20 +357,18 @@ class RouteMixin(metaclass=SanicMeta):
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def head(
self,
uri: str,
host: Optional[Union[str, List[str]]] = None,
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
version: Optional[Union[int, str, float]] = None,
version: Optional[int] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **HEAD** *HTTP* method
@@ -423,8 +389,6 @@ class RouteMixin(metaclass=SanicMeta):
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -437,20 +401,18 @@ class RouteMixin(metaclass=SanicMeta):
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def options(
self,
uri: str,
host: Optional[Union[str, List[str]]] = None,
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
version: Optional[Union[int, str, float]] = None,
version: Optional[int] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **OPTIONS** *HTTP* method
@@ -471,8 +433,6 @@ class RouteMixin(metaclass=SanicMeta):
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -485,20 +445,18 @@ class RouteMixin(metaclass=SanicMeta):
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def patch(
self,
uri: str,
host: Optional[Union[str, List[str]]] = None,
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
stream=False,
version: Optional[Union[int, str, float]] = None,
version: Optional[int] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **PATCH** *HTTP* method
@@ -521,8 +479,6 @@ class RouteMixin(metaclass=SanicMeta):
:type ignore_body: bool, optional
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -535,20 +491,18 @@ class RouteMixin(metaclass=SanicMeta):
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def delete(
self,
uri: str,
host: Optional[Union[str, List[str]]] = None,
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
version: Optional[Union[int, str, float]] = None,
version: Optional[int] = None,
name: Optional[str] = None,
ignore_body: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
) -> RouteWrapper:
"""
Add an API URL under the **DELETE** *HTTP* method
@@ -561,8 +515,6 @@ class RouteMixin(metaclass=SanicMeta):
:param name: Unique name that can be used to identify the Route
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Object decorated with :func:`route` method
"""
return self.route(
@@ -575,21 +527,19 @@ class RouteMixin(metaclass=SanicMeta):
ignore_body=ignore_body,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def websocket(
self,
uri: str,
host: Optional[Union[str, List[str]]] = None,
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
subprotocols: Optional[List[str]] = None,
version: Optional[Union[int, str, float]] = None,
version: Optional[int] = None,
name: Optional[str] = None,
apply: bool = True,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
):
"""
Decorate a function to be registered as a websocket route
@@ -603,8 +553,6 @@ class RouteMixin(metaclass=SanicMeta):
be used with :func:`url_for`
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: tuple of routes, decorated function
"""
return self.route(
@@ -619,21 +567,19 @@ class RouteMixin(metaclass=SanicMeta):
websocket=True,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)
def add_websocket_route(
self,
handler,
uri: str,
host: Optional[Union[str, List[str]]] = None,
host: Optional[str] = None,
strict_slashes: Optional[bool] = None,
subprotocols=None,
version: Optional[Union[int, str, float]] = None,
version: Optional[int] = None,
name: Optional[str] = None,
version_prefix: str = "/v",
error_format: Optional[str] = None,
**ctx_kwargs,
):
"""
A helper method to register a function as a websocket route.
@@ -652,8 +598,6 @@ class RouteMixin(metaclass=SanicMeta):
be used with :func:`url_for`
:param version_prefix: URL path that should be before the version
value; default: ``/v``
:param ctx_kwargs: Keyword arguments that begin with a ctx_* prefix
will be appended to the route context (``route.ctx``)
:return: Objected decorated by :func:`websocket`
"""
return self.websocket(
@@ -665,7 +609,6 @@ class RouteMixin(metaclass=SanicMeta):
name=name,
version_prefix=version_prefix,
error_format=error_format,
**ctx_kwargs,
)(handler)
def static(
@@ -975,16 +918,19 @@ class RouteMixin(metaclass=SanicMeta):
return route
def _determine_error_format(self, handler) -> str:
with suppress(OSError, TypeError):
src = dedent(getsource(handler))
tree = parse(src)
http_response_types = self._get_response_types(tree)
def _determine_error_format(self, handler) -> Optional[str]:
if not isinstance(handler, CompositionView):
try:
src = dedent(getsource(handler))
tree = parse(src)
http_response_types = self._get_response_types(tree)
if len(http_response_types) == 1:
return next(iter(http_response_types))
if len(http_response_types) == 1:
return next(iter(http_response_types))
except (OSError, TypeError):
...
return ""
return None
def _get_response_types(self, node):
types = set()
@@ -993,18 +939,7 @@ class RouteMixin(metaclass=SanicMeta):
def visit_Return(self, node: Return) -> Any:
nonlocal types
with suppress(AttributeError):
if node.value.func.id == "stream": # type: ignore
deprecation(
"The sanic.response.stream method has been "
"deprecated and will be removed in v22.6. Please "
"upgrade your application to use the new style "
"streaming pattern. See "
"https://sanicframework.org/en/guide/advanced/"
"streaming.html#response-streaming for more "
"information.",
22.6,
)
try:
checks = [node.value.func.id] # type: ignore
if node.value.keywords: # type: ignore
checks += [
@@ -1016,32 +951,9 @@ class RouteMixin(metaclass=SanicMeta):
for check in checks:
if check in RESPONSE_MAPPING:
types.add(RESPONSE_MAPPING[check])
except AttributeError:
...
HttpResponseVisitor().visit(node)
return types
def _build_route_context(self, raw):
ctx_kwargs = {
key.replace("ctx_", ""): raw.pop(key)
for key in {**raw}.keys()
if key.startswith("ctx_")
}
restricted = [
key for key in ctx_kwargs.keys() if key in RESTRICTED_ROUTE_CONTEXT
]
if restricted:
restricted_arguments = ", ".join(restricted)
raise AttributeError(
"Cannot use restricted route context: "
f"{restricted_arguments}. This limitation is only in place "
"until v22.3 when the restricted names will no longer be in"
"conflict. See https://github.com/sanic-org/sanic/issues/2303 "
"for more information."
)
if raw:
unexpected_arguments = ", ".join(raw.keys())
raise TypeError(
f"Unexpected keyword arguments: {unexpected_arguments}"
)
return HashableDict(ctx_kwargs)

View File

@@ -1,14 +1,16 @@
from enum import Enum
from typing import Any, Callable, Dict, Optional, Set, Union
from typing import Any, Callable, Dict, Optional, Set
from sanic.base.meta import SanicMeta
from sanic.models.futures import FutureSignal
from sanic.models.handler_types import SignalHandler
from sanic.signals import Signal
from sanic.types import HashableDict
class SignalMixin(metaclass=SanicMeta):
class HashableDict(dict):
def __hash__(self):
return hash(tuple(sorted(self.items())))
class SignalMixin:
def __init__(self, *args, **kwargs) -> None:
self._future_signals: Set[FutureSignal] = set()
@@ -17,11 +19,10 @@ class SignalMixin(metaclass=SanicMeta):
def signal(
self,
event: Union[str, Enum],
event: str,
*,
apply: bool = True,
condition: Dict[str, Any] = None,
exclusive: bool = True,
) -> Callable[[SignalHandler], SignalHandler]:
"""
For creating a signal handler, used similar to a route handler:
@@ -34,22 +35,19 @@ class SignalMixin(metaclass=SanicMeta):
:param event: Representation of the event in ``one.two.three`` form
:type event: str
:param apply: For lazy evaluation, defaults to ``True``
:param apply: For lazy evaluation, defaults to True
:type apply: bool, optional
:param condition: For use with the ``condition`` argument in dispatch
filtering, defaults to ``None``
:param exclusive: When ``True``, the signal can only be dispatched
when the condition has been met. When ``False``, the signal can
be dispatched either with or without it. *THIS IS INAPPLICABLE TO
BLUEPRINT SIGNALS. THEY ARE ALWAYS NON-EXCLUSIVE*, defaults
to ``True``
filtering, defaults to None
:type condition: Dict[str, Any], optional
"""
event_value = str(event.value) if isinstance(event, Enum) else event
def decorator(handler: SignalHandler):
nonlocal event
nonlocal apply
future_signal = FutureSignal(
handler, event_value, HashableDict(condition or {}), exclusive
handler, event, HashableDict(condition or {})
)
self._future_signals.add(future_signal)
@@ -65,7 +63,6 @@ class SignalMixin(metaclass=SanicMeta):
handler: Optional[Callable[..., Any]],
event: str,
condition: Dict[str, Any] = None,
exclusive: bool = True,
):
if not handler:
@@ -73,9 +70,7 @@ class SignalMixin(metaclass=SanicMeta):
...
handler = noop
self.signal(event=event, condition=condition, exclusive=exclusive)(
handler
)
self.signal(event=event, condition=condition)(handler)
return handler
def event(self, event: str):

View File

@@ -1,5 +1,4 @@
import asyncio
import sys
from typing import Any, Awaitable, Callable, MutableMapping, Optional, Union
@@ -15,20 +14,10 @@ ASGIReceive = Callable[[], Awaitable[ASGIMessage]]
class MockProtocol:
def __init__(self, transport: "MockTransport", loop):
# This should be refactored when < 3.8 support is dropped
self.transport = transport
# Fixup for 3.8+; Sanic still supports 3.7 where loop is required
loop = loop if sys.version_info[:2] < (3, 8) else None
# Optional in 3.9, necessary in 3.10 because the parameter "loop"
# was completely removed
if not loop:
self._not_paused = asyncio.Event()
self._not_paused.set()
self._complete = asyncio.Event()
else:
self._not_paused = asyncio.Event(loop=loop)
self._not_paused.set()
self._complete = asyncio.Event(loop=loop)
self._not_paused = asyncio.Event(loop=loop)
self._not_paused.set()
self._complete = asyncio.Event(loop=loop)
def pause_writing(self) -> None:
self._not_paused.clear()

View File

@@ -7,14 +7,13 @@ from sanic.models.handler_types import (
MiddlewareType,
SignalHandler,
)
from sanic.types import HashableDict
class FutureRoute(NamedTuple):
handler: str
uri: str
methods: Optional[Iterable[str]]
host: Union[str, List[str]]
host: str
strict_slashes: bool
stream: bool
version: Optional[int]
@@ -26,7 +25,6 @@ class FutureRoute(NamedTuple):
static: bool
version_prefix: str
error_format: Optional[str]
route_context: HashableDict
class FutureListener(NamedTuple):
@@ -62,8 +60,3 @@ class FutureSignal(NamedTuple):
handler: SignalHandler
event: str
condition: Optional[Dict[str, str]]
exclusive: bool
class FutureRegistry(set):
...

View File

@@ -1,6 +1,4 @@
from ssl import SSLObject
from types import SimpleNamespace
from typing import Any, Dict, Optional
from sanic.models.protocol_types import TransportProtocol
@@ -22,10 +20,8 @@ class ConnInfo:
"peername",
"server_port",
"server",
"server_name",
"sockname",
"ssl",
"cert",
)
def __init__(self, transport: TransportProtocol, unix=None):
@@ -35,16 +31,8 @@ class ConnInfo:
self.server_port = self.client_port = 0
self.client_ip = ""
self.sockname = addr = transport.get_extra_info("sockname")
self.ssl = False
self.server_name = ""
self.cert: Dict[str, Any] = {}
sslobj: Optional[SSLObject] = transport.get_extra_info(
"ssl_object"
) # type: ignore
if sslobj:
self.ssl = True
self.server_name = getattr(sslobj, "sanic_server_name", None) or ""
self.cert = dict(getattr(sslobj.context, "sanic", {}))
self.ssl: bool = bool(transport.get_extra_info("sslcontext"))
if isinstance(addr, str): # UNIX socket
self.server = unix or addr
return

View File

@@ -6,6 +6,9 @@ import sys
from time import sleep
from sanic.config import BASE_LOGO
from sanic.log import logger
def _iter_module_files():
"""This iterates over all relevant Python files.
@@ -47,19 +50,13 @@ def _get_args_for_reloading():
return [sys.executable] + sys.argv
def restart_with_reloader(changed=None):
def restart_with_reloader():
"""Create a new process and a subprocess in it with the same arguments as
this one.
"""
reloaded = ",".join(changed) if changed else ""
return subprocess.Popen(
_get_args_for_reloading(),
env={
**os.environ,
"SANIC_SERVER_RUNNING": "true",
"SANIC_RELOADER_PROCESS": "true",
"SANIC_RELOADED_FILES": reloaded,
},
env={**os.environ, "SANIC_SERVER_RUNNING": "true"},
)
@@ -94,29 +91,31 @@ def watchdog(sleep_interval, app):
worker_process = restart_with_reloader()
if app.config.LOGO:
logger.debug(
app.config.LOGO if isinstance(app.config.LOGO, str) else BASE_LOGO
)
try:
while True:
need_reload = False
changed = set()
for filename in itertools.chain(
_iter_module_files(),
*(d.glob("**/*") for d in app.reload_dirs),
):
try:
if _check_file(filename, mtimes):
path = (
filename
if isinstance(filename, str)
else filename.resolve()
)
changed.add(str(path))
check = _check_file(filename, mtimes)
except OSError:
continue
if changed:
if check:
need_reload = True
if need_reload:
worker_process.terminate()
worker_process.wait()
worker_process = restart_with_reloader(changed)
worker_process = restart_with_reloader()
sleep(sleep_interval)
except KeyboardInterrupt:

View File

@@ -15,9 +15,10 @@ from typing import (
from sanic_routing.route import Route # type: ignore
if TYPE_CHECKING: # no cov
if TYPE_CHECKING:
from sanic.server import ConnInfo
from sanic.app import Sanic
from sanic.http import Http
import email.utils
import uuid
@@ -31,7 +32,7 @@ from httptools import parse_url # type: ignore
from sanic.compat import CancelledErrors, Header
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.exceptions import InvalidUsage, ServerError
from sanic.exceptions import InvalidUsage
from sanic.headers import (
AcceptContainer,
Options,
@@ -41,7 +42,6 @@ from sanic.headers import (
parse_host,
parse_xforwarded,
)
from sanic.http import Http, Stage
from sanic.log import error_logger, logger
from sanic.models.protocol_types import TransportProtocol
from sanic.response import BaseHTTPResponse, HTTPResponse
@@ -104,7 +104,6 @@ class Request:
"parsed_json",
"parsed_forwarded",
"raw_url",
"responded",
"request_middleware_started",
"route",
"stream",
@@ -156,7 +155,6 @@ class Request:
self.stream: Optional[Http] = None
self.route: Optional[Route] = None
self._protocol = None
self.responded: bool = False
def __repr__(self):
class_name = self.__class__.__name__
@@ -166,21 +164,6 @@ class Request:
def generate_id(*_):
return uuid.uuid4()
def reset_response(self):
try:
if (
self.stream is not None
and self.stream.stage is not Stage.HANDLER
):
raise ServerError(
"Cannot reset response because previous response was sent."
)
self.stream.response.stream = None
self.stream.response = None
self.responded = False
except AttributeError:
pass
async def respond(
self,
response: Optional[BaseHTTPResponse] = None,
@@ -189,19 +172,13 @@ class Request:
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
):
try:
if self.stream is not None and self.stream.response:
raise ServerError("Second respond call is not allowed.")
except AttributeError:
pass
# This logic of determining which response to use is subject to change
if response is None:
response = HTTPResponse(
response = (self.stream and self.stream.response) or HTTPResponse(
status=status,
headers=headers,
content_type=content_type,
)
# Connect the response
if isinstance(response, BaseHTTPResponse) and self.stream:
response = self.stream.respond(response)
@@ -216,7 +193,6 @@ class Request:
error_logger.exception(
"Exception occurred in one of response middleware handlers"
)
self.responded = True
return response
async def receive_body(self):
@@ -784,10 +760,9 @@ def parse_multipart_form(body, boundary):
break
colon_index = form_line.index(":")
idx = colon_index + 2
form_header_field = form_line[0:colon_index].lower()
form_header_value, form_parameters = parse_content_header(
form_line[idx:]
form_line[colon_index + 2 :]
)
if form_header_field == "content-disposition":

View File

@@ -1,11 +1,8 @@
from __future__ import annotations
from functools import partial
from mimetypes import guess_type
from os import path
from pathlib import PurePath
from typing import (
TYPE_CHECKING,
Any,
AnyStr,
Callable,
@@ -14,27 +11,19 @@ from typing import (
Iterator,
Optional,
Tuple,
TypeVar,
Union,
)
from urllib.parse import quote_plus
from warnings import warn
from sanic.compat import Header, open_async
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE
from sanic.cookies import CookieJar
from sanic.exceptions import SanicException, ServerError
from sanic.helpers import has_message_body, remove_entity_headers
from sanic.http import Http
from sanic.models.protocol_types import HTMLProtocol, Range
if TYPE_CHECKING:
from sanic.asgi import ASGIApp
from sanic.request import Request
else:
Request = TypeVar("Request")
try:
from ujson import dumps as json_dumps
except ImportError:
@@ -56,7 +45,7 @@ class BaseHTTPResponse:
self.asgi: bool = False
self.body: Optional[bytes] = None
self.content_type: Optional[str] = None
self.stream: Optional[Union[Http, ASGIApp]] = None
self.stream: Http = None
self.status: int = None
self.headers = Header({})
self._cookies: Optional[CookieJar] = None
@@ -112,7 +101,7 @@ class BaseHTTPResponse:
async def send(
self,
data: Optional[AnyStr] = None,
data: Optional[Union[AnyStr]] = None,
end_stream: Optional[bool] = None,
) -> None:
"""
@@ -123,17 +112,8 @@ class BaseHTTPResponse:
"""
if data is None and end_stream is None:
end_stream = True
if self.stream is None:
raise SanicException(
"No stream is connected to the response object instance."
)
if self.stream.send is None:
if end_stream and not data:
return
raise ServerError(
"Response stream was ended, no more response data is "
"allowed to be sent."
)
if end_stream and not data and self.stream.send is None:
return
data = (
data.encode() # type: ignore
if hasattr(data, "encode")
@@ -142,6 +122,95 @@ class BaseHTTPResponse:
await self.stream.send(data, end_stream=end_stream)
StreamingFunction = Callable[[BaseHTTPResponse], Coroutine[Any, Any, None]]
class StreamingHTTPResponse(BaseHTTPResponse):
"""
Old style streaming response where you pass a streaming function:
.. code-block:: python
async def sample_streaming_fn(response):
await response.write("foo")
await asyncio.sleep(1)
await response.write("bar")
await asyncio.sleep(1)
@app.post("/")
async def test(request):
return stream(sample_streaming_fn)
.. warning::
**Deprecated** and set for removal in v21.12. You can now achieve the
same functionality without a callback.
.. code-block:: python
@app.post("/")
async def test(request):
response = await request.respond()
await response.send("foo", False)
await asyncio.sleep(1)
await response.send("bar", False)
await asyncio.sleep(1)
await response.send("", True)
return response
"""
__slots__ = (
"streaming_fn",
"status",
"content_type",
"headers",
"_cookies",
)
def __init__(
self,
streaming_fn: StreamingFunction,
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: str = "text/plain; charset=utf-8",
ignore_deprecation_notice: bool = False,
):
if not ignore_deprecation_notice:
warn(
"Use of the StreamingHTTPResponse is deprecated in v21.6, and "
"will be removed in v21.12. Please upgrade your streaming "
"response implementation. You can learn more here: "
"https://sanicframework.org/en/guide/advanced/streaming.html"
"#response-streaming. If you use the builtin stream() or "
"file_stream() methods, this upgrade will be be done for you."
)
super().__init__()
self.content_type = content_type
self.streaming_fn = streaming_fn
self.status = status
self.headers = Header(headers or {})
self._cookies = None
async def write(self, data):
"""Writes a chunk of data to the streaming response.
:param data: str or bytes-ish data to be written.
"""
await super().send(self._encode_body(data))
async def send(self, *args, **kwargs):
if self.streaming_fn is not None:
await self.streaming_fn(self)
self.streaming_fn = None
await super().send(*args, **kwargs)
async def eof(self):
raise NotImplementedError
class HTTPResponse(BaseHTTPResponse):
"""
HTTP response to be sent back to the client.
@@ -336,109 +405,6 @@ async def file(
)
def redirect(
to: str,
headers: Optional[Dict[str, str]] = None,
status: int = 302,
content_type: str = "text/html; charset=utf-8",
) -> HTTPResponse:
"""
Abort execution and cause a 302 redirect (by default) by setting a
Location header.
:param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302
:param content_type: the content type (string) of the response
"""
headers = headers or {}
# URL Quote the URL before redirecting
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
# According to RFC 7231, a relative URI is now permitted.
headers["Location"] = safe_to
return HTTPResponse(
status=status, headers=headers, content_type=content_type
)
class ResponseStream:
"""
ResponseStream is a compat layer to bridge the gap after the deprecation
of StreamingHTTPResponse. In v22.6 it will be removed when:
- stream is removed
- file_stream is moved to new style streaming
- file and file_stream are combined into a single API
"""
__slots__ = (
"_cookies",
"content_type",
"headers",
"request",
"response",
"status",
"streaming_fn",
)
def __init__(
self,
streaming_fn: Callable[
[Union[BaseHTTPResponse, ResponseStream]],
Coroutine[Any, Any, None],
],
status: int = 200,
headers: Optional[Union[Header, Dict[str, str]]] = None,
content_type: Optional[str] = None,
):
self.streaming_fn = streaming_fn
self.status = status
self.headers = headers or Header()
self.content_type = content_type
self.request: Optional[Request] = None
self._cookies: Optional[CookieJar] = None
async def write(self, message: str):
await self.response.send(message)
async def stream(self) -> HTTPResponse:
if not self.request:
raise ServerError("Attempted response to unknown request")
self.response = await self.request.respond(
headers=self.headers,
status=self.status,
content_type=self.content_type,
)
await self.streaming_fn(self)
return self.response
async def eof(self) -> None:
await self.response.eof()
@property
def cookies(self) -> CookieJar:
if self._cookies is None:
self._cookies = CookieJar(self.headers)
return self._cookies
@property
def processed_headers(self):
return self.response.processed_headers
@property
def body(self):
return self.response.body
def __call__(self, request: Request) -> ResponseStream:
self.request = request
return self
def __await__(self):
return self.stream().__await__()
async def file_stream(
location: Union[str, PurePath],
status: int = 200,
@@ -447,7 +413,7 @@ async def file_stream(
headers: Optional[Dict[str, str]] = None,
filename: Optional[str] = None,
_range: Optional[Range] = None,
) -> ResponseStream:
) -> StreamingHTTPResponse:
"""Return a streaming response object with file data.
:param location: Location of file on system.
@@ -455,6 +421,7 @@ async def file_stream(
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
:param filename: Override filename.
:param chunked: Deprecated
:param _range:
"""
headers = headers or {}
@@ -490,24 +457,23 @@ async def file_stream(
break
await response.write(content)
return ResponseStream(
return StreamingHTTPResponse(
streaming_fn=_streaming_fn,
status=status,
headers=headers,
content_type=mime_type,
ignore_deprecation_notice=True,
)
def stream(
streaming_fn: Callable[
[Union[BaseHTTPResponse, ResponseStream]], Coroutine[Any, Any, None]
],
streaming_fn: StreamingFunction,
status: int = 200,
headers: Optional[Dict[str, str]] = None,
content_type: str = "text/plain; charset=utf-8",
) -> ResponseStream:
"""Accepts a coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `ResponseStream`.
):
"""Accepts an coroutine `streaming_fn` which can be used to
write chunks to a streaming response. Returns a `StreamingHTTPResponse`.
Example usage::
@@ -521,13 +487,42 @@ def stream(
:param streaming_fn: A coroutine accepts a response and
writes content to that response.
:param status: HTTP status.
:param content_type: Specific content_type.
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
:param chunked: Deprecated
"""
return ResponseStream(
return StreamingHTTPResponse(
streaming_fn,
headers=headers,
content_type=content_type,
status=status,
ignore_deprecation_notice=True,
)
def redirect(
to: str,
headers: Optional[Dict[str, str]] = None,
status: int = 302,
content_type: str = "text/html; charset=utf-8",
) -> HTTPResponse:
"""
Abort execution and cause a 302 redirect (by default) by setting a
Location header.
:param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302
:param content_type: the content type (string) of the response
"""
headers = headers or {}
# URL Quote the URL before redirecting
safe_to = quote_plus(to, safe=":/%#?&=@[]!$&'()*+,;")
# According to RFC 7231, a relative URI is now permitted.
headers["Location"] = safe_to
return HTTPResponse(
status=status, headers=headers, content_type=content_type
)

View File

@@ -54,7 +54,7 @@ class Router(BaseRouter):
self, path: str, method: str, host: Optional[str]
) -> Tuple[Route, RouteHandler, Dict[str, Any]]:
"""
Retrieve a `Route` object containing the details about how to handle
Retrieve a `Route` object containg the details about how to handle
a response for a given request
:param request: the incoming request object

View File

@@ -1,10 +1,20 @@
import asyncio
from sanic.models.server_types import ConnInfo, Signal
from sanic.server.async_server import AsyncioServer
from sanic.server.loop import try_use_uvloop
from sanic.server.protocols.http_protocol import HttpProtocol
from sanic.server.runners import serve, serve_multiple, serve_single
try:
import uvloop # type: ignore
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
except ImportError:
pass
__all__ = (
"AsyncioServer",
"ConnInfo",
@@ -13,5 +23,4 @@ __all__ = (
"serve",
"serve_multiple",
"serve_single",
"try_use_uvloop",
)

View File

@@ -2,14 +2,7 @@ from __future__ import annotations
import asyncio
from typing import TYPE_CHECKING
from sanic.exceptions import SanicException
from sanic.log import deprecation
if TYPE_CHECKING:
from sanic import Sanic
class AsyncioServer:
@@ -18,11 +11,11 @@ class AsyncioServer:
a user who needs to manage the server lifecycle manually.
"""
__slots__ = ("app", "connections", "loop", "serve_coro", "server")
__slots__ = ("app", "connections", "loop", "serve_coro", "server", "init")
def __init__(
self,
app: Sanic,
app,
loop,
serve_coro,
connections,
@@ -34,20 +27,13 @@ class AsyncioServer:
self.loop = loop
self.serve_coro = serve_coro
self.server = None
@property
def init(self):
deprecation(
"AsyncioServer.init has been deprecated and will be removed "
"in v22.6. Use Sanic.state.is_started instead.",
22.6,
)
return self.app.state.is_started
self.init = False
def startup(self):
"""
Trigger "before_server_start" events
"""
self.init = True
return self.app._startup()
def before_start(self):
@@ -91,33 +77,30 @@ class AsyncioServer:
return task
def start_serving(self):
return self._serve(self.server.start_serving)
def serve_forever(self):
return self._serve(self.server.serve_forever)
def _serve(self, serve_func):
if self.server:
if not self.app.state.is_started:
raise SanicException(
"Cannot run Sanic server without first running "
"await server.startup()"
try:
return self.server.start_serving()
except AttributeError:
raise NotImplementedError(
"server.start_serving not available in this version "
"of asyncio or uvloop."
)
def serve_forever(self):
if self.server:
try:
return serve_func()
return self.server.serve_forever()
except AttributeError:
name = serve_func.__name__
raise NotImplementedError(
f"server.{name} not available in this version "
"server.serve_forever not available in this version "
"of asyncio or uvloop."
)
def _server_event(self, concern: str, action: str):
if not self.app.state.is_started:
if not self.init:
raise SanicException(
"Cannot dispatch server event without "
"first running await server.startup()"
"first running server.startup()"
)
return self.app._server_event(concern, action, loop=self.loop)

View File

@@ -1,49 +0,0 @@
import asyncio
from distutils.util import strtobool
from os import getenv
from sanic.compat import OS_IS_WINDOWS
from sanic.log import error_logger
def try_use_uvloop() -> None:
"""
Use uvloop instead of the default asyncio loop.
"""
if OS_IS_WINDOWS:
error_logger.warning(
"You are trying to use uvloop, but uvloop is not compatible "
"with your system. You can disable uvloop completely by setting "
"the 'USE_UVLOOP' configuration value to false, or simply not "
"defining it and letting Sanic handle it for you. Sanic will now "
"continue to run using the default event loop."
)
return
try:
import uvloop # type: ignore
except ImportError:
error_logger.warning(
"You are trying to use uvloop, but uvloop is not "
"installed in your system. In order to use uvloop "
"you must first install it. Otherwise, you can disable "
"uvloop completely by setting the 'USE_UVLOOP' "
"configuration value to false. Sanic will now continue "
"to run with the default event loop."
)
return
uvloop_install_removed = strtobool(getenv("SANIC_NO_UVLOOP", "no"))
if uvloop_install_removed:
error_logger.info(
"You are requesting to run Sanic using uvloop, but the "
"install-time 'SANIC_NO_UVLOOP' environment variable (used to "
"opt-out of installing uvloop with Sanic) is set to true. If "
"you want to prevent Sanic from overriding the event loop policy "
"during runtime, set the 'USE_UVLOOP' configuration value to "
"false."
)
if not isinstance(asyncio.get_event_loop_policy(), uvloop.EventLoopPolicy):
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

View File

@@ -1,11 +1,10 @@
from typing import TYPE_CHECKING, Optional, Sequence, cast
from typing import TYPE_CHECKING, Optional, Sequence
from websockets.connection import CLOSED, CLOSING, OPEN
from websockets.server import ServerConnection
from websockets.typing import Subprotocol
from sanic.exceptions import ServerError
from sanic.log import deprecation, error_logger
from sanic.log import error_logger
from sanic.server import HttpProtocol
from ..websockets.impl import WebsocketImplProtocol
@@ -16,13 +15,12 @@ if TYPE_CHECKING:
class WebSocketProtocol(HttpProtocol):
__slots__ = (
"websocket",
"websocket_timeout",
"websocket_max_size",
"websocket_ping_interval",
"websocket_ping_timeout",
)
websocket: Optional[WebsocketImplProtocol]
websocket_timeout: float
websocket_max_size = Optional[int]
websocket_ping_interval = Optional[float]
websocket_ping_timeout = Optional[float]
def __init__(
self,
@@ -37,29 +35,32 @@ class WebSocketProtocol(HttpProtocol):
**kwargs,
):
super().__init__(*args, **kwargs)
self.websocket: Optional[WebsocketImplProtocol] = None
self.websocket = None
self.websocket_timeout = websocket_timeout
self.websocket_max_size = websocket_max_size
if websocket_max_queue is not None and websocket_max_queue > 0:
# TODO: Reminder remove this warning in v22.3
deprecation(
"Websocket no longer uses queueing, so websocket_max_queue"
" is no longer required.",
22.3,
error_logger.warning(
DeprecationWarning(
"Websocket no longer uses queueing, so websocket_max_queue"
" is no longer required."
)
)
if websocket_read_limit is not None and websocket_read_limit > 0:
# TODO: Reminder remove this warning in v22.3
deprecation(
"Websocket no longer uses read buffers, so "
"websocket_read_limit is not required.",
22.3,
error_logger.warning(
DeprecationWarning(
"Websocket no longer uses read buffers, so "
"websocket_read_limit is not required."
)
)
if websocket_write_limit is not None and websocket_write_limit > 0:
# TODO: Reminder remove this warning in v22.3
deprecation(
"Websocket no longer uses write buffers, so "
"websocket_write_limit is not required.",
22.3,
error_logger.warning(
DeprecationWarning(
"Websocket no longer uses write buffers, so "
"websocket_write_limit is not required."
)
)
self.websocket_ping_interval = websocket_ping_interval
self.websocket_ping_timeout = websocket_ping_timeout
@@ -108,22 +109,14 @@ class WebSocketProtocol(HttpProtocol):
return super().close_if_idle()
async def websocket_handshake(
self, request, subprotocols: Optional[Sequence[str]] = None
self, request, subprotocols=Optional[Sequence[str]]
):
# let the websockets package do the handshake with the client
try:
if subprotocols is not None:
# subprotocols can be a set or frozenset,
# but ServerConnection needs a list
subprotocols = cast(
Optional[Sequence[Subprotocol]],
list(
[
Subprotocol(subprotocol)
for subprotocol in subprotocols
]
),
)
subprotocols = list(subprotocols)
ws_conn = ServerConnection(
max_size=self.websocket_max_size,
subprotocols=subprotocols,
@@ -138,18 +131,21 @@ class WebSocketProtocol(HttpProtocol):
)
raise ServerError(msg, status_code=500)
if 100 <= resp.status_code <= 299:
first_line = (
f"HTTP/1.1 {resp.status_code} {resp.reason_phrase}\r\n"
).encode()
rbody = bytearray(first_line)
rbody += (
"".join([f"{k}: {v}\r\n" for k, v in resp.headers.items()])
).encode()
rbody += b"\r\n"
rbody = "".join(
[
"HTTP/1.1 ",
str(resp.status_code),
" ",
resp.reason_phrase,
"\r\n",
]
)
rbody += "".join(f"{k}: {v}\r\n" for k, v in resp.headers.items())
if resp.body is not None:
rbody += resp.body
rbody += b"\r\n\r\n"
await super().send(rbody)
rbody += f"\r\n{resp.body}\r\n\r\n"
else:
rbody += "\r\n"
await super().send(rbody.encode())
else:
raise ServerError(resp.body, resp.status_code)
self.websocket = WebsocketImplProtocol(

View File

@@ -1,7 +1,5 @@
from __future__ import annotations
import sys
from ssl import SSLContext
from typing import TYPE_CHECKING, Dict, Optional, Type, Union
@@ -21,7 +19,6 @@ from functools import partial
from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func
from sanic.application.ext import setup_ext
from sanic.compat import OS_IS_WINDOWS, ctrlc_workaround_for_windows
from sanic.log import error_logger, logger
from sanic.models.server_types import Signal
@@ -117,7 +114,6 @@ def serve(
**asyncio_server_kwargs,
)
setup_ext(app)
if run_async:
return AsyncioServer(
app=app,
@@ -138,7 +134,6 @@ def serve(
# Ignore SIGINT when run_multiple
if run_multiple:
signal_func(SIGINT, SIG_IGN)
os.environ["SANIC_WORKER_PROCESS"] = "true"
# Register signals for graceful termination
if register_sys_signals:
@@ -178,9 +173,6 @@ def serve(
loop.run_until_complete(asyncio.sleep(0.1))
start_shutdown = start_shutdown + 0.1
if sys.version_info > (3, 7):
app.shutdown_tasks(graceful - start_shutdown)
# Force close non-idle connection after waiting for
# graceful_shutdown_timeout
for conn in connections:
@@ -189,6 +181,7 @@ def serve(
else:
conn.abort()
loop.run_until_complete(app._server_event("shutdown", "after"))
remove_unix_socket(unix)
@@ -256,10 +249,7 @@ def serve_multiple(server_settings, workers):
mp = multiprocessing.get_context("fork")
for _ in range(workers):
process = mp.Process(
target=serve,
kwargs=server_settings,
)
process = mp.Process(target=serve, kwargs=server_settings)
process.daemon = True
process.start()
processes.append(process)

View File

@@ -2,9 +2,8 @@ from __future__ import annotations
import asyncio
from enum import Enum
from inspect import isawaitable
from typing import Any, Dict, List, Optional, Tuple, Union, cast
from typing import Any, Dict, List, Optional, Tuple, Union
from sanic_routing import BaseRouter, Route, RouteGroup # type: ignore
from sanic_routing.exceptions import NotFound # type: ignore
@@ -15,47 +14,29 @@ from sanic.log import error_logger, logger
from sanic.models.handler_types import SignalHandler
class Event(Enum):
SERVER_INIT_AFTER = "server.init.after"
SERVER_INIT_BEFORE = "server.init.before"
SERVER_SHUTDOWN_AFTER = "server.shutdown.after"
SERVER_SHUTDOWN_BEFORE = "server.shutdown.before"
HTTP_LIFECYCLE_BEGIN = "http.lifecycle.begin"
HTTP_LIFECYCLE_COMPLETE = "http.lifecycle.complete"
HTTP_LIFECYCLE_EXCEPTION = "http.lifecycle.exception"
HTTP_LIFECYCLE_HANDLE = "http.lifecycle.handle"
HTTP_LIFECYCLE_READ_BODY = "http.lifecycle.read_body"
HTTP_LIFECYCLE_READ_HEAD = "http.lifecycle.read_head"
HTTP_LIFECYCLE_REQUEST = "http.lifecycle.request"
HTTP_LIFECYCLE_RESPONSE = "http.lifecycle.response"
HTTP_ROUTING_AFTER = "http.routing.after"
HTTP_ROUTING_BEFORE = "http.routing.before"
HTTP_LIFECYCLE_SEND = "http.lifecycle.send"
HTTP_MIDDLEWARE_AFTER = "http.middleware.after"
HTTP_MIDDLEWARE_BEFORE = "http.middleware.before"
RESERVED_NAMESPACES = {
"server": (
Event.SERVER_INIT_AFTER.value,
Event.SERVER_INIT_BEFORE.value,
Event.SERVER_SHUTDOWN_AFTER.value,
Event.SERVER_SHUTDOWN_BEFORE.value,
# "server.main.start",
# "server.main.stop",
"server.init.before",
"server.init.after",
"server.shutdown.before",
"server.shutdown.after",
),
"http": (
Event.HTTP_LIFECYCLE_BEGIN.value,
Event.HTTP_LIFECYCLE_COMPLETE.value,
Event.HTTP_LIFECYCLE_EXCEPTION.value,
Event.HTTP_LIFECYCLE_HANDLE.value,
Event.HTTP_LIFECYCLE_READ_BODY.value,
Event.HTTP_LIFECYCLE_READ_HEAD.value,
Event.HTTP_LIFECYCLE_REQUEST.value,
Event.HTTP_LIFECYCLE_RESPONSE.value,
Event.HTTP_ROUTING_AFTER.value,
Event.HTTP_ROUTING_BEFORE.value,
Event.HTTP_LIFECYCLE_SEND.value,
Event.HTTP_MIDDLEWARE_AFTER.value,
Event.HTTP_MIDDLEWARE_BEFORE.value,
"http.lifecycle.begin",
"http.lifecycle.complete",
"http.lifecycle.exception",
"http.lifecycle.handle",
"http.lifecycle.read_body",
"http.lifecycle.read_head",
"http.lifecycle.request",
"http.lifecycle.response",
"http.routing.after",
"http.routing.before",
"http.lifecycle.send",
"http.middleware.after",
"http.middleware.before",
),
}
@@ -132,7 +113,7 @@ class SignalRouter(BaseRouter):
if fail_not_found:
raise e
else:
if self.ctx.app.debug and self.ctx.app.state.verbosity >= 1:
if self.ctx.app.debug:
error_logger.warning(str(e))
return None
@@ -142,21 +123,12 @@ class SignalRouter(BaseRouter):
if context:
params.update(context)
signals = group.routes
if not reverse:
signals = signals[::-1]
handlers = handlers[::-1]
try:
for signal in signals:
params.pop("__trigger__", None)
if (
(condition is None and signal.ctx.exclusive is False)
or (
condition is None
and not signal.handler.__requirements__
)
or (condition == signal.handler.__requirements__)
) and (signal.ctx.trigger or event == signal.ctx.definition):
maybe_coroutine = signal.handler(**params)
for handler in handlers:
if condition is None or condition == handler.__requirements__:
maybe_coroutine = handler(**params)
if isawaitable(maybe_coroutine):
retval = await maybe_coroutine
if retval:
@@ -199,36 +171,23 @@ class SignalRouter(BaseRouter):
handler: SignalHandler,
event: str,
condition: Optional[Dict[str, Any]] = None,
exclusive: bool = True,
) -> Signal:
event_definition = event
parts = self._build_event_parts(event)
if parts[2].startswith("<"):
name = ".".join([*parts[:-1], "*"])
trigger = self._clean_trigger(parts[2])
else:
name = event
trigger = ""
if not trigger:
event = ".".join([*parts[:2], "<__trigger__>"])
handler.__requirements__ = condition # type: ignore
handler.__trigger__ = trigger # type: ignore
signal = super().add(
return super().add(
event,
handler,
requirements=condition,
name=name,
append=True,
) # type: ignore
signal.ctx.exclusive = exclusive
signal.ctx.trigger = trigger
signal.ctx.definition = event_definition
return cast(Signal, signal)
def finalize(self, do_compile: bool = True, do_optimize: bool = False):
self.add(_blank, "sanic.__signal__.__init__")
@@ -260,9 +219,3 @@ class SignalRouter(BaseRouter):
"Cannot declare reserved signal event: %s" % event
)
return parts
def _clean_trigger(self, trigger: str) -> str:
trigger = trigger[1:-1]
if ":" in trigger:
trigger, _ = trigger.split(":")
return trigger

View File

@@ -1,196 +0,0 @@
import os
import ssl
from typing import Iterable, Optional, Union
from sanic.log import logger
# Only allow secure ciphers, notably leaving out AES-CBC mode
# OpenSSL chooses ECDSA or RSA depending on the cert in use
CIPHERS_TLS12 = [
"ECDHE-ECDSA-CHACHA20-POLY1305",
"ECDHE-ECDSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-CHACHA20-POLY1305",
"ECDHE-RSA-AES256-GCM-SHA384",
"ECDHE-RSA-AES128-GCM-SHA256",
]
def create_context(
certfile: Optional[str] = None,
keyfile: Optional[str] = None,
password: Optional[str] = None,
) -> ssl.SSLContext:
"""Create a context with secure crypto and HTTP/1.1 in protocols."""
context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
context.minimum_version = ssl.TLSVersion.TLSv1_2
context.set_ciphers(":".join(CIPHERS_TLS12))
context.set_alpn_protocols(["http/1.1"])
context.sni_callback = server_name_callback
if certfile and keyfile:
context.load_cert_chain(certfile, keyfile, password)
return context
def shorthand_to_ctx(
ctxdef: Union[None, ssl.SSLContext, dict, str]
) -> Optional[ssl.SSLContext]:
"""Convert an ssl argument shorthand to an SSLContext object."""
if ctxdef is None or isinstance(ctxdef, ssl.SSLContext):
return ctxdef
if isinstance(ctxdef, str):
return load_cert_dir(ctxdef)
if isinstance(ctxdef, dict):
return CertSimple(**ctxdef)
raise ValueError(
f"Invalid ssl argument {type(ctxdef)}."
" Expecting a list of certdirs, a dict or an SSLContext."
)
def process_to_context(
ssldef: Union[None, ssl.SSLContext, dict, str, list, tuple]
) -> Optional[ssl.SSLContext]:
"""Process app.run ssl argument from easy formats to full SSLContext."""
return (
CertSelector(map(shorthand_to_ctx, ssldef))
if isinstance(ssldef, (list, tuple))
else shorthand_to_ctx(ssldef)
)
def load_cert_dir(p: str) -> ssl.SSLContext:
if os.path.isfile(p):
raise ValueError(f"Certificate folder expected but {p} is a file.")
keyfile = os.path.join(p, "privkey.pem")
certfile = os.path.join(p, "fullchain.pem")
if not os.access(keyfile, os.R_OK):
raise ValueError(
f"Certificate not found or permission denied {keyfile}"
)
if not os.access(certfile, os.R_OK):
raise ValueError(
f"Certificate not found or permission denied {certfile}"
)
return CertSimple(certfile, keyfile)
class CertSimple(ssl.SSLContext):
"""A wrapper for creating SSLContext with a sanic attribute."""
def __new__(cls, cert, key, **kw):
# try common aliases, rename to cert/key
certfile = kw["cert"] = kw.pop("certificate", None) or cert
keyfile = kw["key"] = kw.pop("keyfile", None) or key
password = kw.pop("password", None)
if not certfile or not keyfile:
raise ValueError("SSL dict needs filenames for cert and key.")
subject = {}
if "names" not in kw:
cert = ssl._ssl._test_decode_cert(certfile) # type: ignore
kw["names"] = [
name
for t, name in cert["subjectAltName"]
if t in ["DNS", "IP Address"]
]
subject = {k: v for item in cert["subject"] for k, v in item}
self = create_context(certfile, keyfile, password)
self.__class__ = cls
self.sanic = {**subject, **kw}
return self
def __init__(self, cert, key, **kw):
pass # Do not call super().__init__ because it is already initialized
class CertSelector(ssl.SSLContext):
"""Automatically select SSL certificate based on the hostname that the
client is trying to access, via SSL SNI. Paths to certificate folders
with privkey.pem and fullchain.pem in them should be provided, and
will be matched in the order given whenever there is a new connection.
"""
def __new__(cls, ctxs):
return super().__new__(cls)
def __init__(self, ctxs: Iterable[Optional[ssl.SSLContext]]):
super().__init__()
self.sni_callback = selector_sni_callback # type: ignore
self.sanic_select = []
self.sanic_fallback = None
all_names = []
for i, ctx in enumerate(ctxs):
if not ctx:
continue
names = dict(getattr(ctx, "sanic", {})).get("names", [])
all_names += names
self.sanic_select.append(ctx)
if i == 0:
self.sanic_fallback = ctx
if not all_names:
raise ValueError(
"No certificates with SubjectAlternativeNames found."
)
logger.info(f"Certificate vhosts: {', '.join(all_names)}")
def find_cert(self: CertSelector, server_name: str):
"""Find the first certificate that matches the given SNI.
:raises ssl.CertificateError: No matching certificate found.
:return: A matching ssl.SSLContext object if found."""
if not server_name:
if self.sanic_fallback:
return self.sanic_fallback
raise ValueError(
"The client provided no SNI to match for certificate."
)
for ctx in self.sanic_select:
if match_hostname(ctx, server_name):
return ctx
if self.sanic_fallback:
return self.sanic_fallback
raise ValueError(f"No certificate found matching hostname {server_name!r}")
def match_hostname(
ctx: Union[ssl.SSLContext, CertSelector], hostname: str
) -> bool:
"""Match names from CertSelector against a received hostname."""
# Local certs are considered trusted, so this can be less pedantic
# and thus faster than the deprecated ssl.match_hostname function is.
names = dict(getattr(ctx, "sanic", {})).get("names", [])
hostname = hostname.lower()
for name in names:
if name.startswith("*."):
if hostname.split(".", 1)[-1] == name[2:]:
return True
elif name == hostname:
return True
return False
def selector_sni_callback(
sslobj: ssl.SSLObject, server_name: str, ctx: CertSelector
) -> Optional[int]:
"""Select a certificate matching the SNI."""
# Call server_name_callback to store the SNI on sslobj
server_name_callback(sslobj, server_name, ctx)
# Find a new context matching the hostname
try:
sslobj.context = find_cert(ctx, server_name)
except ValueError as e:
logger.warning(f"Rejecting TLS connection: {e}")
# This would show ERR_SSL_UNRECOGNIZED_NAME_ALERT on client side if
# asyncio/uvloop did proper SSL shutdown. They don't.
return ssl.ALERT_DESCRIPTION_UNRECOGNIZED_NAME
return None # mypy complains without explicit return
def server_name_callback(
sslobj: ssl.SSLObject, server_name: str, ctx: ssl.SSLContext
) -> None:
"""Store the received SNI as sslobj.sanic_server_name."""
sslobj.sanic_server_name = server_name # type: ignore

View File

@@ -1,10 +1,9 @@
from sanic.base.meta import SanicMeta
from sanic.exceptions import SanicException
from .service import TouchUp
class TouchUpMeta(SanicMeta):
class TouchUpMeta(type):
def __new__(cls, name, bases, attrs, **kwargs):
gen_class = super().__new__(cls, name, bases, attrs, **kwargs)

View File

@@ -22,9 +22,7 @@ class OptionalDispatchEvent(BaseScheme):
raw_source = getsource(method)
src = dedent(raw_source)
tree = parse(src)
node = RemoveDispatch(
self._registered_events, self.app.state.verbosity
).visit(tree)
node = RemoveDispatch(self._registered_events).visit(tree)
compiled_src = compile(node, method.__name__, "exec")
exec_locals: Dict[str, Any] = {}
exec(compiled_src, module_globals, exec_locals) # nosec
@@ -33,9 +31,8 @@ class OptionalDispatchEvent(BaseScheme):
class RemoveDispatch(NodeTransformer):
def __init__(self, registered_events, verbosity: int = 0) -> None:
def __init__(self, registered_events) -> None:
self._registered_events = registered_events
self._verbosity = verbosity
def visit_Expr(self, node: Expr) -> Any:
call = node.value
@@ -52,8 +49,7 @@ class RemoveDispatch(NodeTransformer):
if hasattr(event, "s"):
event_name = getattr(event, "value", event.s)
if self._not_registered(event_name):
if self._verbosity >= 2:
logger.debug(f"Disabling event: {event_name}")
logger.debug(f"Disabling event: {event_name}")
return None
return node

Some files were not shown because too many files have changed in this diff Show More