Skip to content
Draft
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
3 changes: 2 additions & 1 deletion docs/reference.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
::: unparallel
options:
members:
members:
- up
- Up
- RequestError
43 changes: 30 additions & 13 deletions tests/test_code_in_markdown.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import functools
import re
import urllib.parse

import httpx
import pytest
import respx
from httpx import Response
Expand Down Expand Up @@ -30,20 +32,30 @@ def mock_urls(example: CodeExample):
for url in re.findall(r"\"http.+?\"", example.source):
url = url.strip('"')
host = urllib.parse.urlsplit(url).hostname
response = "mocked"
if "httpbin" in url:
response = {
"args": 42,
"data": {"foo": "bar"},
"headers": {
"Accept": "application/json",
"Authorization": "Basic qpowe3jioqoni2q1",
},
}
elif "universities.hipolabs" in url:
response = [{"page": 1}, {"page": 2}]

def my_side_effect(
request: httpx.Request,
url,
):
response = "mocked"
if "httpbin" in url:
response = {
"args": urllib.parse.parse_qs(request.url.query.decode()),
"data": {"foo": "bar"},
"headers": {
"Accept": "application/json",
"Authorization": "Basic qpowe3jioqoni2q1",
},
}
elif "universities.hipolabs" in url:
response = [{"page": 1}, {"page": 2}]
else:
response = "mocked"
return Response(200, json=response)

side_effect = functools.partial(my_side_effect, url=url)
route = respx.route(host=host, method__in=["GET", "POST", "HEAD"])
route.return_value = Response(200, json=response)
route.side_effect = side_effect


@respx.mock
Expand All @@ -68,3 +80,8 @@ def test_docs_usage(example: CodeExample, eval_example: EvalExample):
eval_example.lint(example)

eval_example.run(example)


@pytest.mark.parametrize("example", find_examples("unparallel/unparallel.py"), ids=str)
def test_docstrings(example: CodeExample, eval_example: EvalExample):
eval_example.run(example)
186 changes: 5 additions & 181 deletions tests/test_unparallel.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
import asyncio
import logging
from unittest import mock

import httpx
import pytest
from httpx import AsyncClient, Response, TimeoutException

from unparallel import up
from unparallel import Up
from unparallel.unparallel import (
DEFAULT_LIMITS,
DEFAULT_TIMEOUT,
RequestError,
request_urls,
single_request,
)

BASE_URL = "http://test.com"


def get_kwargs(*args, **kwargs):
return kwargs


@pytest.mark.asyncio
@pytest.mark.parametrize(
"url, method, payload",
Expand Down Expand Up @@ -106,70 +97,6 @@ async def test_single_request_timeout(respx_mock, retries: int):
await session.aclose()


@pytest.mark.asyncio
async def test_basic_example(respx_mock):
def query_param_value(request, i):
return Response(200, json={"i": i})

respx_mock.get(url__regex=rf"{BASE_URL}/get\?i=(?P<i>\d)").mock(
side_effect=query_param_value
)

paths = [f"/get?i={i}" for i in range(5)]
results = await up(paths, method="get", base_url=BASE_URL)
assert len(results) == 5
assert all(res["i"] == str(i) for i, res in enumerate(results))


@pytest.mark.asyncio
async def test_up_get(caplog, respx_mock):
caplog.set_level(logging.DEBUG)
urls = [
"http://test1.com/get?foo=0",
"http://test2.com/get?foo=1",
"http://test3.com/get?foo=2",
]
for url in urls:
key, val = url.split("?")[1].split("=")
respx_mock.get(url).mock(return_value=Response(200, json={key: int(val)}))

results = await up(urls, "GET")

my_log = next(rec for rec in caplog.records if rec.module == "unparallel")
assert "Issuing 3 GET request(s)" in my_log.message
assert len(results) == len(urls)
for i, (res, path) in enumerate(zip(results, urls)):
assert res == {"foo": i}
assert path.endswith(str(i))


@pytest.mark.asyncio
@pytest.mark.parametrize("paths", ["/post", ["/post"], ["/post"] * 5])
async def test_up_post_single_vs_multi_path(paths, respx_mock):
payloads = [{"bar": i} for i in range(5)]
respx_mock.post(f"{BASE_URL}/post").mock(
side_effect=[Response(200, json=data) for data in payloads]
)

results = await up(paths, "post", base_url=BASE_URL, payloads=payloads)
assert len(results) == len(payloads)
for res, data in zip(results, payloads):
assert res == data


@pytest.mark.asyncio
@pytest.mark.parametrize(
"payloads", [{"bar": 1}, [{"bar": 1}], [{"bar": i} for i in range(5)]]
)
async def test_up_post_single_vs_multi_payload(payloads, respx_mock):
paths = [f"/post/{i}" for i in range(5)]
for path in paths:
respx_mock.post(f"{BASE_URL}{path}").mock(return_value=Response(200))

results = await up(paths, "post", base_url=BASE_URL, payloads=payloads)
assert len(results) == len(paths)


@pytest.mark.asyncio
@pytest.mark.parametrize(
"flatten, expected",
Expand All @@ -179,112 +106,9 @@ async def test_up_post_single_vs_multi_payload(payloads, respx_mock):
@mock.patch(
"unparallel.unparallel.single_request", side_effect=[(1, [1, 2, 3]), (2, [4, 5, 6])]
)
async def test_request_urls_flat(patched_fetch, flatten, expected):
results = await request_urls(
urls=["/a", "/b"],
method="get",
base_url=BASE_URL,
flatten_result=flatten,
async def test_request_urls_flat(_patched_fetch, flatten, expected):
up_obj = Up(
urls=["/a", "/b"], method="get", base_url=BASE_URL, flatten_result=flatten
)
results = await up_obj.all()
assert results == expected


@pytest.mark.asyncio
async def test_up_custom_response_text(respx_mock):
urls = ["http://test.com", "http://example.com"]
for url in urls:
respx_mock.get(url).mock(return_value=Response(200, text="foobar"))

results = await up(urls, method="GET", response_fn=lambda x: x.text)

assert results == ["foobar", "foobar"]


@pytest.mark.asyncio
async def test_up_misaligned_paths_and_payloads():
with pytest.raises(ValueError):
await up(
urls=["/a", "/b"],
method="POST",
base_url=BASE_URL,
payloads=[1, 2, 3],
)


@pytest.mark.asyncio
async def test_up_wrong_method():
with pytest.raises(ValueError):
await up("/a", method="foobar", base_url=BASE_URL)


@pytest.mark.parametrize(
"up_kwargs, expected_timeouts, expected_limits",
[
({}, DEFAULT_TIMEOUT, DEFAULT_LIMITS),
(
{"timeout": 3, "max_connections": 10},
httpx.Timeout(3),
httpx.Limits(
max_connections=10,
max_keepalive_connections=DEFAULT_LIMITS.max_keepalive_connections,
),
),
(
{
"timeout": 100,
"timeouts": httpx.Timeout(2, connect=5),
"limits": httpx.Limits(max_connections=300),
},
httpx.Timeout(2, connect=5),
httpx.Limits(max_connections=300),
),
],
)
@mock.patch("unparallel.unparallel.request_urls")
@pytest.mark.asyncio
async def test_httpx_config(
request_urls_mock, up_kwargs, expected_timeouts, expected_limits
):
request_urls_mock.side_effect = get_kwargs

options = await up("/bar", base_url="foobar", **up_kwargs)
assert options["timeouts"] == expected_timeouts
assert options["limits"] == expected_limits


@pytest.mark.parametrize(
"up_kwargs, expected_sem_value",
[
({}, 100),
({"max_connections": 10}, 10),
({"max_connections": 2000}, 1000),
({"max_connections": None}, 1000),
({"semaphore_value": 42}, 42),
({"max_connections": 100, "semaphore_value": 42}, 42),
({"semaphore_value": None}, None),
],
)
@mock.patch("unparallel.unparallel.request_urls")
@pytest.mark.asyncio
async def test_up_semaphore_value(request_urls_mock, up_kwargs, expected_sem_value):
request_urls_mock.side_effect = get_kwargs

options = await up("https://example.com", **up_kwargs)

assert options["semaphore_value"] == expected_sem_value


@pytest.mark.asyncio
async def test_up_with_client(respx_mock):
auth = httpx.BasicAuth("foo", "bar")
headers = {"Authorization": auth._auth_header}
client = httpx.AsyncClient(auth=auth)
route = respx_mock.get(BASE_URL, headers=headers)

result = await up(BASE_URL, response_fn=None)
assert isinstance(result[0], RequestError)

result = await up(BASE_URL, response_fn=None, client=client)
assert result[0].status_code == 200

route.calls.assert_called_once()
Loading