Skip to content

Commit

Permalink
Add files pattern (#253)
Browse files Browse the repository at this point in the history
  • Loading branch information
lundberg authored Mar 18, 2024
1 parent 07ae887 commit 24ee4a9
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 0 deletions.
11 changes: 11 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,17 @@ Matches request *form data*, excluding files, using [eq](#eq) as default lookup.
respx.post("https://example.org/", data={"foo": "bar"})
```

### Files
Matches files within request *form data*, using [contains](#contains) as default lookup.
> Key: `files`
> Lookups: [contains](#contains), [eq](#eq)
``` python
respx.post("https://example.org/", files={"some_file": b"..."})
respx.post("https://example.org/", files={"some_file": ANY})
respx.post("https://example.org/", files={"some_file": ("filename.txt", b"...")})
respx.post("https://example.org/", files={"some_file": ("filename.txt", ANY)})
```

### JSON
Matches request *json* content, using [eq](#eq) as default lookup.
> Key: `json`
Expand Down
36 changes: 36 additions & 0 deletions respx/patterns.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json as jsonlib
import operator
import pathlib
import re
from abc import ABC
from enum import Enum
Expand All @@ -12,6 +13,7 @@
ClassVar,
Dict,
List,
Mapping,
Optional,
Pattern as RegexPattern,
Sequence,
Expand All @@ -30,8 +32,10 @@
from .types import (
URL as RawURL,
CookieTypes,
FileTypes,
HeaderTypes,
QueryParamTypes,
RequestFiles,
URLPatternTypes,
)

Expand Down Expand Up @@ -551,6 +555,38 @@ def parse(self, request: httpx.Request) -> Any:
return data


class Files(MultiItemsMixin, Pattern):
lookups = (Lookup.CONTAINS, Lookup.EQUAL)
key = "files"
value: MultiItems

def _normalize_file_value(self, value: FileTypes) -> Tuple[Any, ...]:
# Mimic httpx `FileField` to normalize `files` kwarg to shortest tuple style
if isinstance(value, tuple):
filename, fileobj = value[:2]
else:
try:
filename = pathlib.Path(str(getattr(value, "name"))).name # noqa: B009
except AttributeError:
filename = ANY
fileobj = value

return filename, fileobj

def clean(self, value: RequestFiles) -> MultiItems:
if isinstance(value, Mapping):
value = list(value.items())

files = MultiItems(
(name, self._normalize_file_value(file_value)) for name, file_value in value
)
return files

def parse(self, request: httpx.Request) -> Any:
_, files = decode_data(request)
return files


def M(*patterns: Pattern, **lookups: Any) -> Pattern:
extras = None

Expand Down
16 changes: 16 additions & 0 deletions respx/types.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import (
IO,
Any,
AsyncIterable,
Awaitable,
Expand All @@ -7,6 +8,7 @@
Iterable,
Iterator,
List,
Mapping,
Optional,
Pattern,
Sequence,
Expand Down Expand Up @@ -53,3 +55,17 @@
Type[Exception],
Iterator[SideEffectListTypes],
]

# Borrowed from HTTPX's "private" types.
FileContent = Union[IO[bytes], bytes, str]
FileTypes = Union[
# file (or bytes)
FileContent,
# (filename, file (or bytes))
Tuple[Optional[str], FileContent],
# (filename, file (or bytes), content_type)
Tuple[Optional[str], FileContent, Optional[str]],
# (filename, file (or bytes), content_type, headers)
Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]],
]
RequestFiles = Union[Mapping[str, FileTypes], Sequence[Tuple[str, FileTypes]]]
10 changes: 10 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,16 @@ def test_data_post_body():
assert route.called


def test_files_post_body():
with respx.mock:
url = "https://foo.bar/"
file = ("file", ("filename.txt", b"...", "text/plain", {"X-Foo": "bar"}))
route = respx.post(url, files={"file": mock.ANY}) % 201
response = httpx.post(url, files=[file])
assert response.status_code == 201
assert route.called


async def test_raising_content(client):
async with MockRouter() as respx_mock:
url = "https://foo.bar/"
Expand Down
107 changes: 107 additions & 0 deletions tests/test_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Content,
Cookies,
Data,
Files,
Headers,
Host,
Lookup,
Expand Down Expand Up @@ -389,6 +390,112 @@ def test_data_pattern(lookup, data, request_data, expected):
assert bool(match) is expected


@pytest.mark.parametrize(
("lookup", "files", "request_files", "expected"),
[
(
Lookup.EQUAL,
[("file_1", b"foo..."), ("file_2", b"bar...")],
None,
True,
),
(
Lookup.EQUAL,
{"file_1": b"foo...", "file_2": b"bar..."},
None,
True,
),
(
Lookup.EQUAL,
{"file_1": ANY},
{"file_1": b"foobar..."},
True,
),
(
Lookup.EQUAL,
{
"file_1": ("filename_1.txt", b"foo..."),
"file_2": ("filename_2.txt", b"bar..."),
},
None,
True,
),
(
Lookup.EQUAL,
{"file_1": ("filename_1.txt", ANY)},
{"file_1": ("filename_1.txt", b"...")},
True,
),
(
Lookup.EQUAL,
{"upload": b"foo..."},
{"upload": b"bar..."}, # Wrong file data
False,
),
(
Lookup.EQUAL,
{
"file_1": ("filename_1.txt", b"foo..."),
"file_2": ("filename_2.txt", b"bar..."),
},
{
"file_1": ("filename_1.txt", b"foo..."),
"file_2": ("filename_2.txt", b"ham..."), # Wrong file data
},
False,
),
(
Lookup.CONTAINS,
{
"file_1": ("filename_1.txt", b"foo..."),
},
{
"file_1": ("filename_1.txt", b"foo..."),
"file_2": ("filename_2.txt", b"bar..."),
},
True,
),
(
Lookup.CONTAINS,
{
"file_1": ("filename_1.txt", ANY),
},
{
"file_1": ("filename_1.txt", b"foo..."),
"file_2": ("filename_2.txt", b"bar..."),
},
True,
),
(
Lookup.CONTAINS,
[("file_1", ANY)],
{
"file_1": ("filename_1.txt", b"foo..."),
"file_2": ("filename_2.txt", b"bar..."),
},
True,
),
(
Lookup.CONTAINS,
[("file_1", b"ham...")],
{
"file_1": ("filename_1.txt", b"foo..."),
"file_2": ("filename_2.txt", b"bar..."),
},
False,
),
],
)
def test_files_pattern(lookup, files, request_files, expected):
request = httpx.Request(
"POST",
"https://foo.bar/",
files=request_files or files,
)
match = Files(files, lookup=lookup).match(request)
assert bool(match) is expected


@pytest.mark.parametrize(
("lookup", "value", "json", "expected"),
[
Expand Down

0 comments on commit 24ee4a9

Please sign in to comment.