Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ python:
- "3.8"
- "3.9"
- "3.10.0"
- "3.11"
# command to install dependencies
install:
- pip install codecov
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# async-asgi-testclient

[![Build Status](https://travis-ci.com/vinissimus/async-asgi-testclient.svg?branch=master)](https://travis-ci.com/vinissimus/async-asgi-testclient) [![PyPI version](https://badge.fury.io/py/async-asgi-testclient.svg)](https://badge.fury.io/py/async-asgi-testclient) ![](https://img.shields.io/pypi/pyversions/async-asgi-testclient.svg) [![Codcov](https://codecov.io/gh/vinissimus/async-asgi-testclient/branch/master/graph/badge.svg)](https://codecov.io/gh/vinissimus/async-asgi-testclient/branch/master) ![](https://img.shields.io/github/license/vinissimus/async-asgi-testclient)
[![Build Status](https://travis-ci.com/numberly/async-asgi-testclient.svg?branch=master)](https://travis-ci.com/numberly/async-asgi-testclient) [![Codcov](https://codecov.io/gh/numberly/async-asgi-testclient/branch/master/graph/badge.svg)](https://codecov.io/gh/numberly/async-asgi-testclient/branch/master) ![](https://img.shields.io/github/license/numberly/async-asgi-testclient)

Async ASGI TestClient is a library for testing web applications that implements ASGI specification (version 2 and 3).

The motivation behind this project is building a common testing library that doesn't depend on the web framework ([Quart](https://gitlab.com/pgjones/quart), [Startlette](https://github.com/encode/starlette), ...).
The motivation behind this project is building a common testing library that doesn't depend on the web framework ([Quart](https://gitlab.com/pgjones/quart), [Starlette](https://github.com/encode/starlette), ...).

It works by calling the ASGI app directly. This avoids the need to run the app with a http server in a different process/thread/asyncio-loop. Since the app and test run in the same asyncio loop, it's easier to write tests and debug code.

Expand Down
16 changes: 5 additions & 11 deletions async_asgi_testclient/compatibility.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""
Copyright (c) Django Software Foundation and individual contributors.
"""Copyright (c) Django Software Foundation and individual contributors.
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
Expand Down Expand Up @@ -32,9 +31,7 @@


def is_double_callable(application):
"""
Tests to see if an application is a legacy-style (double-callable) application.
"""
"""Tests to see if an application is a legacy-style (double-callable) application."""
# Look for a hint on the object first
if getattr(application, "_asgi_single_callable", False):
return False
Expand All @@ -44,7 +41,7 @@ def is_double_callable(application):
if inspect.isclass(application):
return True
# Instanted classes depend on their __call__
if hasattr(application, "__call__"):
if callable(application):
# We only check to see if its __call__ is a coroutine function -
# if it's not, it still might be a coroutine function itself.
if asyncio.iscoroutinefunction(application.__call__):
Expand All @@ -54,9 +51,7 @@ def is_double_callable(application):


def double_to_single_callable(application):
"""
Transforms a double-callable ASGI application into a single-callable one.
"""
"""Transforms a double-callable ASGI application into a single-callable one."""

async def new_application(scope, receive, send):
instance = application(scope)
Expand All @@ -66,8 +61,7 @@ async def new_application(scope, receive, send):


def guarantee_single_callable(application):
"""
Takes either a single- or double-callable application and always returns it
"""Takes either a single- or double-callable application and always returns it
in single-callable style. Use this to add backwards compatibility for ASGI
2.0 applications to your server/test harness/etc.
"""
Expand Down
16 changes: 6 additions & 10 deletions async_asgi_testclient/multipart.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
from typing import Dict
from typing import Tuple
from typing import Union

import binascii
import os
from typing import Dict, Tuple, Union


def encode_multipart_formdata(
Expand All @@ -23,12 +20,11 @@ def encode_multipart_formdata(


def build_part(boundary: str, field_name: str, file_tuple: Union[str, Tuple]) -> bytes:
"""
file_tuple:
- 'string value'
- (fileobj,)
- ('filename', fileobj)
- ('filename', fileobj, 'content_type')
"""file_tuple:
- 'string value'
- (fileobj,)
- ('filename', fileobj)
- ('filename', fileobj, 'content_type').
"""
value = b""
filename = ""
Expand Down
7 changes: 3 additions & 4 deletions async_asgi_testclient/response.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import io

from requests.exceptions import StreamConsumedError
from requests.models import Response as _Response
from requests.utils import iter_slices
from requests.utils import stream_decode_response_unicode

import io
from requests.utils import iter_slices, stream_decode_response_unicode


class BytesRW(object):
Expand Down
50 changes: 24 additions & 26 deletions async_asgi_testclient/testing.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""
Copyright P G Jones 2017.
"""Copyright P G Jones 2017.

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
Expand All @@ -22,30 +21,31 @@
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
"""
from async_asgi_testclient.compatibility import guarantee_single_callable
from async_asgi_testclient.multipart import encode_multipart_formdata
from async_asgi_testclient.response import BytesRW
from async_asgi_testclient.response import Response
from async_asgi_testclient.utils import create_monitored_task
from async_asgi_testclient.utils import flatten_headers
from async_asgi_testclient.utils import is_last_one
from async_asgi_testclient.utils import make_test_headers_path_and_query_string
from async_asgi_testclient.utils import Message
from async_asgi_testclient.utils import receive
from async_asgi_testclient.utils import to_relative_path
from async_asgi_testclient.websocket import WebSocketSession
import asyncio
import inspect
from functools import partial
from http.cookies import SimpleCookie
from json import dumps
from multidict import CIMultiDict
from typing import Any
from typing import Optional
from typing import Union
from typing import Any, Optional, Union
from urllib.parse import urlencode

import asyncio
import inspect
import requests
from multidict import CIMultiDict

from async_asgi_testclient.compatibility import guarantee_single_callable
from async_asgi_testclient.multipart import encode_multipart_formdata
from async_asgi_testclient.response import BytesRW, Response
from async_asgi_testclient.utils import (
Message,
create_monitored_task,
flatten_headers,
get_cookie_header_value,
is_last_one,
make_test_headers_path_and_query_string,
receive,
to_relative_path,
)
from async_asgi_testclient.websocket import WebSocketSession

sentinel = object()

Expand Down Expand Up @@ -110,7 +110,7 @@ async def send_lifespan(self, action):
def websocket_connect(self, *args: Any, **kwargs: Any) -> WebSocketSession:
return WebSocketSession(self, *args, **kwargs)

async def open(
async def open( # noqa: C901
self,
path: str,
*,
Expand Down Expand Up @@ -221,11 +221,9 @@ async def open(
cookie_jar = SimpleCookie(cookies)

if cookie_jar:
cookie_data = []
for cookie_name, cookie in cookie_jar.items():
cookie_data.append(f"{cookie_name}={cookie.value}")
if cookie_data:
headers.add("Cookie", "; ".join(cookie_data))
cookie_value = get_cookie_header_value(cookie_jar)
if cookie_value:
headers.add("Cookie", cookie_value)

scope = {
"type": "http",
Expand Down
1 change: 1 addition & 0 deletions async_asgi_testclient/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from async_asgi_testclient.testing import TestClient # noqa
Loading