Skip to content

Commit a4bdd5b

Browse files
cursoragentP4X-ng
andcommitted
Add CDPConnection.execute_many and clean docs/artifacts
Co-authored-by: P4x-ng <P4X-ng@users.noreply.github.com>
1 parent 2140462 commit a4bdd5b

File tree

8 files changed

+148
-69
lines changed

8 files changed

+148
-69
lines changed

.github/copilot-instructions.md~

Lines changed: 0 additions & 32 deletions
This file was deleted.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ htmlcov/
2626
.eggs/
2727
*.log
2828
.DS_Store
29+
*~

README.md

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ async def main():
5757
# Connect to a Chrome DevTools Protocol endpoint
5858
async with CDPConnection("ws://localhost:9222/devtools/page/YOUR_PAGE_ID") as conn:
5959
# Navigate to a URL
60-
frame_id, loader_id, error = await conn.execute(
60+
frame_id, loader_id, error_text, is_download = await conn.execute(
6161
page.navigate(url="https://example.com")
6262
)
6363
print(f"Navigated to example.com, frame_id: {frame_id}")
@@ -73,6 +73,17 @@ asyncio.run(main())
7373
- **Event Handling**: Async iterator for receiving browser events
7474
- **Error Handling**: Comprehensive error handling with typed exceptions
7575

76+
Batch execution helper:
77+
78+
```python
79+
results = await conn.execute_many([
80+
page.navigate(url="https://example.com"),
81+
page.navigate(url="https://example.org"),
82+
])
83+
```
84+
85+
Use `return_exceptions=True` to collect command failures in the returned list.
86+
7687
See the [examples directory](examples/) for more usage patterns.
7788

7889
## Sans-I/O Mode (Original)
@@ -142,18 +153,4 @@ All CDP types, commands, and events are fully typed with Python type hints, prov
142153
- Clear API contracts
143154
- Inline documentation
144155

145-
## Contributing
146-
147-
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details on:
148-
- How to report bugs and request features
149-
- Development setup and workflow
150-
- Coding standards and testing requirements
151-
- Pull request process
152-
153-
For questions or discussions, feel free to open an issue on GitHub.
154-
155-
## License
156-
157-
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
158-
159156
<a href="https://www.hyperiongray.com/?pk_campaign=github&pk_kwd=pycdp"><img alt="define hyperion gray" width="500px" src="https://hyperiongray.s3.amazonaws.com/define-hg.svg"></a>

cdp/connection.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,42 @@ async def execute(
294294
# Clean up the pending command on error
295295
self._pending_commands.pop(cmd_id, None)
296296
raise
297+
298+
async def execute_many(
299+
self,
300+
commands: typing.Iterable[typing.Generator[T_JSON_DICT, T_JSON_DICT, typing.Any]],
301+
timeout: typing.Optional[float] = None,
302+
return_exceptions: bool = False,
303+
) -> typing.List[typing.Any]:
304+
"""
305+
Execute multiple CDP commands concurrently.
306+
307+
Args:
308+
commands: Iterable of CDP command generators.
309+
timeout: Optional timeout override applied to each command.
310+
return_exceptions: If True, exceptions are returned in the output
311+
list in command order. If False, the first command exception is
312+
raised after all commands complete.
313+
314+
Returns:
315+
A list of command results in the same order as ``commands``.
316+
"""
317+
tasks = [
318+
asyncio.create_task(self.execute(command, timeout=timeout))
319+
for command in commands
320+
]
321+
322+
if not tasks:
323+
return []
324+
325+
results = await asyncio.gather(*tasks, return_exceptions=True)
326+
if return_exceptions:
327+
return results
328+
329+
for result in results:
330+
if isinstance(result, BaseException):
331+
raise result
332+
return results
297333

298334
async def listen(self) -> typing.AsyncIterator[typing.Any]:
299335
"""

docs/connection.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ async def main():
2121
# Connect using async context manager
2222
async with CDPConnection("ws://localhost:9222/devtools/page/YOUR_PAGE_ID") as conn:
2323
# Execute a command
24-
frame_id, loader_id, error = await conn.execute(
24+
frame_id, loader_id, error_text, is_download = await conn.execute(
2525
page.navigate(url="https://example.com")
2626
)
2727

@@ -95,6 +95,28 @@ async with CDPConnection(url) as conn:
9595
print(results[2][0].value) # 6
9696
```
9797

98+
For convenience, you can also pass a batch of commands to `execute_many()`:
99+
100+
```python
101+
async with CDPConnection(url) as conn:
102+
results = await conn.execute_many([
103+
runtime.evaluate(expression="1 + 1"),
104+
runtime.evaluate(expression="2 + 2"),
105+
runtime.evaluate(expression="3 + 3"),
106+
])
107+
print([result[0].value for result in results]) # [2, 4, 6]
108+
```
109+
110+
By default, command exceptions are raised. To collect exceptions in-place, set
111+
`return_exceptions=True`:
112+
113+
```python
114+
results = await conn.execute_many(commands, return_exceptions=True)
115+
for item in results:
116+
if isinstance(item, Exception):
117+
print(f"Command failed: {item}")
118+
```
119+
98120
### Event Handling
99121

100122
Listen for browser events using an async iterator:
@@ -160,6 +182,12 @@ class CDPConnection:
160182
async def connect(self) -> None
161183
async def close(self) -> None
162184
async def execute(self, cmd, timeout: Optional[float] = None) -> Any
185+
async def execute_many(
186+
self,
187+
commands,
188+
timeout: Optional[float] = None,
189+
return_exceptions: bool = False,
190+
) -> List[Any]
163191
async def listen(self) -> AsyncIterator[Any]
164192
def get_event_nowait(self) -> Optional[Any]
165193

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Python wrappers for Chrome DevTools Protocol (CDP).
1111

1212
overview
1313
getting_started
14+
connection
1415
api
1516
develop
1617
changelog

pf_test_results.txt

Lines changed: 0 additions & 21 deletions
This file was deleted.

test/test_connection.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,75 @@ async def test_execute_multiple_commands_multiplexing():
211211
assert len(mock_ws.sent_messages) == 3
212212

213213

214+
@pytest.mark.asyncio
215+
async def test_execute_many_success():
216+
"""Test execute_many returns ordered command results."""
217+
mock_ws = MockWebSocket()
218+
219+
# Queue responses in reverse order to verify request multiplexing.
220+
mock_ws.queue_message({"id": 2, "result": {"result": {"type": "number", "value": 2}}})
221+
mock_ws.queue_message({"id": 1, "result": {"result": {"type": "number", "value": 1}}})
222+
223+
with patch('cdp.connection.websockets.connect', new_callable=AsyncMock) as mock_connect:
224+
mock_connect.return_value = mock_ws
225+
226+
async with CDPConnection("ws://localhost:9222/test") as conn:
227+
results = await conn.execute_many([
228+
runtime.evaluate(expression="1"),
229+
runtime.evaluate(expression="2"),
230+
])
231+
232+
assert len(results) == 2
233+
assert isinstance(results[0][0], runtime.RemoteObject)
234+
assert isinstance(results[1][0], runtime.RemoteObject)
235+
assert results[0][0].value == 1
236+
assert results[1][0].value == 2
237+
assert len(mock_ws.sent_messages) == 2
238+
239+
240+
@pytest.mark.asyncio
241+
async def test_execute_many_return_exceptions():
242+
"""Test execute_many can collect exceptions instead of raising."""
243+
mock_ws = MockWebSocket()
244+
245+
# First command succeeds, second command fails.
246+
mock_ws.queue_message({"id": 1, "result": {"result": {"type": "number", "value": 1}}})
247+
mock_ws.queue_message({"id": 2, "error": {"code": -32602, "message": "Invalid params"}})
248+
249+
with patch('cdp.connection.websockets.connect', new_callable=AsyncMock) as mock_connect:
250+
mock_connect.return_value = mock_ws
251+
252+
async with CDPConnection("ws://localhost:9222/test") as conn:
253+
results = await conn.execute_many(
254+
[
255+
runtime.evaluate(expression="1"),
256+
page.navigate(url="https://example.com"),
257+
],
258+
return_exceptions=True,
259+
)
260+
261+
assert len(results) == 2
262+
assert isinstance(results[0][0], runtime.RemoteObject)
263+
assert results[0][0].value == 1
264+
assert isinstance(results[1], CDPCommandError)
265+
assert results[1].code == -32602
266+
267+
268+
@pytest.mark.asyncio
269+
async def test_execute_many_raises_first_exception_by_default():
270+
"""Test execute_many raises command errors by default."""
271+
mock_ws = MockWebSocket()
272+
mock_ws.queue_message({"id": 1, "error": {"code": -32000, "message": "Command failed"}})
273+
274+
with patch('cdp.connection.websockets.connect', new_callable=AsyncMock) as mock_connect:
275+
mock_connect.return_value = mock_ws
276+
277+
async with CDPConnection("ws://localhost:9222/test") as conn:
278+
with pytest.raises(CDPCommandError) as exc_info:
279+
await conn.execute_many([page.navigate(url="https://example.com")])
280+
assert exc_info.value.code == -32000
281+
282+
214283
@pytest.mark.asyncio
215284
async def test_command_timeout():
216285
"""Test command timeout."""

0 commit comments

Comments
 (0)