Skip to content

Commit 3b59797

Browse files
committed
chore(scrapy): serialization module docstring, drop test dividers
1 parent dca36d1 commit 3b59797

2 files changed

Lines changed: 19 additions & 34 deletions

File tree

src/apify/scrapy/_serialization.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
1+
"""JSON serialization of Scrapy requests and cached responses for storage on the Apify platform.
2+
3+
Scrapy requests and cached responses are stored in the Apify request queue and key-value store,
4+
which hold JSON, so they are serialized as JSON here rather than pickled.
5+
6+
Only `body` (`bytes`) and `headers` (`{bytes: [bytes]}`) are not natively JSON-serializable; both sit at
7+
fixed keys and are base64-encoded in place. A `str` `body` is encoded as its UTF-8 bytes and comes back as
8+
`bytes`, matching Scrapy, which always stores `body` as `bytes`. Pydantic models such as Crawlee's
9+
`UserData` are dumped via `model_dump()`. Everything else, notably `meta` and `cb_kwargs`, must already be
10+
JSON-serializable, otherwise serialization fails with a clear error naming the offending value. No in-band
11+
sentinel is used, so no user value can collide with the encoding.
12+
13+
Known limitations of the pickle -> JSON switch (a documented breaking change): JSON has fewer types than
14+
pickle, so values in `meta`/`cb_kwargs` are subject to JSON's coercions. A `tuple` round-trips as a `list`
15+
and non-string `dict` keys round-trip as strings (e.g. `{1: 'a'}` becomes `{'1': 'a'}`). Values JSON cannot
16+
represent at all (`datetime`, `set`, `Decimal`, arbitrary objects, ...) are not coerced silently:
17+
serialization raises and the request is skipped loudly rather than stored in a corrupted form.
18+
"""
19+
120
from __future__ import annotations
221

322
import base64
@@ -6,22 +25,6 @@
625

726
from pydantic import BaseModel
827

9-
# Scrapy requests and cached responses are stored in the Apify request queue and key-value store,
10-
# which hold JSON, so they are serialized as JSON here rather than pickled.
11-
#
12-
# Only `body` (`bytes`) and `headers` (`{bytes: [bytes]}`) are not natively JSON-serializable; both sit at
13-
# fixed keys and are base64-encoded in place. A `str` `body` is encoded as its UTF-8 bytes and comes back as
14-
# `bytes`, matching Scrapy, which always stores `body` as `bytes`. Pydantic models such as Crawlee's
15-
# `UserData` are dumped via `model_dump()`. Everything else, notably `meta` and `cb_kwargs`, must already be
16-
# JSON-serializable, otherwise serialization fails with a clear error naming the offending value. No in-band
17-
# sentinel is used, so no user value can collide with the encoding.
18-
#
19-
# Known limitations of the pickle -> JSON switch (a documented breaking change): JSON has fewer types than
20-
# pickle, so values in `meta`/`cb_kwargs` are subject to JSON's coercions. A `tuple` round-trips as a `list`
21-
# and non-string `dict` keys round-trip as strings (e.g. `{1: 'a'}` becomes `{'1': 'a'}`). Values JSON cannot
22-
# represent at all (`datetime`, `set`, `Decimal`, arbitrary objects, ...) are not coerced silently:
23-
# serialization raises and the request is skipped loudly rather than stored in a corrupted form.
24-
2528
# Cap the offending value's repr in a serialization error message so a huge value cannot bloat the log.
2629
_MAX_ERROR_VALUE_REPR_LEN = 200
2730

tests/unit/scrapy/test_serialization.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ def _round_trip(data: dict) -> dict:
1515
return decoded
1616

1717

18-
# --- body encoding (bytes/str symmetry) ---
19-
20-
2118
def test_bytes_body_round_trips() -> None:
2219
assert _round_trip({'body': b'\x00\x01\xff binary'})['body'] == b'\x00\x01\xff binary'
2320

@@ -35,9 +32,6 @@ def test_empty_str_body_round_trips() -> None:
3532
assert _round_trip({'body': ''})['body'] == b''
3633

3734

38-
# --- headers ---
39-
40-
4135
def test_bytes_headers_round_trip() -> None:
4236
data = {'headers': {b'Content-Type': [b'text/html'], b'X-Bin': [b'\x00\xff']}}
4337
assert _round_trip(data)['headers'] == {b'Content-Type': [b'text/html'], b'X-Bin': [b'\x00\xff']}
@@ -53,9 +47,6 @@ def test_bare_header_value_is_normalized_to_list() -> None:
5347
assert _round_trip({'headers': {b'X-Single': b'one'}})['headers'] == {b'X-Single': [b'one']}
5448

5549

56-
# --- non-ASCII text is kept as UTF-8 (ensure_ascii=False) ---
57-
58-
5950
def test_non_ascii_is_not_escaped() -> None:
6051
"""Non-ASCII text stays in its UTF-8 form instead of ASCII escape sequences, which would bloat storage."""
6152
encoded = encode_to_json({'meta': {'name': 'Ñoño café 日本語'}})
@@ -64,9 +55,6 @@ def test_non_ascii_is_not_escaped() -> None:
6455
assert decode_from_json(encoded)['meta']['name'] == 'Ñoño café 日本語'
6556

6657

67-
# --- pydantic models are dumped ---
68-
69-
7058
def test_pydantic_model_is_dumped_by_alias() -> None:
7159
class Model(BaseModel):
7260
first: int = Field(serialization_alias='First')
@@ -75,9 +63,6 @@ class Model(BaseModel):
7563
assert decode_from_json(encoded)['meta']['m'] == {'First': 1}
7664

7765

78-
# --- documented JSON coercions (a breaking change vs. pickle) ---
79-
80-
8166
def test_tuple_is_coerced_to_list() -> None:
8267
"""Documented limitation: JSON has no tuple type, so a tuple round-trips as a list."""
8368
assert _round_trip({'meta': {'coords': (1, 2, 3)}})['meta']['coords'] == [1, 2, 3]
@@ -88,9 +73,6 @@ def test_non_string_dict_keys_are_coerced_to_strings() -> None:
8873
assert _round_trip({'cb_kwargs': {'m': {1: 'a'}}})['cb_kwargs']['m'] == {'1': 'a'}
8974

9075

91-
# --- values JSON cannot represent fail loudly with a useful message ---
92-
93-
9476
def test_non_serializable_value_raises_with_type_and_repr() -> None:
9577
"""A value JSON cannot represent raises a `TypeError` naming the offending type and value."""
9678
when = datetime(2020, 1, 2, 3, 4, 5, tzinfo=UTC)

0 commit comments

Comments
 (0)