Skip to content

Commit c1bfa49

Browse files
Centralize plain-type helper utilities
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent 86f6853 commit c1bfa49

File tree

9 files changed

+95
-19
lines changed

9 files changed

+95
-19
lines changed

hyperbrowser/client/managers/async_manager/computer_action.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pydantic import BaseModel
22
from typing import Union, List, Optional
33
from hyperbrowser.exceptions import HyperbrowserError
4+
from hyperbrowser.type_utils import is_string_subclass_instance
45
from ..response_utils import parse_response_model
56
from ..serialization_utils import serialize_model_dump_to_dict
67
from hyperbrowser.models import (
@@ -33,7 +34,7 @@ async def _execute_request(
3334
) -> ComputerActionResponse:
3435
if type(session) is str:
3536
session = await self._client.sessions.get(session)
36-
elif type(session) is not str and str in type(session).__mro__:
37+
elif is_string_subclass_instance(session):
3738
raise HyperbrowserError(
3839
"session must be a plain string session ID or SessionDetail"
3940
)

hyperbrowser/client/managers/async_manager/session.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import IO, List, Optional, Union, overload
44
import warnings
55
from hyperbrowser.exceptions import HyperbrowserError
6+
from hyperbrowser.type_utils import is_string_subclass_instance
67
from ...file_utils import ensure_existing_file_path
78
from ..serialization_utils import serialize_model_dump_to_dict
89
from ..session_utils import (
@@ -192,7 +193,7 @@ async def upload_file(
192193
f"Failed to open upload file at path: {file_path}",
193194
original_error=exc,
194195
) from exc
195-
elif type(file_input) is not str and str in type(file_input).__mro__:
196+
elif is_string_subclass_instance(file_input):
196197
raise HyperbrowserError("file_input path must be a plain string path")
197198
else:
198199
try:

hyperbrowser/client/managers/sync_manager/computer_action.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from pydantic import BaseModel
22
from typing import Union, List, Optional
33
from hyperbrowser.exceptions import HyperbrowserError
4+
from hyperbrowser.type_utils import is_string_subclass_instance
45
from ..response_utils import parse_response_model
56
from ..serialization_utils import serialize_model_dump_to_dict
67
from hyperbrowser.models import (
@@ -33,7 +34,7 @@ def _execute_request(
3334
) -> ComputerActionResponse:
3435
if type(session) is str:
3536
session = self._client.sessions.get(session)
36-
elif type(session) is not str and str in type(session).__mro__:
37+
elif is_string_subclass_instance(session):
3738
raise HyperbrowserError(
3839
"session must be a plain string session ID or SessionDetail"
3940
)

hyperbrowser/client/managers/sync_manager/session.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import IO, List, Optional, Union, overload
44
import warnings
55
from hyperbrowser.exceptions import HyperbrowserError
6+
from hyperbrowser.type_utils import is_string_subclass_instance
67
from ...file_utils import ensure_existing_file_path
78
from ..serialization_utils import serialize_model_dump_to_dict
89
from ..session_utils import (
@@ -184,7 +185,7 @@ def upload_file(
184185
f"Failed to open upload file at path: {file_path}",
185186
original_error=exc,
186187
) from exc
187-
elif type(file_input) is not str and str in type(file_input).__mro__:
188+
elif is_string_subclass_instance(file_input):
188189
raise HyperbrowserError("file_input path must be a plain string path")
189190
else:
190191
try:

hyperbrowser/models/session.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@
1414
SessionRegion,
1515
SessionEventLogType,
1616
)
17+
from hyperbrowser.type_utils import (
18+
is_int_subclass_instance,
19+
is_plain_int,
20+
is_plain_string,
21+
is_string_subclass_instance,
22+
)
1723

1824
SessionStatus = Literal["active", "closed", "error"]
1925

@@ -149,20 +155,20 @@ def parse_timestamp(cls, value: Optional[Union[str, int]]) -> Optional[int]:
149155
raise ValueError(
150156
"timestamp values must be integers or plain numeric strings"
151157
)
152-
if value_type is int:
158+
if is_plain_int(value):
153159
return value
154-
if int in value_type.__mro__:
160+
if is_int_subclass_instance(value):
155161
raise ValueError(
156162
"timestamp values must be plain integers or plain numeric strings"
157163
)
158-
if value_type is str:
164+
if is_plain_string(value):
159165
try:
160166
return int(value)
161167
except Exception as exc:
162168
raise ValueError(
163169
"timestamp string values must be integer-formatted"
164170
) from exc
165-
if str in value_type.__mro__:
171+
if is_string_subclass_instance(value):
166172
raise ValueError("timestamp string values must be plain strings")
167173
raise ValueError(
168174
"timestamp values must be plain integers or plain numeric strings"

hyperbrowser/tools/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
copy_mapping_values_by_string_keys,
1010
read_string_mapping_keys,
1111
)
12+
from hyperbrowser.type_utils import is_string_subclass_instance
1213
from hyperbrowser.models.agents.browser_use import StartBrowserUseTaskParams
1314
from hyperbrowser.models.crawl import StartCrawlJobParams
1415
from hyperbrowser.models.extract import StartExtractJobParams
@@ -207,7 +208,7 @@ def _normalize_optional_text_field_value(
207208
error_message,
208209
original_error=exc,
209210
) from exc
210-
if type(field_value) is not str and str in type(field_value).__mro__:
211+
if is_string_subclass_instance(field_value):
211212
raise HyperbrowserError(error_message)
212213
if isinstance(field_value, (bytes, bytearray, memoryview)):
213214
try:

hyperbrowser/transport/error_utils.py

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
from typing import Any
66

77
import httpx
8+
from hyperbrowser.type_utils import (
9+
is_plain_string as _is_plain_string,
10+
is_string_subclass_instance as _is_string_subclass_instance,
11+
)
812

913
_HTTP_METHOD_TOKEN_PATTERN = re.compile(r"^[!#$%&'*+\-.^_`|~0-9A-Z]+$")
1014
_NUMERIC_LIKE_URL_PATTERN = re.compile(
@@ -45,16 +49,6 @@
4549
"-infinity",
4650
}
4751

48-
49-
def _is_plain_string(value: Any) -> bool:
50-
return type(value) is str
51-
52-
53-
def _is_string_subclass_instance(value: Any) -> bool:
54-
value_type = type(value)
55-
return value_type is not str and str in value_type.__mro__
56-
57-
5852
def _safe_to_string(value: Any) -> str:
5953
try:
6054
normalized_value = str(value)

hyperbrowser/type_utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
from typing import Any, Type
2+
3+
4+
def is_plain_instance(value: Any, expected_type: Type[object]) -> bool:
5+
return type(value) is expected_type
6+
7+
8+
def is_subclass_instance(value: Any, expected_type: Type[object]) -> bool:
9+
value_type = type(value)
10+
return value_type is not expected_type and expected_type in value_type.__mro__
11+
12+
13+
def is_plain_string(value: Any) -> bool:
14+
return is_plain_instance(value, str)
15+
16+
17+
def is_string_subclass_instance(value: Any) -> bool:
18+
return is_subclass_instance(value, str)
19+
20+
21+
def is_plain_int(value: Any) -> bool:
22+
return is_plain_instance(value, int)
23+
24+
25+
def is_int_subclass_instance(value: Any) -> bool:
26+
return is_subclass_instance(value, int)

tests/test_type_utils.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from hyperbrowser.type_utils import (
2+
is_int_subclass_instance,
3+
is_plain_instance,
4+
is_plain_int,
5+
is_plain_string,
6+
is_string_subclass_instance,
7+
is_subclass_instance,
8+
)
9+
10+
11+
def test_is_plain_instance_requires_concrete_type_match():
12+
assert is_plain_instance("value", str) is True
13+
assert is_plain_instance(10, int) is True
14+
assert is_plain_instance(True, int) is False
15+
assert is_plain_instance("value", int) is False
16+
17+
18+
def test_is_subclass_instance_detects_string_subclasses_only():
19+
class _StringSubclass(str):
20+
pass
21+
22+
assert is_subclass_instance(_StringSubclass("value"), str) is True
23+
assert is_subclass_instance("value", str) is False
24+
assert is_subclass_instance(10, str) is False
25+
26+
27+
def test_string_helpers_enforce_plain_string_boundaries():
28+
class _StringSubclass(str):
29+
pass
30+
31+
assert is_plain_string("value") is True
32+
assert is_plain_string(_StringSubclass("value")) is False
33+
assert is_string_subclass_instance("value") is False
34+
assert is_string_subclass_instance(_StringSubclass("value")) is True
35+
36+
37+
def test_int_helpers_enforce_plain_integer_boundaries():
38+
class _IntSubclass(int):
39+
pass
40+
41+
assert is_plain_int(10) is True
42+
assert is_plain_int(_IntSubclass(10)) is False
43+
assert is_int_subclass_instance(10) is False
44+
assert is_int_subclass_instance(_IntSubclass(10)) is True
45+
assert is_int_subclass_instance(True) is True

0 commit comments

Comments
 (0)