Skip to content

Commit dd7108c

Browse files
Centralize extension create input normalization
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 54cf519 commit dd7108c

File tree

7 files changed

+177
-48
lines changed

7 files changed

+177
-48
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ This runs lint, format checks, compile checks, tests, and package build.
8888
- `tests/test_example_sync_async_parity.py` (sync/async example parity enforcement),
8989
- `tests/test_examples_naming_convention.py` (example sync/async prefix naming enforcement),
9090
- `tests/test_examples_syntax.py` (example script syntax guardrail),
91+
- `tests/test_extension_create_helper_usage.py` (extension create-input normalization helper usage enforcement),
9192
- `tests/test_guardrail_ast_utils.py` (shared AST guard utility contract),
9293
- `tests/test_job_pagination_helper_usage.py` (shared scrape/crawl pagination helper usage enforcement),
9394
- `tests/test_makefile_quality_targets.py` (Makefile quality-gate target enforcement),

hyperbrowser/client/managers/async_manager/extension.py

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
from typing import List
22

33
from hyperbrowser.exceptions import HyperbrowserError
4-
from ...file_utils import ensure_existing_file_path
5-
from ..serialization_utils import serialize_model_dump_to_dict
4+
from ..extension_create_utils import normalize_extension_create_input
65
from ..extension_utils import parse_extension_list_response_data
76
from ..response_utils import parse_response_model
87
from hyperbrowser.models.extension import CreateExtensionParams, ExtensionResponse
@@ -13,28 +12,7 @@ def __init__(self, client):
1312
self._client = client
1413

1514
async def create(self, params: CreateExtensionParams) -> ExtensionResponse:
16-
if type(params) is not CreateExtensionParams:
17-
raise HyperbrowserError("params must be CreateExtensionParams")
18-
try:
19-
raw_file_path = params.file_path
20-
except HyperbrowserError:
21-
raise
22-
except Exception as exc:
23-
raise HyperbrowserError(
24-
"params.file_path is invalid",
25-
original_error=exc,
26-
) from exc
27-
payload = serialize_model_dump_to_dict(
28-
params,
29-
error_message="Failed to serialize extension create params",
30-
)
31-
payload.pop("filePath", None)
32-
33-
file_path = ensure_existing_file_path(
34-
raw_file_path,
35-
missing_file_message=f"Extension file not found at path: {raw_file_path}",
36-
not_file_message=f"Extension file path must point to a file: {raw_file_path}",
37-
)
15+
file_path, payload = normalize_extension_create_input(params)
3816

3917
try:
4018
with open(file_path, "rb") as extension_file:
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from typing import Any, Dict, Tuple
2+
3+
from hyperbrowser.exceptions import HyperbrowserError
4+
from hyperbrowser.models.extension import CreateExtensionParams
5+
6+
from ..file_utils import ensure_existing_file_path
7+
from .serialization_utils import serialize_model_dump_to_dict
8+
9+
10+
def normalize_extension_create_input(params: Any) -> Tuple[str, Dict[str, Any]]:
11+
if type(params) is not CreateExtensionParams:
12+
raise HyperbrowserError("params must be CreateExtensionParams")
13+
try:
14+
raw_file_path = params.file_path
15+
except HyperbrowserError:
16+
raise
17+
except Exception as exc:
18+
raise HyperbrowserError(
19+
"params.file_path is invalid",
20+
original_error=exc,
21+
) from exc
22+
23+
payload = serialize_model_dump_to_dict(
24+
params,
25+
error_message="Failed to serialize extension create params",
26+
)
27+
payload.pop("filePath", None)
28+
29+
file_path = ensure_existing_file_path(
30+
raw_file_path,
31+
missing_file_message=f"Extension file not found at path: {raw_file_path}",
32+
not_file_message=f"Extension file path must point to a file: {raw_file_path}",
33+
)
34+
return file_path, payload

hyperbrowser/client/managers/sync_manager/extension.py

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
from typing import List
22

33
from hyperbrowser.exceptions import HyperbrowserError
4-
from ...file_utils import ensure_existing_file_path
5-
from ..serialization_utils import serialize_model_dump_to_dict
4+
from ..extension_create_utils import normalize_extension_create_input
65
from ..extension_utils import parse_extension_list_response_data
76
from ..response_utils import parse_response_model
87
from hyperbrowser.models.extension import CreateExtensionParams, ExtensionResponse
@@ -13,28 +12,7 @@ def __init__(self, client):
1312
self._client = client
1413

1514
def create(self, params: CreateExtensionParams) -> ExtensionResponse:
16-
if type(params) is not CreateExtensionParams:
17-
raise HyperbrowserError("params must be CreateExtensionParams")
18-
try:
19-
raw_file_path = params.file_path
20-
except HyperbrowserError:
21-
raise
22-
except Exception as exc:
23-
raise HyperbrowserError(
24-
"params.file_path is invalid",
25-
original_error=exc,
26-
) from exc
27-
payload = serialize_model_dump_to_dict(
28-
params,
29-
error_message="Failed to serialize extension create params",
30-
)
31-
payload.pop("filePath", None)
32-
33-
file_path = ensure_existing_file_path(
34-
raw_file_path,
35-
missing_file_message=f"Extension file not found at path: {raw_file_path}",
36-
not_file_message=f"Extension file path must point to a file: {raw_file_path}",
37-
)
15+
file_path, payload = normalize_extension_create_input(params)
3816

3917
try:
4018
with open(file_path, "rb") as extension_file:

tests/test_architecture_marker_usage.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"tests/test_readme_examples_listing.py",
3030
"tests/test_examples_syntax.py",
3131
"tests/test_docs_python3_commands.py",
32+
"tests/test_extension_create_helper_usage.py",
3233
"tests/test_examples_naming_convention.py",
3334
"tests/test_job_pagination_helper_usage.py",
3435
"tests/test_example_sync_async_parity.py",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
pytestmark = pytest.mark.architecture
6+
7+
8+
EXTENSION_MANAGER_MODULES = (
9+
"hyperbrowser/client/managers/sync_manager/extension.py",
10+
"hyperbrowser/client/managers/async_manager/extension.py",
11+
)
12+
13+
14+
def test_extension_managers_use_shared_extension_create_helper():
15+
for module_path in EXTENSION_MANAGER_MODULES:
16+
module_text = Path(module_path).read_text(encoding="utf-8")
17+
assert "normalize_extension_create_input(" in module_text
18+
assert "serialize_model_dump_to_dict(" not in module_text
19+
assert "ensure_existing_file_path(" not in module_text
20+
assert "params.file_path is invalid" not in module_text
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
from pathlib import Path
2+
from types import MappingProxyType
3+
4+
import pytest
5+
6+
from hyperbrowser.client.managers.extension_create_utils import (
7+
normalize_extension_create_input,
8+
)
9+
from hyperbrowser.exceptions import HyperbrowserError
10+
from hyperbrowser.models.extension import CreateExtensionParams
11+
12+
13+
def _create_test_extension_zip(tmp_path: Path) -> Path:
14+
file_path = tmp_path / "extension.zip"
15+
file_path.write_bytes(b"extension-bytes")
16+
return file_path
17+
18+
19+
def test_normalize_extension_create_input_returns_file_path_and_payload(tmp_path):
20+
file_path = _create_test_extension_zip(tmp_path)
21+
params = CreateExtensionParams(name="my-extension", file_path=file_path)
22+
23+
normalized_path, payload = normalize_extension_create_input(params)
24+
25+
assert normalized_path == str(file_path.resolve())
26+
assert payload == {"name": "my-extension"}
27+
28+
29+
def test_normalize_extension_create_input_rejects_invalid_param_type():
30+
with pytest.raises(HyperbrowserError, match="params must be CreateExtensionParams"):
31+
normalize_extension_create_input({"name": "bad"}) # type: ignore[arg-type]
32+
33+
34+
def test_normalize_extension_create_input_rejects_subclass_param_type(tmp_path):
35+
class _Params(CreateExtensionParams):
36+
pass
37+
38+
params = _Params(name="bad", file_path=_create_test_extension_zip(tmp_path))
39+
40+
with pytest.raises(HyperbrowserError, match="params must be CreateExtensionParams"):
41+
normalize_extension_create_input(params)
42+
43+
44+
def test_normalize_extension_create_input_wraps_serialization_errors(
45+
tmp_path, monkeypatch: pytest.MonkeyPatch
46+
):
47+
params = CreateExtensionParams(
48+
name="serialize-extension",
49+
file_path=_create_test_extension_zip(tmp_path),
50+
)
51+
52+
def _raise_model_dump_error(*args, **kwargs):
53+
_ = args
54+
_ = kwargs
55+
raise RuntimeError("broken model_dump")
56+
57+
monkeypatch.setattr(CreateExtensionParams, "model_dump", _raise_model_dump_error)
58+
59+
with pytest.raises(
60+
HyperbrowserError, match="Failed to serialize extension create params"
61+
) as exc_info:
62+
normalize_extension_create_input(params)
63+
64+
assert isinstance(exc_info.value.original_error, RuntimeError)
65+
66+
67+
def test_normalize_extension_create_input_preserves_hyperbrowser_serialization_errors(
68+
tmp_path, monkeypatch: pytest.MonkeyPatch
69+
):
70+
params = CreateExtensionParams(
71+
name="serialize-extension",
72+
file_path=_create_test_extension_zip(tmp_path),
73+
)
74+
75+
def _raise_model_dump_error(*args, **kwargs):
76+
_ = args
77+
_ = kwargs
78+
raise HyperbrowserError("custom model_dump failure")
79+
80+
monkeypatch.setattr(CreateExtensionParams, "model_dump", _raise_model_dump_error)
81+
82+
with pytest.raises(
83+
HyperbrowserError, match="custom model_dump failure"
84+
) as exc_info:
85+
normalize_extension_create_input(params)
86+
87+
assert exc_info.value.original_error is None
88+
89+
90+
def test_normalize_extension_create_input_rejects_non_dict_serialized_payload(
91+
tmp_path, monkeypatch: pytest.MonkeyPatch
92+
):
93+
params = CreateExtensionParams(
94+
name="serialize-extension",
95+
file_path=_create_test_extension_zip(tmp_path),
96+
)
97+
98+
monkeypatch.setattr(
99+
CreateExtensionParams,
100+
"model_dump",
101+
lambda *args, **kwargs: MappingProxyType({"name": "my-extension"}),
102+
)
103+
104+
with pytest.raises(
105+
HyperbrowserError, match="Failed to serialize extension create params"
106+
) as exc_info:
107+
normalize_extension_create_input(params)
108+
109+
assert exc_info.value.original_error is None
110+
111+
112+
def test_normalize_extension_create_input_rejects_missing_file(tmp_path):
113+
missing_path = tmp_path / "missing-extension.zip"
114+
params = CreateExtensionParams(name="missing-extension", file_path=missing_path)
115+
116+
with pytest.raises(HyperbrowserError, match="Extension file not found"):
117+
normalize_extension_create_input(params)

0 commit comments

Comments
 (0)