sanic-org / sanic-testing Goto Github PK
View Code? Open in Web Editor NEWTest clients for Sanic
Home Page: https://sanic.dev/en/plugins/sanic-testing/getting-started.html
License: MIT License
Test clients for Sanic
Home Page: https://sanic.dev/en/plugins/sanic-testing/getting-started.html
License: MIT License
ERROR sanic.error:handlers.py:146 Exception occurred while handling uri: 'http://mockserver:1234/api/v3/projects/fourth-memento-308807-test2/locations/cn/agents'
Traceback (most recent call last):
File "xx/.venv/lib/python3.8/site-packages/sanic/app.py", line 749, in handle_request
response = await self._run_request_middleware(
File "xx/.venv/lib/python3.8/site-packages/sanic/app.py", line 1116, in _run_request_middleware
response = await response
File "xx/tests/api/base.py", line 68, in load_request_ctx
request.ctx.engine = AIOEngine(request.app.ctx.mongo_client, database=request.ctx.database_name)
AttributeError: 'types.SimpleNamespace' object has no attribute 'mongo_client'
=============================== warnings summary ===============================
tests/api/test_agent.py::test_agent_list_get
xx/.venv/lib/python3.8/site-packages/sanic/asgi.py:22: UserWarning: You have set a listener for "before_server_start" in ASGI mode. It will be executed as early as possible, but not before the ASGI server is started.
warnings.warn(
When I used async test, I encountered the above error, but there was no problem when it was sync test.
I have a before_server_start
listener to add mongo motor.
Hi,
It looks like sanic
and sanic-testing
can't be co-installed on Python 3.6, I assume because sanic 20.12 requires httpx==0.15.4
but there's no version of sanic-testing
that supports a version that old?
I can apply the obvious workaround to either stop using such an old version of Python, or use "sanic_testing; python_version > '3.6'",
and some conditional importing to make it work, but it might be worth dropping the 3.6 classifier from https://github.com/sanic-org/sanic-testing/blob/main/setup.py#L44 to make it more obvious it isn't going to work?
Cheers!
❯ python3 --version
Python 3.6.12
❯ python3 -m venv testvenv
❯ source testvenv/bin/activate
❯ pip install -U pip
Looking in indexes: https://pypi.org/simple, https://artifactory.virt.ch.bbc.co.uk/artifactory/api/pypi/ap-python/simple
Collecting pip
Using cached https://files.pythonhosted.org/packages/ca/31/b88ef447d595963c01060998cb329251648acf4a067721b0452c45527eb8/pip-21.2.4-py3-none-any.whl
Installing collected packages: pip
Found existing installation: pip 18.1
Uninstalling pip-18.1:
Successfully uninstalled pip-18.1
Successfully installed pip-21.2.4
pip install sanic sanic-testing
Looking in indexes: https://pypi.org/simple, https://artifactory.virt.ch.bbc.co.uk/artifactory/api/pypi/ap-python/simple
Collecting sanic
Using cached sanic-20.12.3-py3-none-any.whl (80 kB)
Collecting sanic-testing
Using cached sanic_testing-0.6.0-py3-none-any.whl (7.2 kB)
Collecting uvloop<0.15.0,>=0.5.3
Using cached uvloop-0.14.0-cp36-cp36m-macosx_10_11_x86_64.whl (1.5 MB)
Collecting httptools>=0.0.10
Using cached httptools-0.3.0-cp36-cp36m-macosx_10_9_x86_64.whl (155 kB)
Collecting ujson>=1.35
Using cached ujson-4.1.0-cp36-cp36m-macosx_10_14_x86_64.whl (45 kB)
Collecting aiofiles>=0.6.0
Using cached aiofiles-0.7.0-py3-none-any.whl (13 kB)
Collecting httpx==0.15.4
Using cached httpx-0.15.4-py3-none-any.whl (65 kB)
Collecting multidict<6.0,>=5.0
Using cached multidict-5.1.0-cp36-cp36m-macosx_10_14_x86_64.whl (49 kB)
Collecting websockets<9.0,>=8.1
Using cached websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl (66 kB)
Collecting httpcore==0.11.*
Using cached httpcore-0.11.1-py3-none-any.whl (52 kB)
Collecting rfc3986[idna2008]<2,>=1.3
Using cached rfc3986-1.5.0-py2.py3-none-any.whl (31 kB)
Collecting sniffio
Using cached sniffio-1.2.0-py3-none-any.whl (10 kB)
Collecting certifi
Using cached certifi-2021.5.30-py2.py3-none-any.whl (145 kB)
Collecting h11<0.10,>=0.8
Using cached h11-0.9.0-py2.py3-none-any.whl (53 kB)
Collecting contextvars>=2.1
Using cached contextvars-2.4-py3-none-any.whl
Collecting sanic-testing
Using cached sanic_testing-0.4.0-py3-none-any.whl (7.1 kB)
Using cached sanic_testing-0.3.1-py3-none-any.whl (6.3 kB)
Using cached sanic_testing-0.3.0-py3-none-any.whl (6.3 kB)
Using cached sanic_testing-0.2.1-py3-none-any.whl (6.0 kB)
Using cached sanic_testing-0.2.0-py3-none-any.whl (6.0 kB)
Using cached sanic_testing-0.1.2-py3-none-any.whl (5.7 kB)
Using cached sanic_testing-0.1.1-py3-none-any.whl (5.6 kB)
Using cached sanic_testing-0.1.0-py3-none-any.whl (5.5 kB)
INFO: pip is looking at multiple versions of sniffio to determine which version is compatible with other requirements. This could take a while.
Collecting sniffio
Using cached sniffio-1.1.0-py3-none-any.whl (4.5 kB)
Downloading sniffio-1.0.0-py3-none-any.whl (4.4 kB)
INFO: pip is looking at multiple versions of httpcore to determine which version is compatible with other requirements. This could take a while.
Collecting httpcore==0.11.*
Downloading httpcore-0.11.0-py3-none-any.whl (52 kB)
|████████████████████████████████| 52 kB 628 kB/s
INFO: pip is looking at multiple versions of httpx to determine which version is compatible with other requirements. This could take a while.
INFO: pip is looking at multiple versions of <Python from Requires-Python> to determine which version is compatible with other requirements. This could take a while.
INFO: pip is looking at multiple versions of sanic to determine which version is compatible with other requirements. This could take a while.
Collecting sanic
Using cached sanic-20.12.2-py3-none-any.whl (79 kB)
INFO: pip is looking at multiple versions of sniffio to determine which version is compatible with other requirements. This could take a while.
Using cached sanic-20.12.1-py3-none-any.whl (79 kB)
INFO: This is taking longer than usual. You might need to provide the dependency resolver with stricter constraints to reduce runtime. If you want to abort this run, you can press Ctrl + C to do so. To improve how pip performs, tell us what happened here: https://pip.pypa.io/surveys/backtracking
Using cached sanic-20.12.0-py3-none-any.whl (79 kB)
INFO: pip is looking at multiple versions of httpcore to determine which version is compatible with other requirements. This could take a while.
<snip very long output - gave up after 5min>
import pytest
from sanic import Sanic
from sanic_testing import TestManager
@pytest.fixture
def app():
sanic_app = Sanic(__name__)
TestManager(sanic_app)
return sanic_app
@pytest.mark.asyncio
async def test_not_found(app):
request, response = await app.asgi_client.get("/not-a-known-endpoint")
assert response.status == 404
This test will fail with an exception related to routing when ran.
How to reproduce
import pytest
from sanic_testing.reusable import ReusableClient
from sanic import Sanic
from sanic import response
@pytest.fixture(scope="session")
def app():
sanic_app = Sanic("TestSanic")
@sanic_app.get("/")
def basic(request):
return response.text("foo")
@sanic_app.post("/api/login")
def basic(request):
return response.text("foo")
@sanic_app.get("/api/resources")
def basic(request):
return response.text("foo")
return sanic_app
@pytest.fixture(scope="session")
def cli(app):
cli = ReusableClient(app)
return cli
def test_root(cli):
with cli:
_, response = cli.get("/")
assert response.status == 200
def test_login(cli):
with cli:
_, response = cli.post("/api/login", )
assert response.status == 200
_, response = cli.get("/api/resources")
assert response.status == 200
Error that I got
self = <_UnixSelectorEventLoop running=False closed=False debug=False>
future = <Task finished name='Task-18' coro=<StartupMixin.create_server() done, defined at /home/ghost/wuw2/lib/python3.10/site-packages/sanic/mixins/startup.py:347> exception=RuntimeError('cannot reuse already awaited coroutine')>
def run_until_complete(self, future):
"""Run until the Future is done.
If the argument is a coroutine, it is wrapped in a Task.
WARNING: It would be disastrous to call run_until_complete()
with the same coroutine twice -- it would wrap it in two
different Tasks and that can't be good.
Return the Future's result, or raise its exception.
"""
self._check_closed()
self._check_running()
new_task = not futures.isfuture(future)
future = tasks.ensure_future(future, loop=self)
if new_task:
# An exception is raised if the future didn't complete, so there
# is no need to log the "destroy pending task" message
future._log_destroy_pending = False
future.add_done_callback(_run_until_complete_cb)
try:
self.run_forever()
except:
if new_task and future.done() and not future.cancelled():
# The coroutine raised a BaseException. Consume the exception
# to not log a warning, the caller doesn't have access to the
# local task.
future.exception()
raise
finally:
future.remove_done_callback(_run_until_complete_cb)
if not future.done():
raise RuntimeError('Event loop stopped before Future completed.')
> return future.result()
E RuntimeError: cannot reuse already awaited coroutine
[2022-11-02 16:00:05 +0000] [82325] [ERROR] Experienced exception while trying to serve
Traceback (most recent call last):
File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/mixins/startup.py", line 921, in serve_single
worker_serve(monitor_publisher=None, **kwargs)
File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/worker/serve.py", line 106, in worker_serve
return _serve_http_1(
File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/server/runners.py", line 231, in _serve_http_1
loop.run_until_complete(app._server_event("init", "before"))
File "uvloop/loop.pyx", line 1517, in uvloop.loop.Loop.run_until_complete
File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/app.py", line 1549, in _server_event
await self.dispatch(
File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/signals.py", line 197, in dispatch
return await dispatch
File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/signals.py", line 167, in _dispatch
retval = await maybe_coroutine
File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/app.py", line 1140, in _listener
await maybe_coro
File "/Users/vsavin/repos/sanic_testing/venv/lib/python3.8/site-packages/sanic/mixins/startup.py", line 1056, in _start_servers
if not server_info.settings["loop"]:
KeyError: 'loop'
sanic = "22.9.0"
pytest = "^7.2.0"
sanic-testing = "==22.9.0"
import pytest
from sanic import Sanic, response
from sanic_testing.reusable import ReusableClient
@pytest.fixture
def app():
sanic_app = Sanic("app")
@sanic_app.get("/")
def basic(request):
return response.text("foo")
yield sanic_app
@pytest.fixture
def client(app):
client = ReusableClient(app, port=9999)
client.run()
yield client
client.stop()
@pytest.fixture()
def app_2():
app = Sanic("app_2")
@app.route("/")
def handler(request):
return response.text("OK")
yield app
def test_example_1(client):
_, response = client.get("/")
assert response.body == b"foo"
assert response.status == 200
def test_example_2(app_2):
_, response = app_2.test_client.get("/")
assert response.body == b"OK"
assert response.status == 200
I'm not familiar with the code but it looks like a bug in ReusableClient
- it creates ApplicationServerInfo
with the default settings dictionary which misses the loop
key there.
The tests pass if I change that to:
self.app.state.server_info.append(
ApplicationServerInfo(
settings={
"version": "1.1",
"ssl": None,
"unix": None,
"sock": None,
"host": self.host,
"port": self.port,
"loop": None,
}
)
)
httpx 0.24.0 has been released (changelog) so it would be nice to allow sanic-testing to use the latest version if possible
We need to setup CI. Maybe try GitHub Actions for this?
I am having this error:
def _run_request(self, *args, **kwargs):
> return self._do_request(*args, **kwargs)
E TypeError: _blank() takes 1 positional argument but 2 were given
/usr/local/lib/python3.10/site-packages/sanic_testing/testing.py:68: TypeError
Test function:
@pytest.mark.asyncio()
async def test_index(test_cli, app):
resp = await test_cli.get('/')
assert resp.status_code == 200
Versions:
pytest-sanic==1.9.1
sanic==21.6.2
sanic-babel==0.3.0
sanic-base-extension==0.1.1
sanic-devtools==0.1.0
sanic-jwt==1.7.0
sanic-routing==0.7.2
sanic-testing==0.7.0
This fixed it for me:
pawelkoston@9bb5842
I am trying to test app with TortoiseORM and i dont understand what is the right way to setup my test application in fixture.
I was tried to make with listeners and fixtures but i am always how problems with event loop, or tortoise can't connect to the database, or fixture doesnt work and etc. And i can't find any example on stackoverflow/github with testing Sanic with tortoise and test databases.
Can you show me the best way to setup tests for testing Sanic & TortoiseORM on the test db (e.g. sqlite) ?
p.s I think i can use sqlite:///:memory for
Hi,
I took a go at switching over to sanic-testing
for my unit tests on my Sanic
project, and noticed that some of my unit tests where inconsistently failing after migrating them from pytest-sanic
, which I was using before.
It seems that some of my tests that were passing a json payload were sometimes turning up in the request handlers without any payload on the request.
I've produced what looks to be minimal reproduction of this bug. Removing any of this (the unused GET
handler, or the blueprint) seem to stop the error somewhat reliably popping up for me, so it appears they are related.
I've made a public GitHub repo here with this example, but also including the setup below, if that's easier.
Here's the test case:
import pytest
from sanic import Sanic
from sanic import Blueprint
from sanic.request import Request
from sanic.response import text
from sanic.exceptions import InvalidUsage
from sanic_testing import TestManager
@pytest.fixture
def app():
app = Sanic(__name__)
rest = Blueprint("rest", url_prefix="/rest")
@rest.get("/users")
async def select(request: Request, user: dict):
pass
@rest.put("/users")
async def update(request: Request):
if request.json is None:
raise InvalidUsage("Missing json payload")
else:
return text("ok")
app.blueprint(rest)
return TestManager(app)
def test_json_payload(app):
request, response = app.test_client.put("/rest/users", json={})
assert response.status == 200
On running pytest
I will get (inconsistently) either a pass or fail, where sometimes the json payload available on request.json
and other times it is set to None
. Here's the output of two consecutive runs from my terminal:
$ pipenv run pytest tests
Loading .env environment variables...
================================================================= test session starts =================================================================
platform darwin -- Python 3.9.2, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: /private/var/folders/5v/x8dcvr4n2wl0dlq2pxd35y2w0000gn/T/pyground.XXXXXXX.OF7rb6Ys
collected 1 item
tests/test_main.py F [100%]
====================================================================== FAILURES =======================================================================
__________________________________________________________________ test_json_payload __________________________________________________________________
app = <sanic_testing.manager.TestManager object at 0x10e2927c0>
def test_json_payload(app):
request, response = app.test_client.put("/rest/users", json={})
> assert response.status == 200
E assert 400 == 200
E + where 400 = <Response [400 Bad Request]>.status
tests/test_main.py:36: AssertionError
---------------------------------------------------------------- Captured stdout call -----------------------------------------------------------------
[2021-03-31 19:50:14 +0100] [62919] [INFO] http://127.0.0.1:52851/rest/users
[2021-03-31 19:50:14 +0100] - (sanic.access)[INFO][127.0.0.1:52852]: PUT http://127.0.0.1:52851/rest/users 400 -1
[2021-03-31 19:50:14 +0100] [62919] [INFO] Starting worker [62919]
[2021-03-31 19:50:14 +0100] [62919] [INFO] Stopping worker [62919]
[2021-03-31 19:50:14 +0100] [62919] [INFO] Server Stopped
------------------------------------------------------------------ Captured log call ------------------------------------------------------------------
INFO sanic.root:testing.py:73 http://127.0.0.1:52851/rest/users
INFO sanic.access:http.py:435
INFO sanic.root:server.py:577 Starting worker [62919]
INFO sanic.root:server.py:580 Stopping worker [62919]
INFO sanic.root:app.py:928 Server Stopped
================================================================== warnings summary ===================================================================
.venv/lib/python3.9/site-packages/sanic_testing/manager.py:6
/private/var/folders/5v/x8dcvr4n2wl0dlq2pxd35y2w0000gn/T/pyground.XXXXXXX.OF7rb6Ys/.venv/lib/python3.9/site-packages/sanic_testing/manager.py:6: PytestCollectionWarning: cannot collect test class 'TestManager' because it has a __init__ constructor (from: tests/test_main.py)
class TestManager:
-- Docs: https://docs.pytest.org/en/stable/warnings.html
=============================================================== short test summary info ===============================================================
FAILED tests/test_main.py::test_json_payload - assert 400 == 200
============================================================ 1 failed, 1 warning in 5.43s =============================================================
$ pipenv run pytest tests
Loading .env environment variables...
================================================================= test session starts =================================================================
platform darwin -- Python 3.9.2, pytest-6.2.2, py-1.10.0, pluggy-0.13.1
rootdir: /private/var/folders/5v/x8dcvr4n2wl0dlq2pxd35y2w0000gn/T/pyground.XXXXXXX.OF7rb6Ys
collected 1 item
tests/test_main.py . [100%]
================================================================== warnings summary ===================================================================
.venv/lib/python3.9/site-packages/sanic_testing/manager.py:6
/private/var/folders/5v/x8dcvr4n2wl0dlq2pxd35y2w0000gn/T/pyground.XXXXXXX.OF7rb6Ys/.venv/lib/python3.9/site-packages/sanic_testing/manager.py:6: PytestCollectionWarning: cannot collect test class 'TestManager' because it has a __init__ constructor (from: tests/test_main.py)
class TestManager:
-- Docs: https://docs.pytest.org/en/stable/warnings.html
============================================================ 1 passed, 1 warning in 0.17s =============================================================
I've tried to hunt this down but have been unsuccessful. I've also tried this on different python versions, and machines. So I'm hoping you'll find the same inconsistent error/pass.
Here's the pipenv
environment used with the versions for the dependancies:
Pipfile:
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"
[packages]
sanic = "*"
sanic-testing = "*"
pytest = "*"
[dev-packages]
[requires]
python_version = "3.9"
Pipfile.lock
{
"_meta": {
"hash": {
"sha256": "a5e4466bac5fdc5f5fba3b90f03441ffb8a799a6c607ba412c25b1ff10d06652"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.9"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"aiofiles": {
"hashes": [
"sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27",
"sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092"
],
"version": "==0.6.0"
},
"attrs": {
"hashes": [
"sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
"sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.3.0"
},
"certifi": {
"hashes": [
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
],
"version": "==2020.12.5"
},
"h11": {
"hashes": [
"sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6",
"sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"
],
"markers": "python_version >= '3.6'",
"version": "==0.12.0"
},
"httpcore": {
"hashes": [
"sha256:37ae835fb370049b2030c3290e12ed298bf1473c41bb72ca4aa78681eba9b7c9",
"sha256:93e822cd16c32016b414b789aeff4e855d0ccbfc51df563ee34d4dbadbb3bcdc"
],
"markers": "python_version >= '3.6'",
"version": "==0.12.3"
},
"httptools": {
"hashes": [
"sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be",
"sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d",
"sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce",
"sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2",
"sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6",
"sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f",
"sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009",
"sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce",
"sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a",
"sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c",
"sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4",
"sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437"
],
"version": "==0.1.1"
},
"httpx": {
"hashes": [
"sha256:126424c279c842738805974687e0518a94c7ae8d140cd65b9c4f77ac46ffa537",
"sha256:9cffb8ba31fac6536f2c8cde30df859013f59e4bcc5b8d43901cb3654a8e0a5b"
],
"markers": "python_version >= '3.6'",
"version": "==0.16.1"
},
"idna": {
"hashes": [
"sha256:5205d03e7bcbb919cc9c19885f9920d622ca52448306f2377daede5cf3faac16",
"sha256:c5b02147e01ea9920e6b0a3f1f7bb833612d507592c837a6c49552768f4054e1"
],
"version": "==3.1"
},
"iniconfig": {
"hashes": [
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
],
"version": "==1.1.1"
},
"multidict": {
"hashes": [
"sha256:018132dbd8688c7a69ad89c4a3f39ea2f9f33302ebe567a879da8f4ca73f0d0a",
"sha256:051012ccee979b2b06be928a6150d237aec75dd6bf2d1eeeb190baf2b05abc93",
"sha256:05c20b68e512166fddba59a918773ba002fdd77800cad9f55b59790030bab632",
"sha256:07b42215124aedecc6083f1ce6b7e5ec5b50047afa701f3442054373a6deb656",
"sha256:0e3c84e6c67eba89c2dbcee08504ba8644ab4284863452450520dad8f1e89b79",
"sha256:0e929169f9c090dae0646a011c8b058e5e5fb391466016b39d21745b48817fd7",
"sha256:1ab820665e67373de5802acae069a6a05567ae234ddb129f31d290fc3d1aa56d",
"sha256:25b4e5f22d3a37ddf3effc0710ba692cfc792c2b9edfb9c05aefe823256e84d5",
"sha256:2e68965192c4ea61fff1b81c14ff712fc7dc15d2bd120602e4a3494ea6584224",
"sha256:2f1a132f1c88724674271d636e6b7351477c27722f2ed789f719f9e3545a3d26",
"sha256:37e5438e1c78931df5d3c0c78ae049092877e5e9c02dd1ff5abb9cf27a5914ea",
"sha256:3a041b76d13706b7fff23b9fc83117c7b8fe8d5fe9e6be45eee72b9baa75f348",
"sha256:3a4f32116f8f72ecf2a29dabfb27b23ab7cdc0ba807e8459e59a93a9be9506f6",
"sha256:46c73e09ad374a6d876c599f2328161bcd95e280f84d2060cf57991dec5cfe76",
"sha256:46dd362c2f045095c920162e9307de5ffd0a1bfbba0a6e990b344366f55a30c1",
"sha256:4b186eb7d6ae7c06eb4392411189469e6a820da81447f46c0072a41c748ab73f",
"sha256:54fd1e83a184e19c598d5e70ba508196fd0bbdd676ce159feb412a4a6664f952",
"sha256:585fd452dd7782130d112f7ddf3473ffdd521414674c33876187e101b588738a",
"sha256:5cf3443199b83ed9e955f511b5b241fd3ae004e3cb81c58ec10f4fe47c7dce37",
"sha256:6a4d5ce640e37b0efcc8441caeea8f43a06addace2335bd11151bc02d2ee31f9",
"sha256:7df80d07818b385f3129180369079bd6934cf70469f99daaebfac89dca288359",
"sha256:806068d4f86cb06af37cd65821554f98240a19ce646d3cd24e1c33587f313eb8",
"sha256:830f57206cc96ed0ccf68304141fec9481a096c4d2e2831f311bde1c404401da",
"sha256:929006d3c2d923788ba153ad0de8ed2e5ed39fdbe8e7be21e2f22ed06c6783d3",
"sha256:9436dc58c123f07b230383083855593550c4d301d2532045a17ccf6eca505f6d",
"sha256:9dd6e9b1a913d096ac95d0399bd737e00f2af1e1594a787e00f7975778c8b2bf",
"sha256:ace010325c787c378afd7f7c1ac66b26313b3344628652eacd149bdd23c68841",
"sha256:b47a43177a5e65b771b80db71e7be76c0ba23cc8aa73eeeb089ed5219cdbe27d",
"sha256:b797515be8743b771aa868f83563f789bbd4b236659ba52243b735d80b29ed93",
"sha256:b7993704f1a4b204e71debe6095150d43b2ee6150fa4f44d6d966ec356a8d61f",
"sha256:d5c65bdf4484872c4af3150aeebe101ba560dcfb34488d9a8ff8dbcd21079647",
"sha256:d81eddcb12d608cc08081fa88d046c78afb1bf8107e6feab5d43503fea74a635",
"sha256:dc862056f76443a0db4509116c5cd480fe1b6a2d45512a653f9a855cc0517456",
"sha256:ecc771ab628ea281517e24fd2c52e8f31c41e66652d07599ad8818abaad38cda",
"sha256:f200755768dc19c6f4e2b672421e0ebb3dd54c38d5a4f262b872d8cfcc9e93b5",
"sha256:f21756997ad8ef815d8ef3d34edd98804ab5ea337feedcd62fb52d22bf531281",
"sha256:fc13a9524bc18b6fb6e0dbec3533ba0496bbed167c56d0aabefd965584557d80"
],
"markers": "python_version >= '3.6'",
"version": "==5.1.0"
},
"packaging": {
"hashes": [
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.9"
},
"pluggy": {
"hashes": [
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.13.1"
},
"py": {
"hashes": [
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.10.0"
},
"pyparsing": {
"hashes": [
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7"
},
"pytest": {
"hashes": [
"sha256:9d1edf9e7d0b84d72ea3dbcdfd22b35fb543a5e8f2a60092dd578936bf63d7f9",
"sha256:b574b57423e818210672e07ca1fa90aaf194a4f63f3ab909a2c67ebb22913839"
],
"index": "pypi",
"version": "==6.2.2"
},
"rfc3986": {
"extras": [
"idna2008"
],
"hashes": [
"sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d",
"sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"
],
"version": "==1.4.0"
},
"sanic": {
"hashes": [
"sha256:84a04c5f12bf321bed3942597787f1854d15c18f157aebd7ced8c851ccc49e08",
"sha256:9b63b0367f45a854023cb1f54a7315a86582442a08ba22f806c1f3ebf0c04e00"
],
"index": "pypi",
"version": "==21.3.2"
},
"sanic-routing": {
"hashes": [
"sha256:6abed924082f853100ad6bd1f0a207aa9a58840e43f3f53bbbc5459781aeb17a",
"sha256:ae403650fe2d8d924edb60074e55c99657e260d7097c2fc50d0b3e1e08d3def4"
],
"version": "==0.4.3"
},
"sanic-testing": {
"hashes": [
"sha256:67dd21e5309105fe3d037cc0e435d1dc7305dcdfde31feb8d13861a82f6ae95c",
"sha256:a0a8a4d7bc0c5b2e2175c529bb4fa5edf3fd8a9317cdd582825997a91be9bfa9"
],
"index": "pypi",
"version": "==0.3.0"
},
"sniffio": {
"hashes": [
"sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663",
"sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"
],
"markers": "python_version >= '3.5'",
"version": "==1.2.0"
},
"toml": {
"hashes": [
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2"
},
"ujson": {
"hashes": [
"sha256:0190d26c0e990c17ad072ec8593647218fe1c675d11089cd3d1440175b568967",
"sha256:0ea07fe57f9157118ca689e7f6db72759395b99121c0ff038d2e38649c626fb1",
"sha256:30962467c36ff6de6161d784cd2a6aac1097f0128b522d6e9291678e34fb2b47",
"sha256:4d6d061563470cac889c0a9fd367013a5dbd8efc36ad01ab3e67a57e56cad720",
"sha256:5e1636b94c7f1f59a8ead4c8a7bab1b12cc52d4c21ababa295ffec56b445fd2a",
"sha256:7333e8bc45ea28c74ae26157eacaed5e5629dbada32e0103c23eb368f93af108",
"sha256:84b1dca0d53b0a8d58835f72ea2894e4d6cf7a5dd8f520ab4cbd698c81e49737",
"sha256:91396a585ba51f84dc71c8da60cdc86de6b60ba0272c389b6482020a1fac9394",
"sha256:a214ba5a21dad71a43c0f5aef917cd56a2d70bc974d845be211c66b6742a471c",
"sha256:aad6d92f4d71e37ea70e966500f1951ecd065edca3a70d3861b37b176dd6702c",
"sha256:b3a6dcc660220539aa718bcc9dbd6dedf2a01d19c875d1033f028f212e36d6bb",
"sha256:b5c70704962cf93ec6ea3271a47d952b75ae1980d6c56b8496cec2a722075939",
"sha256:c615a9e9e378a7383b756b7e7a73c38b22aeb8967a8bfbffd4741f7ffd043c4d",
"sha256:d3a87888c40b5bfcf69b4030427cd666893e826e82cc8608d1ba8b4b5e04ea99",
"sha256:e2cadeb0ddc98e3963bea266cc5b884e5d77d73adf807f0bda9eca64d1c509d5",
"sha256:e390df0dcc7897ffb98e17eae1f4c442c39c91814c298ad84d935a3c5c7a32fa",
"sha256:e6e90330670c78e727d6637bb5a215d3e093d8e3570d439fd4922942f88da361",
"sha256:eb6b25a7670c7537a5998e695fa62ff13c7f9c33faf82927adf4daa460d5f62e",
"sha256:f273a875c0b42c2a019c337631bc1907f6fdfbc84210cc0d1fff0e2019bbfaec",
"sha256:f8aded54c2bc554ce20b397f72101737dd61ee7b81c771684a7dd7805e6cca0c",
"sha256:fc51e545d65689c398161f07fd405104956ec27f22453de85898fa088b2cd4bb"
],
"markers": "sys_platform != 'win32' and implementation_name == 'cpython'",
"version": "==4.0.2"
},
"uvloop": {
"hashes": [
"sha256:114543c84e95df1b4ff546e6e3a27521580466a30127f12172a3278172ad68bc",
"sha256:19fa1d56c91341318ac5d417e7b61c56e9a41183946cc70c411341173de02c69",
"sha256:2bb0624a8a70834e54dde8feed62ed63b50bad7a1265c40d6403a2ac447bce01",
"sha256:42eda9f525a208fbc4f7cecd00fa15c57cc57646c76632b3ba2fe005004f051d",
"sha256:44cac8575bf168601424302045234d74e3561fbdbac39b2b54cc1d1d00b70760",
"sha256:6de130d0cb78985a5d080e323b86c5ecaf3af82f4890492c05981707852f983c",
"sha256:7ae39b11a5f4cec1432d706c21ecc62f9e04d116883178b09671aa29c46f7a47",
"sha256:90e56f17755e41b425ad19a08c41dc358fa7bf1226c0f8e54d4d02d556f7af7c",
"sha256:b45218c99795803fb8bdbc9435ff7f54e3a591b44cd4c121b02fa83affb61c7c",
"sha256:e5e5f855c9bf483ee6cd1eb9a179b740de80cb0ae2988e3fa22309b78e2ea0e7"
],
"markers": "sys_platform != 'win32' and implementation_name == 'cpython'",
"version": "==0.15.2"
},
"websockets": {
"hashes": [
"sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5",
"sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5",
"sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308",
"sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb",
"sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a",
"sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c",
"sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170",
"sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422",
"sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8",
"sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485",
"sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f",
"sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8",
"sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc",
"sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779",
"sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989",
"sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1",
"sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092",
"sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824",
"sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d",
"sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55",
"sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36",
"sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"
],
"markers": "python_full_version >= '3.6.1'",
"version": "==8.1"
}
},
"develop": {}
}
Sanic-testing
uses httpx
under the hood, which makes monkeypatching httpx requests hard to implement.
For example if we have and endpoint which makes external requests, and if we monkeypatch this request, then it patches an actual request to endpoint:
# views.py
import httpx
...
@app.get('/test')
async def view_test(request):
async with httpx.AsyncClient() as client:
api_response = await client.get(
'https://jsonplaceholder.typicode.com/todos/1',
timeout=10,
)
resp = api_response.json()
resp['foo'] = 0
return HTTPResponse(json.dumps(resp), 200)
Test function:
# test_views.py
import httpx, pytest
...
# The `client` parameter is the fixture of web app
def test_view_test(client, monkeypatch):
async def return_mock_response(*args, **kwargs):
return httpx.Response(200, content=b'{"response": "response"}')
monkeypatch.setattr(httpx.AsyncClient, 'get', return_mock_response)
_, response = client.test_client.get('/test')
assert response.json == {'response': 'response', 'foo': 0}
assert response.status_code == 200
Here, we replace our request _, response = client.test_client.get('/test')
with the patched one. Which makes us customize the patch by excluding the host address.
Hi there!
I've implemented sanic-extensions and deleted all my previous CORS-code, that was inspired by these instructions.
So for now I have such code.
"""API application."""
import random
import string
from sanic import Sanic
from sanic.exceptions import ServerError, NotFound, InvalidUsage, MethodNotSupported
from sanic_ext import Extend
from api import api_blueprints, IndexRoute
from api.middlewares.on_request import validate_request
from api.middlewares.on_response import send_metrics
from api.middlewares.on_start import create_app_context
from cfg import config
from tools.abstract.interfaces import BaseView
def create_app(
*, api_config: object = config.APIConfig, log_config: dict = config.logging_config
) -> Sanic:
"""Create and return Sanic application."""
app = Sanic(name=random.choice(string.ascii_letters), log_config=log_config)
app.update_config(api_config)
app.config["API_VERSION"] = config.version
cors_headers = "origin, content-type, accept, authorization, x-xsrf-token, x-request-id"
app.config.CORS_ORIGINS = "*"
app.config.CORS_METHODS = "*"
app.config.CORS_ALLOW_HEADERS = cors_headers
app.config.CORS_EXPOSE_HEADERS = cors_headers
Extend(app)
app.listener(create_app_context, "before_server_start")
app.middleware(validate_request, "request")
app.middleware(send_metrics, "response")
app.blueprint(api_blueprints)
app.add_route(IndexRoute.as_view(), "/", name="index")
app.error_handler.add(NotFound, BaseView.not_found)
app.error_handler.add(InvalidUsage, BaseView.bad_request)
app.error_handler.add(ServerError, BaseView.server_error)
app.error_handler.add(MethodNotSupported, BaseView.method_not_allowed)
app.error_handler.add(Exception, BaseView.server_error)
app.ctx.config = config
return app
if __name__ == "__main__":
api = create_app()
api.run(debug=True)
My BaseView class looks like this:
class BaseView(HTTPMethodView):
"""Base class-based view providing all HTTP-methods."""
@classmethod
async def ok_response(cls, request: Request, response: Union[str, int, float, Iterable, dict]):
"""Return OK and message."""
code = 200
if request.method == "POST":
code = 201
if isinstance(response, str):
return json({"code": code, "message": response}, status=code)
return json(response, code)
@classmethod
async def bad_request(cls, request: Request, exception=None):
"""Return 400 "Bad Request" and message."""
if exception:
logger.exception(f"Code 400: {exception}")
return json({"code": 400, "message": f"Bad request: {exception}"}, status=400)
return json({"code": 400, "message": f"Bad request: {await get_request_info(request)}"}, status=400)
@classmethod
async def not_found(cls, request: Request, exception=None):
"""Return 404 "Not Found" and message."""
if exception:
logger.exception(f"Code 404: {exception}")
return json({"code": 404, "message": f"Not found: {exception}"}, status=404)
return json({"code": 404, "message": f"Not found: {await get_request_info(request)}"}, status=404)
@classmethod
async def method_not_allowed(cls, request: Request, exception=None):
"""Return 405 "Method Not Allowed" and message."""
return json({"code": 405, "message": f"Method not allowed: {request.method} {request.url}"}, status=405)
@classmethod
async def server_error(cls, request: Request, exception):
"""Return 500 "Internal Server Error" and message."""
logger.exception(f"Code 500: {exception}")
request_info = await get_request_info(request)
message = exception.args[0]
return json({"code": 500, "exception": message, "request": request_info}, status=500)
@openapi.exclude(True)
async def get(self, request: Request, *args, **kwargs):
return await self.method_not_allowed(request)
@openapi.exclude(True)
async def post(self, request: Request, *args, **kwargs):
return await self.method_not_allowed(request)
@openapi.exclude(True)
async def put(self, request: Request, *args, **kwargs):
return await self.method_not_allowed(request)
@openapi.exclude(True)
async def patch(self, request: Request, *args, **kwargs):
return await self.method_not_allowed(request)
@openapi.exclude(True)
async def delete(self, request: Request, *args, **kwargs):
return await self.method_not_allowed(request)
async def head(self, request: Request, *args, **kwargs):
return empty()
async def options(self, request: Request, *args, **kwargs):
return empty()
And my IndexView looks like this:
class IndexRoute(BaseView):
"""Base index route."""
async def get(self, request: Request):
"""Redirects to API swagger documentation."""
return redirect(request.app.url_for("openapi.swagger"))
So, after implementing Extend(app) I get
serge@serge:~$ curl -X HEAD http://0.0.0.0:8000/ -I
HTTP/1.1 204 No Content
access-control-allow-origin: *
access-control-expose-headers: content-type, authorization,origin, accept, x-xsrf-token, x-request-id
connection: keep-alive
But when I'm using sanic-testing for testing HEAD and OPTIONS methods my tests fail. Moreover any other test that was successfully passed (no AssertionError) has a traceback with SanicExceptionError.
Let's go step by step.
My client-fixture and HEAD-test code:
@pytest.fixture()
def app_client() -> SanicASGITestClient:
"""Sanic test asgi-client."""
app: Sanic = create_app()
manager: TestManager = TestManager(app)
return manager.asgi_client
@pytest.mark.asyncio()
class TestIndex:
url = "/"
async def test_index_200(self, app_client: SanicASGITestClient):
"""Test index route opens swagger docs route."""
req, resp = await app_client.get(self.url)
assert req.app.url_for("openapi.swagger") in req.url
assert resp.status == 200
print(app_client.sanic_app.ctx.config.mongo.triggers_coll)
async def test_index_302(self, app_client: SanicASGITestClient):
"""Test index route is actually redirecting to swagger docs route."""
req, resp = await app_client.get(self.url, allow_redirects=False)
assert req.app.url_for("openapi.swagger") not in req.url
assert resp.status == 302
async def test_head(self, app_client: SanicASGITestClient):
"""Check CORS is enabled."""
req, res = await app_client.head(self.url)
try:
assert res.status == 204
assert res.headers == {
"access-control-allow-origin": "*",
"access-control-expose-headers": "origin, content-type, accept, authorization, x-xsrf-token, x-request-id",
}
finally:
print(res.headers)
The last test fails with traceback:
FAILED [100%][sanic.root INFO [2021-12-09 15:52:23 +0300]]: HEAD /? : 0.00017905235290527344 ms
Headers({})
[sanic.error ERROR [2021-12-09 15:52:23 +0300]]: Exception occurred in one of response middleware handlers
Traceback (most recent call last):
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic/request.py", line 187, in respond
response = await self.app._run_response_middleware(
File "_run_response_middleware", line 22, in _run_response_middleware
from ssl import Purpose, SSLContext, create_default_context
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 54, in _add_cors_headers
_add_origin_header(request, response)
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 160, in _add_origin_header
allow_origins = _get_from_cors_ctx(
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 151, in _get_from_cors_ctx
value = getattr(request.route.ctx._cors, key, default)
AttributeError: 'types.SimpleNamespace' object has no attribute '_cors'
tests/test_api/test_index.py:23 (TestIndex.test_head)
Headers({}) != {'access-control-allow-origin': '*', 'access-control-expose-headers': 'origin, content-type, accept, authorization, x-xsrf-token, x-request-id'}
Expected :{'access-control-allow-origin': '*', 'access-control-expose-headers': 'origin, content-type, accept, authorization, x-xsrf-token, x-request-id'}
Actual :Headers({})
And the first two cases pass, but have the following trace:
PASSED [ 33%][sanic.root INFO [2021-12-09 15:52:23 +0300]]: GET /? : 0.00021386146545410156 ms
[sanic.root INFO [2021-12-09 15:52:23 +0300]]: GET /docs/swagger? : 0.0001621246337890625 ms
test_triggers
[sanic.error ERROR [2021-12-09 15:52:23 +0300]]: Exception occurred in one of response middleware handlers
Traceback (most recent call last):
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic/request.py", line 187, in respond
response = await self.app._run_response_middleware(
File "_run_response_middleware", line 22, in _run_response_middleware
from ssl import Purpose, SSLContext, create_default_context
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 54, in _add_cors_headers
_add_origin_header(request, response)
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 160, in _add_origin_header
allow_origins = _get_from_cors_ctx(
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 151, in _get_from_cors_ctx
value = getattr(request.route.ctx._cors, key, default)
AttributeError: 'types.SimpleNamespace' object has no attribute '_cors'
[sanic.error ERROR [2021-12-09 15:52:23 +0300]]: Exception occurred in one of response middleware handlers
Traceback (most recent call last):
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic/request.py", line 187, in respond
response = await self.app._run_response_middleware(
File "_run_response_middleware", line 22, in _run_response_middleware
from ssl import Purpose, SSLContext, create_default_context
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 54, in _add_cors_headers
_add_origin_header(request, response)
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 160, in _add_origin_header
allow_origins = _get_from_cors_ctx(
File "/home/serge/owm/mail_triggers/venv3101/lib/python3.10/site-packages/sanic_ext/extensions/http/cors.py", line 151, in _get_from_cors_ctx
value = getattr(request.route.ctx._cors, key, default)
AttributeError: 'types.SimpleNamespace' object has no attribute '_cors'
I wonder is that my fault or it is a bug in testing module? Will be glad to answer your questions to resolve this issue.
By the way, when we may expect the sanic-testing documentation? It was written that it shall be published in October, 2021, but still there is no any.
P.S.: My environment is:
python 3.10.1
sanic 21.9.3
sanic-ext 21.9.3
sanic-testing 0.7.0
sanic-routing 0.7.2
I tried to run the async test and it failed.
_________________________________________________________ test_basic_asgi_client __________________________________________________________
app = Sanic(name="test_test")
@pytest.mark.asyncio
async def test_basic_asgi_client(app):
> request, response = await app.asgi_client.get("/")
test_test.py:16:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.venv/lib64/python3.10/site-packages/httpx/_client.py:1751: in get
return await self.request(
.venv/lib64/python3.10/site-packages/sanic_testing/testing.py:364: in request
await self.sanic_app._startup() # type: ignore
.venv/lib64/python3.10/site-packages/sanic/app.py:1513: in _startup
self.ext._display()
.venv/lib64/python3.10/site-packages/sanic_ext/bootstrap.py:110: in _display
f" > {extension.name} {extension.render_label()}"
.venv/lib64/python3.10/site-packages/sanic_ext/extensions/base.py:56: in render_label
label = self.label()
.venv/lib64/python3.10/site-packages/sanic_ext/extensions/openapi/extension.py:25: in label
return self._make_url()
.venv/lib64/python3.10/site-packages/sanic_ext/extensions/openapi/extension.py:34: in _make_url
else self.app.serve_location
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = Sanic(name="test_test")
@property
def serve_location(self) -> str:
> server_settings = self.state.server_info[0].settings
E IndexError: list index out of range
.venv/lib64/python3.10/site-packages/sanic/mixins/runner.py:574: IndexError
test_test.py
file with this contentsimport pytest
from sanic import Sanic, response
@pytest.fixture
def app():
sanic_app = Sanic(__name__)
@sanic_app.get("/")
def basic(request):
return response.text("foo")
return sanic_app
@pytest.mark.asyncio
async def test_basic_asgi_client(app):
request, response = await app.asgi_client.get("/")
assert request.method.lower() == "get"
assert response.body == b"foo"
assert response.status == 200
pytest
and watch it failaiofiles==0.8.0
anyio==3.6.1
attrs==21.4.0
certifi==2022.6.15
h11==0.12.0
httpcore==0.15.0
httptools==0.4.0
httpx==0.23.0
idna==3.3
iniconfig==1.1.1
multidict==6.0.2
packaging==21.3
pluggy==1.0.0
py==1.11.0
pyparsing==3.0.9
pytest==7.1.2
pytest-asyncio==0.18.3
PyYAML==6.0
rfc3986==1.5.0
sanic==22.6.0
sanic-ext==22.6.2
sanic-routing==22.3.0
sanic-testing==22.6.0
sniffio==1.2.0
tomli==2.0.1
ujson==5.4.0
uvloop==0.16.0
websockets==10.3
Should I use 22.x or 0.8.x testing modules when I'm testing with Sanic 21.12 LTS?
sanic-testing 22.9 breaks compatibility with sanic 21.12:
>>> from sanic import Sanic
>>> app = Sanic('app')
>>> app.test_client.get('/')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/.../python3.9/site-packages/sanic_testing/testing.py", line 288, in get
return self._sanic_endpoint_test("get", *args, **kwargs)
File "/.../python3.9/site-packages/sanic_testing/testing.py", line 237, in _sanic_endpoint_test
self.app.run( # type: ignore
TypeError: run() got an unexpected keyword argument 'single_process'
sanic-testing 22.6 works though
Example:
import pickle
from sanic import Sanic
from sanic_testing import TestManager
app = Sanic(__name__)
test_manager = TestManager(app)
my_dict = {"app": app}
my_pickled = pickle.dumps(my_dict)
Error:
> pickle.dumps(my_dict)
E AttributeError: Can't pickle local object 'SanicASGITestClient.__init__.<locals>._collect_request'
This is due to the use of nested functions in the ASGS TestClient methods, python does not know how to pickle object that use those.
After doing some mypy analysis, I've determined that the ASGITestClient request()
method doesn't actually return a Sanic HTTPResponse, but instead a HTTPX Response object. Is this intended? Looks like theres an attempt to make it compatibile with a Sanic response object by adding in some extra attributes? Was that just to get some tests passing?
response.status = response.status_code
response.body = response.content
response.content_type = response.headers.get("content-type")
Should we try to construct and return a real Sanic HTTPResponse here instead?
During work on sanic-org/sanic#2323, I found that current object that's used to mock stream, TestASGIApp
, doesn't have stage
and response
attribute. I am not sure if it is worth, but it seems implement a mocked stage
and response
in TestASGIApp
can make the testing more like the real use cases.
As discussed in #6, we should always return a Request
object.
Tuple[Optional[Request], HTTPResponse]
Extend sanic-testing with tox tests to ensure python 3.12 is support by the library
It seems as though sanic_testing does not full expose the final response.
import pytest
from sanic import Sanic
from sanic import response
from sanic_testing import TestManager
@pytest.fixture
def app():
sanic_app = Sanic(__name__)
TestManager(sanic_app)
@sanic_app.get('/data.json')
def get_data(request):
return response.json({
'data': [1, 2, 3, 4],
})
return sanic_app
@pytest.mark.asyncio
async def test_json_response(app):
request, response = await app.asgi_client.get("/data.json")
assert response.status == 200
assert response.headers['Content-Type'] == 'application/json'
assert response.headers['Content-Length'] == len(response.body) # <- failure here
When running the same server and making a request to it, the Content-Length
header is correctly set:
$ curl -v http://localhost:3000/data.json
* Trying 127.0.0.1:3000...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 3000 (#0)
> GET /data.json HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/7.68.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-length: 18
< connection: keep-alive
< content-type: application/json
<
* Connection #0 to host localhost left intact
{"data":[1,2,3,4]}
I would like to add that Content-Type
is the only response header sent as well, so Transfer-Encoding
is not set either.
websockets
is pinned. The latest websockets
is 9.1 and this release is fixing a authentication vulnerability which was introduced with 8.0.
I am trying to test a websocket endpoint that uses websocket.recv()
inside, but this method is throwing an exception.
ERROR sanic.error:handlers.py:183 Exception occurred while handling uri: 'http:///ws'
Traceback (most recent call last):
File "/worker/.local/lib/python3.7/site-packages/sanic/app.py", line 971, in _websocket_handler
await fut
File "/app/application_tests/test_sanic_testing.py", line 18, in handler
await ws.recv()
File "/worker/.local/lib/python3.7/site-packages/sanic/server/websockets/connection.py", line 49, in recv
if message["type"] == "websocket.receive":
KeyError: 'type'
import pytest
from sanic import Sanic
from sanic_testing import TestManager
@pytest.fixture
def app():
sanic_app = Sanic("test")
TestManager(sanic_app)
return sanic_app
@pytest.mark.asyncio
async def test_websocket_route(app):
@app.websocket("/ws")
async def handler(request, ws):
await ws.recv()
await app.asgi_client.websocket("/ws")
pytest
and see the error messagePython 3.7
pytest-asyncio==0.19.0
sanic==22.6.1
sanic-testing==22.6.0
Describe the bug
I was wondering why my code was returning True for testing when I wasn't testing it (it behave differently when testing, such as not really sending emails).
After looking, I found out that Sanic/app.py loop over the SANIC_PACKAGES list, which includes "sanic-testing", and if present, import them.
When importing Sanic testing, this package change the test_mode
to True, which is not true until the Sanic Testing package has been instantiated. IMHO, it should only change the state to true
when enabled, not when imported
Code snippet
app.py, line 1836
for package_name in SANIC_PACKAGES:
module_name = package_name.replace("-", "_")
try:
module = import_module(module_name)
packages.append(f"{package_name}=={module.__version__}")
except ImportError:
...
Expected behavior
Importing "sanic-testing" shouldn't change the state of Sanic (test_mode). Only when instantiating it.
Environment (please complete the following information):
We believe we've got the public API to a stable point now, but would strongly recommend pinning your dependencies to the 0.18.* release, so that you're able to properly review API changes between package updates.
The conflict is caused by:
sanic-testing 0.3.1 depends on httpx<0.18 and >=0.16
Because of the logging requirement in Sanic Extensions, the ReusableClient
fails because there is no ApplicationState
.
sanic-testing/sanic_testing/testing.py
Lines 112 to 118 in c22b4dd
The non-asgi client adds the collect_request middleware to the app if "gather_request" flag is True.
However multiple calls to test_client.request()
will result in many copies of the collect_request middleware to be placed in the queue. This results in the collect_request middleware running multiple times, and it will even run if a later call to test_client.request()
specifies gather_request=False.
sanic-testing/sanic_testing/testing.py
Line 258 in c22b4dd
The ASGI Test client gets around this issue by placing the "collect_request" middlware into the queue once only, then stores a "gather_request" flag on the test-client itself.
However this logic is incomplete, because while the self.gather_request
variable is saved, it is never used or looked up during the request, so request is always gathered, even if "gather_request" is False.
sanic-testing/sanic_testing/testing.py
Line 269 in c22b4dd
httpx
< 0.23.0 contains security vulnerability GHSA-h8pj-cxx2-jfg2 that was fixed in httpx = 0.23.0
in:
The version containing the hotfix is currently not allowed
Lines 49 to 51 in 04b1b96
Sanic.test_mode = True
_sanic_endpoint_test can return response
or (request, response)
depending if gather_request
is specified.
Tuple and Union are around the wrong way.
typing.Tuple[typing.Union[Request, HTTPResponse]]
Should be
typing.Union[typing.Tuple[Request, HTTPResponse], HTTPResponse]
The type annotation of Sanic app's test_client
currently is Any
. Create a better type for it can make test dev experience better. I think adding a parent class, either abstract or non-abstract class, is one of the ways to solve this.
For every test I run, my tests pass but I get the following error:
Task was destroyed but it is pending!
task: <Task pending name='Task-22' coro=<ModelRunner.model_runner() running at /opt/url-ml-server/app/request_batching/model_runner.py:77> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7f665dd762b0>()]>>
Task was destroyed but it is pending!
task: <Task pending name='Task-11' coro=<ModelRunner.model_runner() running at /opt/url-ml-server/app/request_batching/model_runner.py:77> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7f665dd762b0>()]>>
Task was destroyed but it is pending!
task: <Task pending name='Task-50' coro=<ModelRunner.model_runner() running at /opt/url-ml-server/app/request_batching/model_runner.py:77> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x7f665dd762b0>()]>>
@ahopkins Is there a way to purge tasks before app/server stops to avoid above warnings?
Hi,
I wanted to enquire if there are examples of unit tests for this. I have been continuously struggling with unit tests for sanic async post method with the following error:
async with self.queue_lock:
AttributeError: __aenter__
I am getting this error for:
async with self.queue_lock:
if len(self.queue) >= MAX_QUEUE_SIZE:
For more details, I am using a code from here: https://github.com/deep-learning-with-pytorch/dlwpt-code/blob/master/p3ch15/request_batching_server.py#L58
The test I am trying:
import json
import pytest
from ..app import app
@pytest.mark.asyncio
async def test_basic_post():
_, response = await app.asgi_client.post("/predict", data=json.dumps(INPUT), headers=HEADERS)
print(response)
Please do let me know if anyone has come across this and has a solution. Thank you.
CC: @ahopkins
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.