Skip to content

Commit ef38e91

Browse files
authored
feat: added Download.saveAs(path) (#147)
1 parent 0f6451e commit ef38e91

File tree

4 files changed

+135
-4
lines changed

4 files changed

+135
-4
lines changed

playwright/async_api.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2939,6 +2939,18 @@ async def path(self) -> typing.Union[str, NoneType]:
29392939
"""
29402940
return mapping.from_maybe_impl(await self._impl_obj.path())
29412941

2942+
async def saveAs(self, path: typing.Union[pathlib.Path, str]) -> NoneType:
2943+
"""Download.saveAs
2944+
2945+
Saves the download to a user-specified path.
2946+
2947+
Parameters
2948+
----------
2949+
path : Union[pathlib.Path, str]
2950+
Path where the download should be saved.
2951+
"""
2952+
return mapping.from_maybe_impl(await self._impl_obj.saveAs(path=path))
2953+
29422954

29432955
mapping.register(DownloadImpl, Download)
29442956

playwright/download.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from typing import Dict, Optional
15+
from pathlib import Path
16+
from typing import Dict, Optional, Union
1617

1718
from playwright.connection import ChannelOwner
1819

@@ -39,3 +40,7 @@ async def failure(self) -> Optional[str]:
3940

4041
async def path(self) -> Optional[str]:
4142
return await self._channel.send("path")
43+
44+
async def saveAs(self, path: Union[Path, str]) -> None:
45+
path = str(Path(path))
46+
return await self._channel.send("saveAs", dict(path=path))

playwright/sync_api.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3065,6 +3065,18 @@ def path(self) -> typing.Union[str, NoneType]:
30653065
"""
30663066
return mapping.from_maybe_impl(self._sync(self._impl_obj.path()))
30673067

3068+
def saveAs(self, path: typing.Union[pathlib.Path, str]) -> NoneType:
3069+
"""Download.saveAs
3070+
3071+
Saves the download to a user-specified path.
3072+
3073+
Parameters
3074+
----------
3075+
path : Union[pathlib.Path, str]
3076+
Path where the download should be saved.
3077+
"""
3078+
return mapping.from_maybe_impl(self._sync(self._impl_obj.saveAs(path=path)))
3079+
30683080

30693081
mapping.register(DownloadImpl, Download)
30703082

tests/async/test_download.py

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
import asyncio
1515
import os
1616
from asyncio.futures import Future
17+
from pathlib import Path
1718
from typing import Optional
1819

1920
import pytest
2021

21-
from playwright import Error as PlaywrightError
22+
from playwright import Error
2223
from playwright.async_api import Browser, Page
2324

2425

@@ -53,10 +54,10 @@ async def test_should_report_downloads_with_acceptDownloads_false(page: Page, se
5354
download = (await asyncio.gather(page.waitForEvent("download"), page.click("a")))[0]
5455
assert download.url == f"{server.PREFIX}/downloadWithFilename"
5556
assert download.suggestedFilename == "file.txt"
56-
error: Optional[PlaywrightError] = None
57+
error: Optional[Error] = None
5758
try:
5859
await download.path()
59-
except PlaywrightError as exc:
60+
except Error as exc:
6061
error = exc
6162
assert "acceptDownloads" in await download.failure()
6263
assert error
@@ -73,6 +74,107 @@ async def test_should_report_downloads_with_acceptDownloads_true(browser, server
7374
await page.close()
7475

7576

77+
async def test_should_save_to_user_specified_path(tmpdir: Path, browser, server):
78+
page = await browser.newPage(acceptDownloads=True)
79+
await page.setContent(f'<a href="{server.PREFIX}/download">download</a>')
80+
[download, _] = await asyncio.gather(page.waitForEvent("download"), page.click("a"))
81+
user_path = tmpdir / "download.txt"
82+
await download.saveAs(user_path)
83+
assert user_path.exists()
84+
assert user_path.read_text("utf-8") == "Hello world"
85+
await page.close()
86+
87+
88+
async def test_should_save_to_user_specified_path_without_updating_original_path(
89+
tmpdir, browser, server
90+
):
91+
page = await browser.newPage(acceptDownloads=True)
92+
await page.setContent(f'<a href="{server.PREFIX}/download">download</a>')
93+
[download, _] = await asyncio.gather(page.waitForEvent("download"), page.click("a"))
94+
user_path = tmpdir / "download.txt"
95+
await download.saveAs(user_path)
96+
assert user_path.exists()
97+
assert user_path.read_text("utf-8") == "Hello world"
98+
99+
originalPath = Path(await download.path())
100+
assert originalPath.exists()
101+
assert originalPath.read_text("utf-8") == "Hello world"
102+
await page.close()
103+
104+
105+
async def test_should_save_to_two_different_paths_with_multiple_saveAs_calls(
106+
tmpdir, browser, server
107+
):
108+
page = await browser.newPage(acceptDownloads=True)
109+
await page.setContent(f'<a href="{server.PREFIX}/download">download</a>')
110+
[download, _] = await asyncio.gather(page.waitForEvent("download"), page.click("a"))
111+
user_path = tmpdir / "download.txt"
112+
await download.saveAs(user_path)
113+
assert user_path.exists()
114+
assert user_path.read_text("utf-8") == "Hello world"
115+
116+
anotheruser_path = tmpdir / "download (2).txt"
117+
await download.saveAs(anotheruser_path)
118+
assert anotheruser_path.exists()
119+
assert anotheruser_path.read_text("utf-8") == "Hello world"
120+
await page.close()
121+
122+
123+
async def test_should_save_to_overwritten_filepath(tmpdir: Path, browser, server):
124+
page = await browser.newPage(acceptDownloads=True)
125+
await page.setContent(f'<a href="{server.PREFIX}/download">download</a>')
126+
[download, _] = await asyncio.gather(page.waitForEvent("download"), page.click("a"))
127+
user_path = tmpdir / "download.txt"
128+
await download.saveAs(user_path)
129+
assert len(list(Path(tmpdir).glob("*.*"))) == 1
130+
await download.saveAs(user_path)
131+
assert len(list(Path(tmpdir).glob("*.*"))) == 1
132+
assert user_path.exists()
133+
assert user_path.read_text("utf-8") == "Hello world"
134+
await page.close()
135+
136+
137+
async def test_should_create_subdirectories_when_saving_to_non_existent_user_specified_path(
138+
tmpdir, browser, server
139+
):
140+
page = await browser.newPage(acceptDownloads=True)
141+
await page.setContent(f'<a href="{server.PREFIX}/download">download</a>')
142+
[download, _] = await asyncio.gather(page.waitForEvent("download"), page.click("a"))
143+
nested_path = tmpdir / "these" / "are" / "directories" / "download.txt"
144+
await download.saveAs(nested_path)
145+
assert nested_path.exists()
146+
assert nested_path.read_text("utf-8") == "Hello world"
147+
await page.close()
148+
149+
150+
async def test_should_error_when_saving_with_downloads_disabled(
151+
tmpdir, browser, server
152+
):
153+
page = await browser.newPage(acceptDownloads=False)
154+
await page.setContent(f'<a href="{server.PREFIX}/download">download</a>')
155+
[download, _] = await asyncio.gather(page.waitForEvent("download"), page.click("a"))
156+
user_path = tmpdir / "download.txt"
157+
with pytest.raises(Error) as exc:
158+
await download.saveAs(user_path)
159+
assert (
160+
"Pass { acceptDownloads: true } when you are creating your browser context"
161+
in exc.value.message
162+
)
163+
await page.close()
164+
165+
166+
async def test_should_error_when_saving_after_deletion(tmpdir, browser, server):
167+
page = await browser.newPage(acceptDownloads=True)
168+
await page.setContent(f'<a href="{server.PREFIX}/download">download</a>')
169+
[download, _] = await asyncio.gather(page.waitForEvent("download"), page.click("a"))
170+
user_path = tmpdir / "download.txt"
171+
await download.delete()
172+
with pytest.raises(Error) as exc:
173+
await download.saveAs(user_path)
174+
assert "Download already deleted. Save before deleting." in exc.value.message
175+
await page.close()
176+
177+
76178
async def test_should_report_non_navigation_downloads(browser, server):
77179
# Mac WebKit embedder does not download in this case, although Safari does.
78180
def handle_download(request):

0 commit comments

Comments
 (0)