Skip to content

Commit 85b4c22

Browse files
Extract shared display text normalization utility
Co-authored-by: Shri Sukhani <shrisukhani@users.noreply.github.com>
1 parent cc60a21 commit 85b4c22

File tree

4 files changed

+65
-57
lines changed

4 files changed

+65
-57
lines changed

hyperbrowser/client/managers/response_utils.py

Lines changed: 11 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,32 @@
11
from typing import Any, Type, TypeVar
22

3+
from hyperbrowser.display_utils import normalize_display_text
34
from hyperbrowser.exceptions import HyperbrowserError
45
from hyperbrowser.mapping_utils import read_string_key_mapping
56

67
T = TypeVar("T")
78
_MAX_OPERATION_NAME_DISPLAY_LENGTH = 120
8-
_TRUNCATED_OPERATION_NAME_SUFFIX = "... (truncated)"
99
_MAX_KEY_DISPLAY_LENGTH = 120
10-
_TRUNCATED_KEY_DISPLAY_SUFFIX = "... (truncated)"
1110

1211

1312
def _normalize_operation_name_for_error(operation_name: str) -> str:
14-
try:
15-
normalized_name = "".join(
16-
"?" if ord(character) < 32 or ord(character) == 127 else character
17-
for character in operation_name
18-
).strip()
19-
if type(normalized_name) is not str:
20-
raise TypeError("normalized operation name must be a string")
21-
except Exception:
22-
return "operation"
13+
normalized_name = normalize_display_text(
14+
operation_name,
15+
max_length=_MAX_OPERATION_NAME_DISPLAY_LENGTH,
16+
)
2317
if not normalized_name:
2418
return "operation"
25-
if len(normalized_name) <= _MAX_OPERATION_NAME_DISPLAY_LENGTH:
26-
return normalized_name
27-
available_length = _MAX_OPERATION_NAME_DISPLAY_LENGTH - len(
28-
_TRUNCATED_OPERATION_NAME_SUFFIX
29-
)
30-
if available_length <= 0:
31-
return _TRUNCATED_OPERATION_NAME_SUFFIX
32-
return f"{normalized_name[:available_length]}{_TRUNCATED_OPERATION_NAME_SUFFIX}"
19+
return normalized_name
3320

3421

3522
def _normalize_response_key_for_error(key: str) -> str:
36-
try:
37-
normalized_key = "".join(
38-
"?" if ord(character) < 32 or ord(character) == 127 else character
39-
for character in key
40-
).strip()
41-
if type(normalized_key) is not str:
42-
raise TypeError("normalized response key must be a string")
43-
except Exception:
44-
return "<unreadable key>"
23+
normalized_key = normalize_display_text(
24+
key,
25+
max_length=_MAX_KEY_DISPLAY_LENGTH,
26+
)
4527
if not normalized_key:
4628
return "<blank key>"
47-
if len(normalized_key) <= _MAX_KEY_DISPLAY_LENGTH:
48-
return normalized_key
49-
available_length = _MAX_KEY_DISPLAY_LENGTH - len(_TRUNCATED_KEY_DISPLAY_SUFFIX)
50-
if available_length <= 0:
51-
return _TRUNCATED_KEY_DISPLAY_SUFFIX
52-
return f"{normalized_key[:available_length]}{_TRUNCATED_KEY_DISPLAY_SUFFIX}"
29+
return normalized_key
5330

5431

5532
def parse_response_model(

hyperbrowser/display_utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
_TRUNCATED_DISPLAY_SUFFIX = "... (truncated)"
2+
3+
4+
def normalize_display_text(value: str, *, max_length: int) -> str:
5+
try:
6+
sanitized_value = "".join(
7+
"?" if ord(character) < 32 or ord(character) == 127 else character
8+
for character in value
9+
).strip()
10+
if type(sanitized_value) is not str:
11+
return ""
12+
if not sanitized_value:
13+
return ""
14+
if len(sanitized_value) <= max_length:
15+
return sanitized_value
16+
available_length = max_length - len(_TRUNCATED_DISPLAY_SUFFIX)
17+
if available_length <= 0:
18+
return _TRUNCATED_DISPLAY_SUFFIX
19+
return f"{sanitized_value[:available_length]}{_TRUNCATED_DISPLAY_SUFFIX}"
20+
except Exception:
21+
return ""

hyperbrowser/transport/base.py

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,17 @@
11
from abc import ABC, abstractmethod
22
from typing import Generic, Mapping, Optional, Type, TypeVar, Union
33

4+
from hyperbrowser.display_utils import normalize_display_text
45
from hyperbrowser.exceptions import HyperbrowserError
56
from hyperbrowser.mapping_utils import read_string_key_mapping
67

78
T = TypeVar("T")
8-
_TRUNCATED_DISPLAY_SUFFIX = "... (truncated)"
99
_MAX_MODEL_NAME_DISPLAY_LENGTH = 120
1010
_MAX_MAPPING_KEY_DISPLAY_LENGTH = 120
1111
_MIN_HTTP_STATUS_CODE = 100
1212
_MAX_HTTP_STATUS_CODE = 599
1313

1414

15-
def _sanitize_display_text(value: str, *, max_length: int) -> str:
16-
try:
17-
sanitized_value = "".join(
18-
"?" if ord(character) < 32 or ord(character) == 127 else character
19-
for character in value
20-
).strip()
21-
if type(sanitized_value) is not str:
22-
return ""
23-
if not sanitized_value:
24-
return ""
25-
if len(sanitized_value) <= max_length:
26-
return sanitized_value
27-
available_length = max_length - len(_TRUNCATED_DISPLAY_SUFFIX)
28-
if available_length <= 0:
29-
return _TRUNCATED_DISPLAY_SUFFIX
30-
return f"{sanitized_value[:available_length]}{_TRUNCATED_DISPLAY_SUFFIX}"
31-
except Exception:
32-
return ""
33-
34-
3515
def _safe_model_name(model: object) -> str:
3616
try:
3717
model_name = getattr(model, "__name__", "response model")
@@ -40,7 +20,7 @@ def _safe_model_name(model: object) -> str:
4020
if type(model_name) is not str:
4121
return "response model"
4222
try:
43-
normalized_model_name = _sanitize_display_text(
23+
normalized_model_name = normalize_display_text(
4424
model_name, max_length=_MAX_MODEL_NAME_DISPLAY_LENGTH
4525
)
4626
except Exception:
@@ -51,7 +31,7 @@ def _safe_model_name(model: object) -> str:
5131

5232

5333
def _format_mapping_key_for_error(key: str) -> str:
54-
normalized_key = _sanitize_display_text(
34+
normalized_key = normalize_display_text(
5535
key, max_length=_MAX_MAPPING_KEY_DISPLAY_LENGTH
5636
)
5737
if normalized_key:

tests/test_display_utils.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from hyperbrowser.display_utils import normalize_display_text
2+
3+
4+
def test_normalize_display_text_keeps_valid_input():
5+
assert normalize_display_text("hello", max_length=20) == "hello"
6+
7+
8+
def test_normalize_display_text_replaces_control_characters_and_trims():
9+
assert (
10+
normalize_display_text(" \nhello\tworld\r ", max_length=50)
11+
== "?hello?world?"
12+
)
13+
14+
15+
def test_normalize_display_text_truncates_long_values():
16+
assert (
17+
normalize_display_text("abcdefghij", max_length=7) == "... (truncated)"
18+
)
19+
20+
21+
def test_normalize_display_text_returns_empty_for_unreadable_inputs():
22+
class _BrokenString(str):
23+
def __iter__(self): # type: ignore[override]
24+
raise RuntimeError("cannot iterate")
25+
26+
assert normalize_display_text(_BrokenString("value"), max_length=20) == ""
27+
28+
29+
def test_normalize_display_text_returns_empty_for_non_string_inputs():
30+
assert normalize_display_text(123, max_length=20) == "" # type: ignore[arg-type]

0 commit comments

Comments
 (0)