Skip to content

Commit 3bbb4b7

Browse files
committed
chore: move value serialization into a shared module
1 parent ac0dd3c commit 3bbb4b7

6 files changed

Lines changed: 276 additions & 232 deletions

File tree

‎playwright/_impl/_assertions.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
FrameExpectOptions,
2828
FrameExpectResult,
2929
)
30-
from playwright._impl._js_handle import parse_value
3130
from playwright._impl._locator import Locator
3231
from playwright._impl._page import Page
32+
from playwright._impl._serializers import parse_value
3333
from playwright._impl._str_utils import escape_regex_flags
3434

3535
_soft_errors: Optional[List[AssertionError]] = None

‎playwright/_impl/_frame.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@
6161
Serializable,
6262
add_source_url_to_script,
6363
parse_result,
64-
parse_value,
6564
serialize_argument,
6665
)
6766
from playwright._impl._locator import (
@@ -78,6 +77,7 @@
7877
test_id_attribute_name,
7978
)
8079
from playwright._impl._network import Response
80+
from playwright._impl._serializers import parse_value
8181
from playwright._impl._set_input_files_helpers import convert_input_files
8282
from playwright._impl._waiter import Waiter
8383
from playwright._impl._web_mcp import WebMCP

‎playwright/_impl/_js_handle.py‎

Lines changed: 11 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -12,21 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import base64
16-
import collections.abc
17-
import datetime
18-
import math
19-
import re
20-
import struct
21-
import traceback
15+
# Upstream: packages/playwright-core/src/client/jsHandle.ts.
16+
2217
from pathlib import Path
23-
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Pattern, Union
24-
from urllib.parse import ParseResult, urlparse, urlunparse
18+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
2519

2620
from playwright._impl._connection import Channel, ChannelOwner, from_channel
27-
from playwright._impl._errors import Error, is_target_closed_error
28-
from playwright._impl._map import Map
29-
from playwright._impl._str_utils import escape_regex_flags, parse_regex_flags
21+
from playwright._impl._errors import is_target_closed_error
22+
from playwright._impl._serializers import parse_value, serialize_value
3023

3124
if TYPE_CHECKING: # pragma: no cover
3225
from playwright._impl._element_handle import ElementHandle
@@ -35,21 +28,6 @@
3528
Serializable = Any
3629

3730

38-
class VisitorInfo:
39-
visited: Map[Any, int]
40-
last_id: int
41-
42-
def __init__(self) -> None:
43-
self.visited = Map()
44-
self.last_id = 0
45-
46-
def visit(self, obj: Any) -> int:
47-
assert obj not in self.visited
48-
self.last_id += 1
49-
self.visited[obj] = self.last_id
50-
return self.last_id
51-
52-
5331
class JSHandle(ChannelOwner):
5432
def __init__(
5533
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
@@ -131,214 +109,17 @@ async def json_value(self) -> Any:
131109
)
132110

133111

134-
def serialize_value(
135-
value: Any, handles: List[Channel], visitor_info: Optional[VisitorInfo] = None
136-
) -> Any:
137-
if visitor_info is None:
138-
visitor_info = VisitorInfo()
139-
if isinstance(value, JSHandle):
140-
h = len(handles)
141-
handles.append(value._channel)
142-
return dict(h=h)
143-
if value is None:
144-
return dict(v="null")
145-
if isinstance(value, float):
146-
if value == float("inf"):
147-
return dict(v="Infinity")
148-
if value == float("-inf"):
149-
return dict(v="-Infinity")
150-
if value == 0 and math.copysign(1.0, value) == -1.0:
151-
return dict(v="-0")
152-
if math.isnan(value):
153-
return dict(v="NaN")
154-
if isinstance(value, datetime.datetime):
155-
# Node.js Date objects are always in UTC.
156-
return {
157-
"d": datetime.datetime.strftime(
158-
value.astimezone(datetime.timezone.utc), "%Y-%m-%dT%H:%M:%S.%fZ"
159-
)
160-
}
161-
if isinstance(value, Exception):
162-
return {
163-
"e": {
164-
"m": str(value),
165-
"n": (
166-
(value.name or "")
167-
if isinstance(value, Error)
168-
else value.__class__.__name__
169-
),
170-
"s": (
171-
(value.stack or "")
172-
if isinstance(value, Error)
173-
else "".join(
174-
traceback.format_exception(type(value), value=value, tb=None)
175-
)
176-
),
177-
}
178-
}
179-
if isinstance(value, bool):
180-
return {"b": value}
181-
if isinstance(value, (int, float)):
182-
return {"n": value}
183-
if isinstance(value, str):
184-
return {"s": value}
185-
if isinstance(value, ParseResult):
186-
return {"u": urlunparse(value)}
187-
if isinstance(value, Pattern):
188-
if not isinstance(value.pattern, str):
189-
raise Error("Only str patterns are supported.")
190-
return {"r": {"p": value.pattern, "f": escape_regex_flags(value)}}
191-
192-
if value in visitor_info.visited:
193-
return dict(ref=visitor_info.visited[value])
194-
195-
if isinstance(value, collections.abc.Sequence) and not isinstance(value, str):
196-
id = visitor_info.visit(value)
197-
a = []
198-
for e in value:
199-
a.append(serialize_value(e, handles, visitor_info))
200-
return dict(a=a, id=id)
201-
202-
if isinstance(value, dict):
203-
id = visitor_info.visit(value)
204-
o = []
205-
for name in value:
206-
o.append(
207-
{"k": name, "v": serialize_value(value[name], handles, visitor_info)}
208-
)
209-
return dict(o=o, id=id)
210-
return dict(v="undefined")
211-
212-
213112
def serialize_argument(arg: Serializable = None) -> Any:
214113
handles: List[Channel] = []
215-
value = serialize_value(arg, handles)
216-
return dict(value=value, handles=handles)
217-
218114

219-
def parse_value(value: Any, refs: Optional[Dict[int, Any]] = None) -> Any:
220-
if refs is None:
221-
refs = {}
222-
if value is None:
115+
def serialize_handle(value: Any) -> Optional[Dict[str, Any]]:
116+
if isinstance(value, JSHandle):
117+
handles.append(value._channel)
118+
return {"h": len(handles) - 1}
223119
return None
224-
if isinstance(value, dict):
225-
if "ref" in value:
226-
return refs[value["ref"]]
227-
228-
if "v" in value:
229-
v = value["v"]
230-
if v == "Infinity":
231-
return float("inf")
232-
if v == "-Infinity":
233-
return float("-inf")
234-
if v == "-0":
235-
return float("-0")
236-
if v == "NaN":
237-
return float("nan")
238-
if v == "undefined":
239-
return None
240-
if v == "null":
241-
return None
242-
return v
243-
244-
if "u" in value:
245-
return urlparse(value["u"])
246-
247-
if "bi" in value:
248-
return int(value["bi"])
249-
250-
if "e" in value:
251-
error = Error(value["e"]["m"])
252-
error._name = value["e"]["n"]
253-
error._stack = value["e"]["s"]
254-
return error
255-
256-
if "r" in value:
257-
try:
258-
return re.compile(value["r"]["p"], parse_regex_flags(value["r"]["f"]))
259-
except re.error:
260-
# JavaScript-only syntax, e.g. /(?<name>x)/, has no re equivalent.
261-
return value
262-
263-
if "a" in value:
264-
a: List = []
265-
refs[value["id"]] = a
266-
for e in value["a"]:
267-
a.append(parse_value(e, refs))
268-
return a
269120

270-
if "d" in value:
271-
# Node.js Date objects are always in UTC.
272-
return datetime.datetime.strptime(
273-
value["d"], "%Y-%m-%dT%H:%M:%S.%fZ"
274-
).replace(tzinfo=datetime.timezone.utc)
275-
276-
if "o" in value:
277-
o: Dict = {}
278-
refs[value["id"]] = o
279-
for e in value["o"]:
280-
o[e["k"]] = parse_value(e["v"], refs)
281-
return o
282-
283-
if "n" in value:
284-
return value["n"]
285-
286-
if "s" in value:
287-
return value["s"]
288-
289-
if "b" in value:
290-
return value["b"]
291-
292-
if "ta" in value:
293-
encoded_bytes = value["ta"]["b"]
294-
decoded_bytes = base64.b64decode(encoded_bytes)
295-
array_type = value["ta"]["k"]
296-
if array_type == "i8":
297-
word_size = 1
298-
fmt = "b"
299-
elif array_type == "ui8" or array_type == "ui8c":
300-
word_size = 1
301-
fmt = "B"
302-
elif array_type == "i16":
303-
word_size = 2
304-
fmt = "h"
305-
elif array_type == "ui16":
306-
word_size = 2
307-
fmt = "H"
308-
elif array_type == "i32":
309-
word_size = 4
310-
fmt = "i"
311-
elif array_type == "ui32":
312-
word_size = 4
313-
fmt = "I"
314-
elif array_type == "f32":
315-
word_size = 4
316-
fmt = "f"
317-
elif array_type == "f64":
318-
word_size = 8
319-
fmt = "d"
320-
elif array_type == "bi64":
321-
word_size = 8
322-
fmt = "q"
323-
elif array_type == "bui64":
324-
word_size = 8
325-
fmt = "Q"
326-
else:
327-
raise ValueError(f"Unsupported array type: {array_type}")
328-
329-
byte_len = len(decoded_bytes)
330-
if byte_len % word_size != 0:
331-
raise ValueError(
332-
f"Decoded bytes length {byte_len} is not a multiple of word size {word_size}"
333-
)
334-
335-
if byte_len == 0:
336-
return []
337-
array_len = byte_len // word_size
338-
# "<" denotes little-endian
339-
format_string = f"<{array_len}{fmt}"
340-
return list(struct.unpack(format_string, decoded_bytes))
341-
return value
121+
value = serialize_value(arg, serialize_handle)
122+
return dict(value=value, handles=handles)
342123

343124

344125
def parse_result(result: Any) -> Any:

‎playwright/_impl/_protocol_check.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
# Checks each message that the client sends against the protocol spec
1616
# (_protocol.py). The driver drops parameters that it does not know without an
1717
# error, so a wrong parameter name in the client is silent without this check.
18+
# The type checks follow the driver's validator,
19+
# packages/protocol/src/validatorPrimitives.ts.
1820
#
1921
# The check is on when PW_CHECK_PROTOCOL=1; the test suite sets it. It does not
2022
# raise: it records each violation, and the test suite fails the test that

0 commit comments

Comments
 (0)