Skip to content

Commit 106c606

Browse files
Extract shared mapping-list parser helper
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 8db848b commit 106c606

4 files changed

Lines changed: 188 additions & 91 deletions

File tree

hyperbrowser/client/managers/extension_utils.py

Lines changed: 7 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from hyperbrowser.exceptions import HyperbrowserError
55
from hyperbrowser.models.extension import ExtensionResponse
6+
from .list_parsing_utils import parse_mapping_list_items
67

78
_MAX_DISPLAYED_MISSING_KEYS = 20
89
_MAX_DISPLAYED_MISSING_KEY_LENGTH = 120
@@ -108,48 +109,9 @@ def parse_extension_list_response_data(response_data: Any) -> List[ExtensionResp
108109
"Failed to iterate 'extensions' list from response",
109110
original_error=exc,
110111
) from exc
111-
parsed_extensions: List[ExtensionResponse] = []
112-
for index, extension in enumerate(extension_items):
113-
if not isinstance(extension, Mapping):
114-
raise HyperbrowserError(
115-
"Expected extension object at index "
116-
f"{index} but got {_get_type_name(extension)}"
117-
)
118-
try:
119-
extension_keys = list(extension.keys())
120-
except HyperbrowserError:
121-
raise
122-
except Exception as exc:
123-
raise HyperbrowserError(
124-
f"Failed to read extension object at index {index}",
125-
original_error=exc,
126-
) from exc
127-
for key in extension_keys:
128-
if type(key) is str:
129-
continue
130-
raise HyperbrowserError(
131-
f"Expected extension object keys to be strings at index {index}"
132-
)
133-
extension_payload: dict[str, object] = {}
134-
for key in extension_keys:
135-
try:
136-
extension_payload[key] = extension[key]
137-
except HyperbrowserError:
138-
raise
139-
except Exception as exc:
140-
key_display = _format_key_display(key)
141-
raise HyperbrowserError(
142-
"Failed to read extension object value for key "
143-
f"'{key_display}' at index {index}",
144-
original_error=exc,
145-
) from exc
146-
try:
147-
parsed_extensions.append(ExtensionResponse(**extension_payload))
148-
except HyperbrowserError:
149-
raise
150-
except Exception as exc:
151-
raise HyperbrowserError(
152-
f"Failed to parse extension at index {index}",
153-
original_error=exc,
154-
) from exc
155-
return parsed_extensions
112+
return parse_mapping_list_items(
113+
extension_items,
114+
item_label="extension",
115+
parse_item=lambda extension_payload: ExtensionResponse(**extension_payload),
116+
key_display=_format_key_display,
117+
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from collections.abc import Mapping
2+
from typing import Any, Callable, List, TypeVar
3+
4+
from hyperbrowser.exceptions import HyperbrowserError
5+
6+
T = TypeVar("T")
7+
8+
9+
def parse_mapping_list_items(
10+
items: List[Any],
11+
*,
12+
item_label: str,
13+
parse_item: Callable[[dict[str, object]], T],
14+
key_display: Callable[[str], str],
15+
) -> List[T]:
16+
parsed_items: List[T] = []
17+
for index, item in enumerate(items):
18+
if not isinstance(item, Mapping):
19+
raise HyperbrowserError(
20+
f"Expected {item_label} object at index {index} but got {type(item).__name__}"
21+
)
22+
try:
23+
item_keys = list(item.keys())
24+
except HyperbrowserError:
25+
raise
26+
except Exception as exc:
27+
raise HyperbrowserError(
28+
f"Failed to read {item_label} object at index {index}",
29+
original_error=exc,
30+
) from exc
31+
for key in item_keys:
32+
if type(key) is str:
33+
continue
34+
raise HyperbrowserError(
35+
f"Expected {item_label} object keys to be strings at index {index}"
36+
)
37+
item_payload: dict[str, object] = {}
38+
for key in item_keys:
39+
try:
40+
item_payload[key] = item[key]
41+
except HyperbrowserError:
42+
raise
43+
except Exception as exc:
44+
raise HyperbrowserError(
45+
f"Failed to read {item_label} object value for key '{key_display(key)}' at index {index}",
46+
original_error=exc,
47+
) from exc
48+
try:
49+
parsed_items.append(parse_item(item_payload))
50+
except HyperbrowserError:
51+
raise
52+
except Exception as exc:
53+
raise HyperbrowserError(
54+
f"Failed to parse {item_label} at index {index}",
55+
original_error=exc,
56+
) from exc
57+
return parsed_items

hyperbrowser/client/managers/session_utils.py

Lines changed: 7 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from collections.abc import Mapping
21
from typing import Any, List, Type, TypeVar
32

43
from hyperbrowser.exceptions import HyperbrowserError
54
from hyperbrowser.models.session import SessionRecording
5+
from .list_parsing_utils import parse_mapping_list_items
66
from .response_utils import parse_response_model
77

88
T = TypeVar("T")
@@ -59,48 +59,9 @@ def parse_session_recordings_response_data(
5959
"Failed to iterate session recording response list",
6060
original_error=exc,
6161
) from exc
62-
parsed_recordings: List[SessionRecording] = []
63-
for index, recording in enumerate(recording_items):
64-
if not isinstance(recording, Mapping):
65-
raise HyperbrowserError(
66-
"Expected session recording object at index "
67-
f"{index} but got {type(recording).__name__}"
68-
)
69-
try:
70-
recording_keys = list(recording.keys())
71-
except HyperbrowserError:
72-
raise
73-
except Exception as exc:
74-
raise HyperbrowserError(
75-
f"Failed to read session recording object at index {index}",
76-
original_error=exc,
77-
) from exc
78-
for key in recording_keys:
79-
if type(key) is str:
80-
continue
81-
raise HyperbrowserError(
82-
f"Expected session recording object keys to be strings at index {index}"
83-
)
84-
recording_payload: dict[str, object] = {}
85-
for key in recording_keys:
86-
try:
87-
recording_payload[key] = recording[key]
88-
except HyperbrowserError:
89-
raise
90-
except Exception as exc:
91-
key_display = _format_recording_key_display(key)
92-
raise HyperbrowserError(
93-
"Failed to read session recording object value for key "
94-
f"'{key_display}' at index {index}",
95-
original_error=exc,
96-
) from exc
97-
try:
98-
parsed_recordings.append(SessionRecording(**recording_payload))
99-
except HyperbrowserError:
100-
raise
101-
except Exception as exc:
102-
raise HyperbrowserError(
103-
f"Failed to parse session recording at index {index}",
104-
original_error=exc,
105-
) from exc
106-
return parsed_recordings
62+
return parse_mapping_list_items(
63+
recording_items,
64+
item_label="session recording",
65+
parse_item=lambda recording_payload: SessionRecording(**recording_payload),
66+
key_display=_format_recording_key_display,
67+
)

tests/test_list_parsing_utils.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
from collections.abc import Mapping
2+
from typing import Iterator
3+
4+
import pytest
5+
6+
from hyperbrowser.client.managers.list_parsing_utils import parse_mapping_list_items
7+
from hyperbrowser.exceptions import HyperbrowserError
8+
9+
10+
class _ExplodingKeysMapping(Mapping[object, object]):
11+
def __getitem__(self, key: object) -> object:
12+
_ = key
13+
return "value"
14+
15+
def __iter__(self) -> Iterator[object]:
16+
return iter(())
17+
18+
def __len__(self) -> int:
19+
return 0
20+
21+
def keys(self): # type: ignore[override]
22+
raise RuntimeError("broken keys")
23+
24+
25+
class _ExplodingValueMapping(Mapping[object, object]):
26+
def __getitem__(self, key: object) -> object:
27+
_ = key
28+
raise RuntimeError("broken getitem")
29+
30+
def __iter__(self) -> Iterator[object]:
31+
return iter(("field",))
32+
33+
def __len__(self) -> int:
34+
return 1
35+
36+
def keys(self): # type: ignore[override]
37+
return ("field",)
38+
39+
40+
def test_parse_mapping_list_items_parses_each_mapping():
41+
parsed = parse_mapping_list_items(
42+
[{"id": "a"}, {"id": "b"}],
43+
item_label="demo",
44+
parse_item=lambda payload: payload["id"],
45+
key_display=lambda key: key,
46+
)
47+
48+
assert parsed == ["a", "b"]
49+
50+
51+
def test_parse_mapping_list_items_rejects_non_mapping_items():
52+
with pytest.raises(
53+
HyperbrowserError, match="Expected demo object at index 0 but got list"
54+
):
55+
parse_mapping_list_items(
56+
[[]],
57+
item_label="demo",
58+
parse_item=lambda payload: payload,
59+
key_display=lambda key: key,
60+
)
61+
62+
63+
def test_parse_mapping_list_items_wraps_key_read_failures():
64+
with pytest.raises(
65+
HyperbrowserError, match="Failed to read demo object at index 0"
66+
) as exc_info:
67+
parse_mapping_list_items(
68+
[_ExplodingKeysMapping()],
69+
item_label="demo",
70+
parse_item=lambda payload: payload,
71+
key_display=lambda key: key,
72+
)
73+
74+
assert isinstance(exc_info.value.original_error, RuntimeError)
75+
76+
77+
def test_parse_mapping_list_items_rejects_non_string_keys():
78+
with pytest.raises(
79+
HyperbrowserError,
80+
match="Expected demo object keys to be strings at index 0",
81+
):
82+
parse_mapping_list_items(
83+
[{1: "value"}],
84+
item_label="demo",
85+
parse_item=lambda payload: payload,
86+
key_display=lambda key: key,
87+
)
88+
89+
90+
def test_parse_mapping_list_items_wraps_value_read_failures():
91+
with pytest.raises(
92+
HyperbrowserError,
93+
match="Failed to read demo object value for key 'field' at index 0",
94+
) as exc_info:
95+
parse_mapping_list_items(
96+
[_ExplodingValueMapping()],
97+
item_label="demo",
98+
parse_item=lambda payload: payload,
99+
key_display=lambda key: key,
100+
)
101+
102+
assert isinstance(exc_info.value.original_error, RuntimeError)
103+
104+
105+
def test_parse_mapping_list_items_wraps_parse_failures():
106+
with pytest.raises(
107+
HyperbrowserError,
108+
match="Failed to parse demo at index 0",
109+
) as exc_info:
110+
parse_mapping_list_items(
111+
[{"id": "x"}],
112+
item_label="demo",
113+
parse_item=lambda payload: 1 / 0,
114+
key_display=lambda key: key,
115+
)
116+
117+
assert isinstance(exc_info.value.original_error, ZeroDivisionError)

0 commit comments

Comments
 (0)