浏览代码

Introduce more explicit download functions (#4564)

This PR tries to solve issue #4104 by introducing more explicit download
functions
- `ui.download.file`,
- `ui.download.from_url` and
- `ui.download.content`.

I thought about deprecating the original `ui.download()` call, but I'm
unsure. Maybe we leave it there for developers who prefer the shorter
more generic API.
Falko Schindler 1 月之前
父节点
当前提交
14d0bfd9c5

+ 65 - 11
nicegui/functions/download.py

@@ -5,18 +5,72 @@ from .. import core, helpers
 from ..context import context
 
 
-def download(src: Union[str, Path, bytes], filename: Optional[str] = None, media_type: str = '') -> None:
-    """Download
+class Download:
+    """Download functions
 
-    Function to trigger the download of a file, URL or bytes.
+    These functions allow you to download files, URLs or raw data.
 
-    :param src: target URL, local path of a file or raw data which should be downloaded
-    :param filename: name of the file to download (default: name of the file on the server)
-    :param media_type: media type of the file to download (default: "")
+    *Added in version 2.x.0*
     """
-    if not isinstance(src, bytes):
-        if helpers.is_file(src):
-            src = core.app.add_static_file(local_file=src, single_use=True)
+
+    def __call__(self, src: Union[str, Path, bytes], filename: Optional[str] = None, media_type: str = '') -> None:
+        """Download
+
+        Function to trigger the download of a file, URL or bytes.
+
+        :param src: target URL, local path of a file or raw data which should be downloaded
+        :param filename: name of the file to download (default: name of the file on the server)
+        :param media_type: media type of the file to download (default: "")
+        """
+        if isinstance(src, bytes):
+            self.content(src, filename, media_type)
+        elif helpers.is_file(src):
+            self.file(src, filename, media_type)
         else:
-            src = str(src)
-    context.client.download(src, filename, media_type)
+            assert isinstance(src, str)
+            self.from_url(src, filename, media_type)
+
+    def file(self, path: Union[str, Path], filename: Optional[str] = None, media_type: str = '') -> None:
+        """Download file from local path
+
+        Function to trigger the download of a file.
+
+        *Added in version 2.x.0*
+
+        :param path: local path of the file
+        :param filename: name of the file to download (default: name of the file on the server)
+        :param media_type: media type of the file to download (default: "")
+        """
+        src = core.app.add_static_file(local_file=path, single_use=True)
+        context.client.download(src, filename, media_type)
+
+    def from_url(self, url: str, filename: Optional[str] = None, media_type: str = '') -> None:
+        """Download from a URL
+
+        Function to trigger the download from a URL.
+
+        *Added in version 2.x.0*
+
+        :param url: URL
+        :param filename: name of the file to download (default: name of the file on the server)
+        :param media_type: media type of the file to download (default: "")
+        """
+        context.client.download(url, filename, media_type)
+
+    def content(self, content: Union[bytes, str], filename: Optional[str] = None, media_type: str = '') -> None:
+        """Download raw bytes or string content
+
+        Function to trigger the download of raw data.
+
+        *Added in version 2.x.0*
+
+        :param content: raw bytes or string
+        :param filename: name of the file to download (default: name of the file on the server)
+        :param media_type: media type of the file to download (default: "")
+        """
+        if isinstance(content, str):
+            content = content.encode('utf-8')
+        context.client.download(content, filename, media_type)
+
+
+download = Download()

+ 11 - 1
nicegui/testing/user_download.py

@@ -8,12 +8,13 @@ from typing import TYPE_CHECKING, Any, List, Optional, Union
 import httpx
 
 from .. import background_tasks
+from ..functions.download import Download
 
 if TYPE_CHECKING:
     from .user import User
 
 
-class UserDownload:
+class UserDownload(Download):
 
     def __init__(self, user: User) -> None:
         self.http_responses: List[httpx.Response] = []
@@ -22,6 +23,15 @@ class UserDownload:
     def __call__(self, src: Union[str, Path, bytes], filename: Optional[str] = None, media_type: str = '') -> Any:
         background_tasks.create(self._get(src))
 
+    def file(self, path: Union[str, Path], filename: Optional[str] = None, media_type: str = '') -> None:
+        self(path)
+
+    def from_url(self, url: str, filename: Optional[str] = None, media_type: str = '') -> None:
+        self(url)
+
+    def content(self, content: Union[bytes, str], filename: Optional[str] = None, media_type: str = '') -> None:
+        self(content)
+
     async def _get(self,  src: Union[str, Path, bytes]) -> None:
         if isinstance(src, bytes):
             await asyncio.sleep(0)

+ 33 - 11
tests/test_download.py

@@ -17,33 +17,55 @@ def test_route() -> Generator[str, None, None]:
 
 def test_download_text_file(screen: Screen, test_route: str):  # pylint: disable=redefined-outer-name
     @app.get(test_route)
-    def test():
-        return PlainTextResponse('test')
+    def test(number: str):
+        return PlainTextResponse(f'test {number}')
 
-    ui.button('Download', on_click=lambda: ui.download(test_route))
+    ui.button('Download 1', on_click=lambda: ui.download(test_route + '?number=1', 'test1.txt'))
+    ui.button('Download 2', on_click=lambda: ui.download.from_url(test_route + '?number=2', 'test2.txt'))
 
     screen.open('/')
-    screen.click('Download')
+    screen.click('Download 1')
     screen.wait(0.5)
-    assert (screen_plugin.DOWNLOAD_DIR / 'test.txt').read_text(encoding='utf-8') == 'test'
+    assert (screen_plugin.DOWNLOAD_DIR / 'test1.txt').read_text(encoding='utf-8') == 'test 1'
+
+    screen.click('Download 2')
+    screen.wait(0.5)
+    assert (screen_plugin.DOWNLOAD_DIR / 'test2.txt').read_text(encoding='utf-8') == 'test 2'
 
 
 def test_downloading_local_file_as_src(screen: Screen):
-    IMAGE_FILE = Path(__file__).parent.parent / 'examples' / 'slideshow' / 'slides' / 'slide1.jpg'
-    ui.button('download', on_click=lambda: ui.download(IMAGE_FILE))
+    IMAGE_FILE1 = Path(__file__).parent.parent / 'examples' / 'slideshow' / 'slides' / 'slide1.jpg'
+    IMAGE_FILE2 = Path(__file__).parent.parent / 'examples' / 'slideshow' / 'slides' / 'slide2.jpg'
+    ui.button('Download 1', on_click=lambda: ui.download(IMAGE_FILE1))
+    ui.button('Download 2', on_click=lambda: ui.download.file(IMAGE_FILE2))
 
     screen.open('/')
     route_count_before_download = len(app.routes)
-    screen.click('download')
+    screen.click('Download 1')
     screen.wait(0.5)
     assert (screen_plugin.DOWNLOAD_DIR / 'slide1.jpg').exists()
     assert len(app.routes) == route_count_before_download
 
+    screen.click('Download 2')
+    screen.wait(0.5)
+    assert (screen_plugin.DOWNLOAD_DIR / 'slide2.jpg').exists()
+    assert len(app.routes) == route_count_before_download
+
 
 def test_download_raw_data(screen: Screen):
-    ui.button('download', on_click=lambda: ui.download(b'test', 'test.txt'))
+    ui.button('Download 1', on_click=lambda: ui.download(b'test 1', 'test1.txt'))
+    ui.button('Download 2', on_click=lambda: ui.download.content(b'test 2', 'test2.txt'))
+    ui.button('Download 3', on_click=lambda: ui.download.content('test 3', 'test3.txt'))
 
     screen.open('/')
-    screen.click('download')
+    screen.click('Download 1')
+    screen.wait(0.5)
+    assert (screen_plugin.DOWNLOAD_DIR / 'test1.txt').read_text(encoding='utf-8') == 'test 1'
+
+    screen.click('Download 2')
+    screen.wait(0.5)
+    assert (screen_plugin.DOWNLOAD_DIR / 'test2.txt').read_text(encoding='utf-8') == 'test 2'
+
+    screen.click('Download 3')
     screen.wait(0.5)
-    assert (screen_plugin.DOWNLOAD_DIR / 'test.txt').read_text(encoding='utf-8') == 'test'
+    assert (screen_plugin.DOWNLOAD_DIR / 'test3.txt').read_text(encoding='utf-8') == 'test 3'

+ 4 - 1
tests/test_user_simulation.py

@@ -454,7 +454,10 @@ async def test_download_file(user: User, data: Union[str, bytes]) -> None:
 
     @ui.page('/')
     def page():
-        ui.button('Download', on_click=lambda: ui.download(data))
+        if isinstance(data, str):
+            ui.button('Download', on_click=lambda: ui.download.file(data))
+        else:
+            ui.button('Download', on_click=lambda: ui.download.content(data))
 
     await user.open('/')
     assert len(user.download.http_responses) == 0

+ 16 - 6
website/documentation/content/download_documentation.py

@@ -5,11 +5,21 @@ from . import doc
 
 @doc.demo(ui.download)
 def main_demo() -> None:
-    ui.button('Logo', on_click=lambda: ui.download('https://nicegui.io/logo.png'))
+    ui.button('Local file', on_click=lambda: ui.download.file('main.py'))
+    ui.button('From URL', on_click=lambda: ui.download.from_url('https://nicegui.io/logo.png'))
+    ui.button('Content', on_click=lambda: ui.download.content('Hello World', 'hello.txt'))
 
 
-@doc.demo('Download raw bytes from memory', '''
-    The `download` function can also be used to download raw bytes from memory.
-''')
-def raw_bytes():
-    ui.button('Download', on_click=lambda: ui.download(b'Hello World', 'hello.txt'))
+@doc.demo(ui.download.from_url)
+def from_url_demo() -> None:
+    ui.button('Download', on_click=lambda: ui.download.from_url('https://nicegui.io/logo.png'))
+
+
+@doc.demo(ui.download.content)
+def content_demo() -> None:
+    ui.button('Download', on_click=lambda: ui.download.content('Hello World', 'hello.txt'))
+
+
+@doc.demo(ui.download.file)
+def file_demo() -> None:
+    ui.button('Download', on_click=lambda: ui.download.file('main.py'))