Browse Source

[code-style] Introduce ruff as an isort replacement and add a pre-commit hook (#2619)

* [code-style] Introduce ruff as an isort replacement and add a pre-commit hook

Introduce a simple pre-commit hook with ruff as isort replacement.
This is a first step towards a broader ruff adoption; potentially.

* code-style: add missing pre-commit file

* cleanup some whitespace

* enable ruff extension

* more cleanup

* enable ruff in devcontainer

* code-style: include pre-commit as a dependency and as the required steps
in the CONTRIBUTING.md file

* use ruff-pre-commit

* improve documentation

* add trailing-whitespace and end-of-file-fixer

* add double-quote-string-fixer

* check against Python 3.8

* use more recent version of pre-commit

---------

Co-authored-by: Mathias Brulatout <m.brulatout@criteo.com>
Co-authored-by: Falko Schindler <falko@zauberzeug.com>
Mathias Brulatout 1 year ago
parent
commit
94cc62c831
87 changed files with 530 additions and 268 deletions
  1. 1 1
      .devcontainer/Dockerfile
  2. 2 1
      .devcontainer/devcontainer.json
  3. 2 0
      .gitattributes
  4. 1 1
      .github/ISSUE_TEMPLATE/issue.yml
  5. 1 1
      .gitignore
  6. 35 0
      .pre-commit-config.yaml
  7. 1 1
      .syncignore
  8. 1 2
      .vscode/settings.json
  9. 23 1
      CONTRIBUTING.md
  10. 1 1
      deploy.sh
  11. 1 1
      docker-entrypoint.sh
  12. 0 1
      docker.sh
  13. 1 1
      examples/ai_interface/requirements.txt
  14. 1 1
      examples/descope_auth/requirements.txt
  15. 1 1
      examples/descope_auth/user.py
  16. 1 1
      examples/editable_table/main.py
  17. 1 1
      examples/generate_pdf/.gitignore
  18. 1 1
      examples/generate_pdf/requirements.txt
  19. 1 1
      examples/pytest/.gitignore
  20. 0 1
      examples/ros2/Dockerfile
  21. 1 1
      examples/ros2/ros2_ws/src/gui/package.xml
  22. 1 1
      examples/ros2/ros2_ws/src/simulator/package.xml
  23. 1 1
      examples/ros2/ros_entrypoint.sh
  24. 1 1
      examples/script_executor/main.py
  25. 1 1
      examples/simpy/requirements.txt
  26. 1 1
      examples/sqlite_database/.gitignore
  27. 1 1
      examples/sqlite_database/requirements.txt
  28. 4 4
      fetch_tailwind.py
  29. 3 3
      fly-entrypoint.sh
  30. 1 1
      fly.dockerfile
  31. 2 1
      nicegui.code-workspace
  32. 1 1
      nicegui/elements/dialog.py
  33. 1 1
      nicegui/elements/html.py
  34. 8 2
      nicegui/elements/keyboard.py
  35. 1 1
      nicegui/elements/mixins/value_element.py
  36. 1 1
      nicegui/elements/restructured_text.py
  37. 7 2
      nicegui/elements/scene.py
  38. 1 1
      nicegui/elements/splitter.py
  39. 1 1
      nicegui/functions/navigate.py
  40. 1 1
      nicegui/page.py
  41. 1 1
      nicegui/static/sad_face.svg
  42. 3 3
      nicegui/testing/conftest.py
  43. 6 3
      nicegui/testing/screen.py
  44. 3 2
      nicegui/ui_run.py
  45. 209 125
      poetry.lock
  46. 15 1
      pyproject.toml
  47. 1 1
      release.dockerfile
  48. 1 1
      tests/test_favicon.py
  49. 1 1
      tests/test_markdown.py
  50. 2 2
      tests/test_serving_files.py
  51. 2 3
      tests/test_storage.py
  52. 1 1
      website/documentation/content/aggrid_documentation.py
  53. 1 1
      website/documentation/content/card_documentation.py
  54. 2 2
      website/documentation/content/code_documentation.py
  55. 3 3
      website/documentation/content/element_documentation.py
  56. 1 1
      website/documentation/content/grid_documentation.py
  57. 1 1
      website/documentation/content/highchart_documentation.py
  58. 2 2
      website/documentation/content/icon_documentation.py
  59. 1 1
      website/documentation/content/input_documentation.py
  60. 1 1
      website/documentation/content/joystick_documentation.py
  61. 1 1
      website/documentation/content/label_documentation.py
  62. 1 1
      website/documentation/content/leaflet_documentation.py
  63. 1 1
      website/documentation/content/link_documentation.py
  64. 1 1
      website/documentation/content/markdown_documentation.py
  65. 1 1
      website/documentation/content/number_documentation.py
  66. 16 6
      website/documentation/content/overview.py
  67. 1 1
      website/documentation/content/query_documentation.py
  68. 1 1
      website/documentation/content/restructured_text_documentation.py
  69. 2 2
      website/documentation/content/scene_documentation.py
  70. 10 2
      website/documentation/content/section_action_events.py
  71. 9 2
      website/documentation/content/section_audiovisual_elements.py
  72. 4 4
      website/documentation/content/section_configuration_deployment.py
  73. 24 6
      website/documentation/content/section_controls.py
  74. 21 5
      website/documentation/content/section_data_elements.py
  75. 25 7
      website/documentation/content/section_page_layout.py
  76. 9 3
      website/documentation/content/section_pages_routing.py
  77. 4 4
      website/documentation/content/section_styling_appearance.py
  78. 11 2
      website/documentation/content/section_text_elements.py
  79. 7 7
      website/documentation/content/storage_documentation.py
  80. 2 2
      website/documentation/content/table_documentation.py
  81. 1 1
      website/documentation/content/tabs_documentation.py
  82. 1 1
      website/documentation/content/textarea_documentation.py
  83. 1 1
      website/documentation/demo.py
  84. 1 1
      website/main_page.py
  85. 1 1
      website/static/github.svg
  86. 1 1
      website/static/happy_face.svg
  87. 1 1
      website/static/nicegui_word.svg

+ 1 - 1
.devcontainer/Dockerfile

@@ -32,4 +32,4 @@ RUN poetry install --all-extras
 
 USER $USERNAME
 
-ENTRYPOINT ["poetry", "run", "python", "-m", "debugpy", "--listen" ,"5678", "main.py"]
+ENTRYPOINT ["poetry", "run", "python", "-m", "debugpy", "--listen" ,"5678", "main.py"]

+ 2 - 1
.devcontainer/devcontainer.json

@@ -8,11 +8,11 @@
   "customizations": {
     "vscode": {
       "extensions": [
+        "charliermarsh.ruff",
         "cschleiden.vscode-github-actions",
         "esbenp.prettier-vscode",
         "littlefoxteam.vscode-python-test-adapter",
         "ms-python.autopep8",
-        "ms-python.isort",
         "ms-python.mypy-type-checker",
         "ms-python.pylint",
         "ms-python.python",
@@ -21,6 +21,7 @@
         "Vue.volar"
       ],
       "settings": {
+        "ruff.enable": true,
         "terminal.integrated.defaultProfile.linux": "bash",
         "terminal.integrated.shell.linux": "bash",
         "terminal.integrated.profiles.linux": {

+ 2 - 0
.gitattributes

@@ -10,4 +10,6 @@ nicegui/static/socket.*            linguist-vendored
 nicegui/static/tailwindcss.*       linguist-vendored
 nicegui/static/vue.*               linguist-vendored
 website/**                         linguist-documentation
+website/static/fuse.js@*           linguist-vendored
 examples/**                        linguist-documentation
+examples/fullcalendar/lib/**       linguist-vendored

+ 1 - 1
.github/ISSUE_TEMPLATE/issue.yml

@@ -9,7 +9,7 @@ body:
         Make sure it is really an issue (see [FAQs](/zauberzeug/nicegui/wiki/FAQs)).
         A lot of people will read your message.
         Make it worth their time.
-        
+
         1. What are you trying to do?
           If possible, give a [minimal reproducible code example](https://en.wikipedia.org/wiki/Minimal_reproducible_example).
           Put source code in [fenced code blocks with syntax highlighting](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting).

+ 1 - 1
.gitignore

@@ -10,4 +10,4 @@ venv
 .idea
 .nicegui/
 *.sqlite*
-.DS_Store
+.DS_Store

+ 35 - 0
.pre-commit-config.yaml

@@ -0,0 +1,35 @@
+default_language_version:
+  python: python3.8
+default_install_hook_types: [pre-commit, pre-push]
+default_stages: [commit]
+
+exclusions: &exclusions
+  exclude: |
+    (?x)^(
+      nicegui/elements/lib/.*|
+      nicegui/static/es-module-shims\.js|
+      nicegui/static/fonts/.*|
+      nicegui/static/fonts\.css|
+      nicegui/static/lang/.*|
+      nicegui/static/quasar\..*|
+      nicegui/static/socket\..*|
+      nicegui/static/tailwindcss\..*|
+      nicegui/static/vue\..*|
+      website/static/fuse\.js\@.*|
+      examples/fullcalendar/lib/.*
+    )$
+
+repos:
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.3.5
+    hooks:
+      - id: ruff
+        args: [--fix]
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.6.0
+    hooks:
+      - id: trailing-whitespace
+        <<: *exclusions
+      - id: end-of-file-fixer
+        <<: *exclusions
+      - id: double-quote-string-fixer

+ 1 - 1
.syncignore

@@ -16,4 +16,4 @@ venv
 .github/
 tests/
 .git/
-.mypy_cache/
+.mypy_cache/

+ 1 - 2
.vscode/settings.json

@@ -2,7 +2,6 @@
   "autopep8.args": ["--max-line-length=120"],
   "editor.defaultFormatter": "esbenp.prettier-vscode",
   "editor.formatOnSave": true,
-  "isort.args": ["--line-length", "120"],
   "prettier.printWidth": 120,
   "pylint.args": [
     "--disable=C0103", // Invalid name (e.g., variable/function/class naming conventions)
@@ -31,7 +30,7 @@
   "[python]": {
     "editor.defaultFormatter": "ms-python.autopep8",
     "editor.codeActionsOnSave": {
-      "source.organizeImports": "explicit"
+      "source.fixAll.ruff": "always"
     }
   }
 }

+ 23 - 1
CONTRIBUTING.md

@@ -75,6 +75,24 @@ To view the log output, use the command
 
 ### Formatting
 
+We use [pre-commit](https://github.com/pre-commit/pre-commit) to make sure the coding style is enforced.
+You first need to install pre-commit and the corresponding git commit hooks by running the following commands:
+
+```bash
+python3 -m pip install pre-commit
+pre-commit install
+```
+
+After that you can make sure your code satisfies the coding style by running the following command:
+
+```bash
+pre-commit run --all-files
+```
+
+These checks will also run automatically before every commit.
+
+### Formatting
+
 We use [autopep8](https://github.com/hhatto/autopep8) with a 120 character line length to format our code.
 Before submitting a pull request, please run
 
@@ -93,7 +111,11 @@ on a second line and leave the other arguments as they are.
 
 ### Imports
 
-We use `isort` to automatically sort imports.
+We use [ruff](https://docs.astral.sh/ruff/) to automatically sort imports:
+
+```bash
+ruff check . --fix
+```
 
 ### Single vs Double Quotes
 

+ 1 - 1
deploy.sh

@@ -1,2 +1,2 @@
 
-fly deploy --wait-timeout 360 --build-arg VERSION=$(git describe --abbrev=0 --tags --match 'v*' 2>/dev/null | sed 's/^v//' || echo '0.0.0') 
+fly deploy --wait-timeout 360 --build-arg VERSION=$(git describe --abbrev=0 --tags --match 'v*' 2>/dev/null | sed 's/^v//' || echo '0.0.0')

+ 1 - 1
docker-entrypoint.sh

@@ -40,4 +40,4 @@ export PATH=/home/appuser/.local/bin:$PATH
 
 # Switch to appuser and execute the Docker CMD or passed in command-line arguments.
 # Using setpriv let's it run as PID 1 which is required for proper signal handling (similar to gosu/su-exec).
-exec setpriv --reuid=$PUID --regid=$PGID --init-groups $@
+exec setpriv --reuid=$PUID --regid=$PGID --init-groups $@

+ 0 - 1
docker.sh

@@ -84,4 +84,3 @@ case $cmd in
         echo "Unsupported command \"$cmd\""
         exit 1
 esac
-

+ 1 - 1
examples/ai_interface/requirements.txt

@@ -1,2 +1,2 @@
 nicegui>=1.0
-replicate>=0.4
+replicate>=0.4

+ 1 - 1
examples/descope_auth/requirements.txt

@@ -1 +1 @@
-descope
+descope

+ 1 - 1
examples/descope_auth/user.py

@@ -59,7 +59,7 @@ class page(ui.page):
                 <script>
                     const sdk = Descope({{ projectId: '{DESCOPE_ID}', persistTokens: true, autoRefresh: true }});
                     const sessionToken = sdk.getSessionToken()
-                </script>                 
+                </script>
             ''')
             await client.connected()
             if await self._is_logged_in():

+ 1 - 1
examples/editable_table/main.py

@@ -59,7 +59,7 @@ table.add_slot('body', r'''
         </q-td>
         <q-td key="age" :props="props">
             {{ props.row.age }}
-            <q-popup-edit v-model="props.row.age" v-slot="scope" 
+            <q-popup-edit v-model="props.row.age" v-slot="scope"
                 @update:model-value="() => $parent.$emit('rename', props.row)"
             >
                 <q-input v-model.number="scope.value" type="number" dense autofocus counter @keyup.enter="scope.set" />

+ 1 - 1
examples/generate_pdf/.gitignore

@@ -1 +1 @@
-*.pdf
+*.pdf

+ 1 - 1
examples/generate_pdf/requirements.txt

@@ -1,2 +1,2 @@
 nicegui
-pycairo
+pycairo

+ 1 - 1
examples/pytest/.gitignore

@@ -1 +1 @@
-screenshots/
+screenshots/

+ 0 - 1
examples/ros2/Dockerfile

@@ -24,4 +24,3 @@ EXPOSE 8080
 ENTRYPOINT ["/ros_entrypoint.sh"]
 
 CMD ros2 launch gui main_launch.py
-

+ 1 - 1
examples/ros2/ros2_ws/src/gui/package.xml

@@ -6,7 +6,7 @@
   <description>This is an example of NiceGUI in a ROS2 node that uses a joystick to send geometry_msgs/Twist messages.</description>
   <maintainer email="nicegui@zauberzeug.com">Zauberzeug GmbH</maintainer>
   <license>MIT License</license>
-  
+
   <depend>rclpy</depend>
   <depend>geometry_msgs</depend>
 

+ 1 - 1
examples/ros2/ros2_ws/src/simulator/package.xml

@@ -6,7 +6,7 @@
   <description>A very simple simulator which receives geometry_msgs/Twist messages and publishes geometry_msgs/Pose messages on the "pose" topic.</description>
   <maintainer email="nicegui@zauberzeug.com">Zauberzeug GmbH</maintainer>
   <license>MIT License</license>
-  
+
   <depend>rclpy</depend>
   <depend>geometry_msgs</depend>
 

+ 1 - 1
examples/ros2/ros_entrypoint.sh

@@ -4,4 +4,4 @@ set -e
 source /opt/ros/humble/setup.bash
 source install/setup.bash
 
-exec "$@"
+exec "$@"

+ 1 - 1
examples/script_executor/main.py

@@ -14,7 +14,7 @@ async def run_command(command: str) -> None:
     result.content = ''
     command = command.replace('python3', sys.executable)  # NOTE replace with machine-independent Python path (#1240)
     process = await asyncio.create_subprocess_exec(
-        *shlex.split(command, posix="win" not in sys.platform.lower()),
+        *shlex.split(command, posix='win' not in sys.platform.lower()),
         stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT,
         cwd=os.path.dirname(os.path.abspath(__file__))
     )

+ 1 - 1
examples/simpy/requirements.txt

@@ -1,2 +1,2 @@
 nicegui>=1.2
-simpy
+simpy

+ 1 - 1
examples/sqlite_database/.gitignore

@@ -1 +1 @@
-*.sqlite*
+*.sqlite*

+ 1 - 1
examples/sqlite_database/requirements.txt

@@ -1 +1 @@
-tortoise-orm
+tortoise-orm

+ 4 - 4
fetch_tailwind.py

@@ -126,7 +126,7 @@ def generate_tailwind_file(properties: List[Property]) -> None:
         f.write('\n')
         f.write('class Tailwind:\n')
         f.write('\n')
-        f.write("    def __init__(self, _element: Optional[Element] = None) -> None:\n")
+        f.write('    def __init__(self, _element: Optional[Element] = None) -> None:\n')
         f.write(
             '        self.element: Union[PseudoElement, Element] = PseudoElement() if _element is None else _element\n')
         f.write('\n')
@@ -147,7 +147,7 @@ def generate_tailwind_file(properties: List[Property]) -> None:
         f.write("            self.element.classes(' '.join(args))\n")
         f.write('        return self\n')
         f.write('\n')
-        f.write("    def apply(self, element: Element) -> None:\n")
+        f.write('    def apply(self, element: Element) -> None:\n')
         f.write('        """Apply the tailwind classes to the given element."""\n')
         f.write('        element._classes.extend(self.element._classes)  # pylint: disable=protected-access\n')
         f.write('        element.update()\n')
@@ -155,7 +155,7 @@ def generate_tailwind_file(properties: List[Property]) -> None:
             f.write('\n')
             prefix = property_.common_prefix
             if property_.members:
-                f.write(f"    def {property_.snake_title}(self, value: {property_.pascal_title}) -> Tailwind:\n")
+                f.write(f'    def {property_.snake_title}(self, value: {property_.pascal_title}) -> Tailwind:\n')
                 f.write(f'        """{property_.description}"""\n')
                 if '' in property_.short_members:
                     f.write(
@@ -164,7 +164,7 @@ def generate_tailwind_file(properties: List[Property]) -> None:
                     f.write(f"        self.element.classes('{prefix}' + value)\n")
                 f.write(f'        return self\n')  # pylint: disable=f-string-without-interpolation
             else:
-                f.write(f"    def {property_.snake_title}(self) -> Tailwind:\n")
+                f.write(f'    def {property_.snake_title}(self) -> Tailwind:\n')
                 f.write(f'        """{property_.description}"""\n')
                 f.write(f"        self.element.classes('{prefix}')\n")
                 f.write(f'        return self\n')  # pylint: disable=f-string-without-interpolation

+ 3 - 3
fly-entrypoint.sh

@@ -1,12 +1,12 @@
 #!/bin/bash
 set -e
 
-if [[ ! -z "$SWAP" ]]; then 
+if [[ ! -z "$SWAP" ]]; then
   fallocate -l $(($(stat -f -c "(%a*%s/10)*7" .))) _swapfile
-  mkswap _swapfile 
+  mkswap _swapfile
   swapon _swapfile
 fi
 
 free -hm
 df -h
-exec "$@"
+exec "$@"

+ 1 - 1
fly.dockerfile

@@ -35,4 +35,4 @@ ENTRYPOINT ["/entrypoint.sh"]
 
 ENV PYTHONUNBUFFERED=1
 
-CMD ["python", "main.py"]
+CMD ["python", "main.py"]

+ 2 - 1
nicegui.code-workspace

@@ -5,17 +5,18 @@
     }
   ],
   "settings": {
+    "ruff.enable": true,
     "files.associations": {
       "*.html": "jinja-html"
     }
   },
   "extensions": {
     "recommendations": [
+      "charliermarsh.ruff",
       "cschleiden.vscode-github-actions",
       "esbenp.prettier-vscode",
       "littlefoxteam.vscode-python-test-adapter",
       "ms-python.autopep8",
-      "ms-python.isort",
       "ms-python.mypy-type-checker",
       "ms-python.pylint",
       "ms-python.python",

+ 1 - 1
nicegui/elements/dialog.py

@@ -13,7 +13,7 @@ class Dialog(ValueElement):
         By default it is dismissible by clicking or pressing ESC.
         To make it persistent, set `.props('persistent')` on the dialog element.
 
-        NOTE: The dialog is an element. 
+        NOTE: The dialog is an element.
         That means it is not removed when closed, but only hidden.
         You should either create it only once and then reuse it, or remove it with `.clear()` after dismissal.
 

+ 1 - 1
nicegui/elements/html.py

@@ -6,7 +6,7 @@ class Html(ContentElement):
     def __init__(self, content: str = '', *, tag: str = 'div') -> None:
         """HTML Element
 
-        Renders arbitrary HTML onto the page, wrapped in the specified tag. 
+        Renders arbitrary HTML onto the page, wrapped in the specified tag.
         `Tailwind <https://tailwindcss.com/>`_ can be used for styling.
         You can also use `ui.add_head_html` to add html code into the head of the document and `ui.add_body_html`
         to add it into the body.

+ 8 - 2
nicegui/elements/keyboard.py

@@ -4,8 +4,14 @@ from typing_extensions import Self
 
 from ..binding import BindableProperty
 from ..element import Element
-from ..events import (GenericEventArguments, KeyboardAction, KeyboardKey, KeyboardModifiers, KeyEventArguments,
-                      handle_event)
+from ..events import (
+    GenericEventArguments,
+    KeyboardAction,
+    KeyboardKey,
+    KeyboardModifiers,
+    KeyEventArguments,
+    handle_event,
+)
 
 
 class Keyboard(Element, component='keyboard.js'):

+ 1 - 1
nicegui/elements/mixins/value_element.py

@@ -13,7 +13,7 @@ class ValueElement(Element):
 
     LOOPBACK: Optional[bool] = True
     """Whether to set the new value directly on the client or after getting an update from the server.
-    
+
     - ``True``: The value is updated by sending a change event to the server which responds with an update.
     - ``False``: The value is updated by setting the VALUE_PROP directly on the client.
     - ``None``: The value is updated automatically by the Vue element.

+ 1 - 1
nicegui/elements/restructured_text.py

@@ -32,4 +32,4 @@ def prepare_content(content: str) -> str:
         writer_name='html4',
         settings_overrides={'syntax_highlight': 'short'},
     )
-    return html["html_body"].replace('<div class="document"', '<div class="codehilite"')
+    return html['html_body'].replace('<div class="document"', '<div class="codehilite"')

+ 7 - 2
nicegui/elements/scene.py

@@ -8,8 +8,13 @@ from .. import binding
 from ..awaitable_response import AwaitableResponse, NullResponse
 from ..dataclasses import KWONLY_SLOTS
 from ..element import Element
-from ..events import (GenericEventArguments, SceneClickEventArguments, SceneClickHit, SceneDragEventArguments,
-                      handle_event)
+from ..events import (
+    GenericEventArguments,
+    SceneClickEventArguments,
+    SceneClickHit,
+    SceneDragEventArguments,
+    handle_event,
+)
 from .scene_object3d import Object3D
 
 

+ 1 - 1
nicegui/elements/splitter.py

@@ -15,7 +15,7 @@ class Splitter(ValueElement, DisableableElement):
                  ) -> None:
         """Splitter
 
-        The `ui.splitter` element divides the screen space into resizable sections, 
+        The `ui.splitter` element divides the screen space into resizable sections,
         allowing for flexible and responsive layouts in your application.
 
         Based on Quasar's Splitter component:

+ 1 - 1
nicegui/functions/navigate.py

@@ -51,7 +51,7 @@ class Navigate:
 
         This functionality was previously available as `ui.open` which is now deprecated.
 
-        Note: When using an `auto-index page </documentation/section_pages_routing#auto-index_page>`_ (e.g. no `@page` decorator), 
+        Note: When using an `auto-index page </documentation/section_pages_routing#auto-index_page>`_ (e.g. no `@page` decorator),
         all clients (i.e. browsers) connected to the page will open the target URL unless a socket is specified.
         User events like button clicks provide such a socket.
 

+ 1 - 1
nicegui/page.py

@@ -37,7 +37,7 @@ class page:
 
         This decorator marks a function to be a page builder.
         Each user accessing the given route will see a new instance of the page.
-        This means it is private to the user and not shared with others 
+        This means it is private to the user and not shared with others
         (as it is done `when placing elements outside of a page decorator <https://nicegui.io/documentation/section_pages_routing#auto-index_page>`_).
 
         :param path: route of the new page (path must start with '/')

+ 1 - 1
nicegui/static/sad_face.svg

@@ -13,4 +13,4 @@
     <path class="cls-1" d="M7.3,13.78c-.44,2.28-.58,4.66-.61,7,.01-2.33,.15-4.71,.61-7Z"/>
     <path class="cls-1" d="M39.44,13.19s1.3,1.99,5.11,1.97c2.97-.02,4.64-1.83,7.1-1.77,3.55,.08,5.36,2.39,6.24,4.28,.05,.1,.19,.47,.19,.47,.43,1.36,.54,1.98,.55,1.98,0,0-1.33-.96-3.34-1.13-.97-.08-2.1,.03-3.31,.52"/>
     <path class="cls-1" d="M41.17,55.69s-3.2-2.47-10.19-2.47-10.9,2.47-10.9,2.47"/>
-</svg>
+</svg>

+ 3 - 3
nicegui/testing/conftest.py

@@ -35,9 +35,9 @@ def chrome_options(chrome_options: webdriver.ChromeOptions) -> webdriver.ChromeO
         chrome_options.add_argument('--use-gl=angle')
     chrome_options.add_argument('window-size=600x600')
     chrome_options.add_experimental_option('prefs', {
-        "download.default_directory": str(DOWNLOAD_DIR),
-        "download.prompt_for_download": False,  # To auto download the file
-        "download.directory_upgrade": True,
+        'download.default_directory': str(DOWNLOAD_DIR),
+        'download.prompt_for_download': False,  # To auto download the file
+        'download.directory_upgrade': True,
     })
     if 'CHROME_BINARY_LOCATION' in os.environ:
         chrome_options.binary_location = os.environ['CHROME_BINARY_LOCATION']

+ 6 - 3
nicegui/testing/screen.py

@@ -8,8 +8,11 @@ from typing import Generator, List, Optional, Union
 
 import pytest
 from selenium import webdriver
-from selenium.common.exceptions import (ElementNotInteractableException, NoSuchElementException,
-                                        StaleElementReferenceException)
+from selenium.common.exceptions import (
+    ElementNotInteractableException,
+    NoSuchElementException,
+    StaleElementReferenceException,
+)
 from selenium.webdriver import ActionChains
 from selenium.webdriver.common.by import By
 from selenium.webdriver.remote.webelement import WebElement
@@ -153,7 +156,7 @@ class Screen:
 
     def type(self, text: str) -> None:
         """Type the given text into the currently focused element."""
-        self.selenium.execute_script("window.focus();")
+        self.selenium.execute_script('window.focus();')
         self.wait(0.2)
         self.selenium.switch_to.active_element.send_keys(text)
 

+ 3 - 2
nicegui/ui_run.py

@@ -4,11 +4,12 @@ import sys
 from pathlib import Path
 from typing import Any, List, Literal, Optional, Tuple, Union
 
-import __main__
 from starlette.routing import Route
 from uvicorn.main import STARTUP_FAILURE
 from uvicorn.supervisors import ChangeReload, Multiprocess
 
+import __main__
+
 from . import core, helpers
 from . import native as native_module
 from .air import Air
@@ -78,7 +79,7 @@ def run(*,
     :param endpoint_documentation: control what endpoints appear in the autogenerated OpenAPI docs (default: 'none', options: 'none', 'internal', 'page', 'all')
     :param storage_secret: secret key for browser-based storage (default: `None`, a value is required to enable ui.storage.individual and ui.storage.browser)
     :param show_welcome_message: whether to show the welcome message (default: `True`)
-    :param kwargs: additional keyword arguments are passed to `uvicorn.run`    
+    :param kwargs: additional keyword arguments are passed to `uvicorn.run`
     """
     core.app.config.add_run_config(
         reload=reload,

+ 209 - 125
poetry.lock

@@ -338,6 +338,17 @@ files = [
 [package.dependencies]
 pycparser = "*"
 
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+description = "Validate configuration and produce human readable error messages."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
+    {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
+]
+
 [[package]]
 name = "charset-normalizer"
 version = "3.3.2"
@@ -462,68 +473,6 @@ files = [
     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 ]
 
-[[package]]
-name = "contourpy"
-version = "1.1.0"
-description = "Python library for calculating contours of 2D quadrilateral grids"
-optional = true
-python-versions = ">=3.8"
-files = [
-    {file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"},
-    {file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"},
-    {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"},
-    {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"},
-    {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"},
-    {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"},
-    {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"},
-    {file = "contourpy-1.1.0-cp310-cp310-win32.whl", hash = "sha256:9b2dd2ca3ac561aceef4c7c13ba654aaa404cf885b187427760d7f7d4c57cff8"},
-    {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"},
-    {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"},
-    {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"},
-    {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"},
-    {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"},
-    {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"},
-    {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"},
-    {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"},
-    {file = "contourpy-1.1.0-cp311-cp311-win32.whl", hash = "sha256:edb989d31065b1acef3828a3688f88b2abb799a7db891c9e282df5ec7e46221b"},
-    {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"},
-    {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"},
-    {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"},
-    {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"},
-    {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"},
-    {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"},
-    {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"},
-    {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"},
-    {file = "contourpy-1.1.0-cp38-cp38-win32.whl", hash = "sha256:108dfb5b3e731046a96c60bdc46a1a0ebee0760418951abecbe0fc07b5b93b27"},
-    {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"},
-    {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"},
-    {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"},
-    {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"},
-    {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"},
-    {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"},
-    {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"},
-    {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"},
-    {file = "contourpy-1.1.0-cp39-cp39-win32.whl", hash = "sha256:71551f9520f008b2950bef5f16b0e3587506ef4f23c734b71ffb7b89f8721999"},
-    {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"},
-    {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"},
-    {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"},
-    {file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"},
-    {file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"},
-    {file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"},
-    {file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"},
-    {file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"},
-]
-
-[package.dependencies]
-numpy = ">=1.16"
-
-[package.extras]
-bokeh = ["bokeh", "selenium"]
-docs = ["furo", "sphinx-copybutton"]
-mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"]
-test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
-test-no-images = ["pytest", "pytest-cov", "wurlitzer"]
-
 [[package]]
 name = "contourpy"
 version = "1.1.1"
@@ -644,6 +593,17 @@ files = [
     {file = "debugpy-1.8.1.zip", hash = "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42"},
 ]
 
+[[package]]
+name = "distlib"
+version = "0.3.8"
+description = "Distribution utilities"
+optional = false
+python-versions = "*"
+files = [
+    {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
+    {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
+]
+
 [[package]]
 name = "docutils"
 version = "0.19"
@@ -702,55 +662,71 @@ typing-extensions = ">=4.8.0"
 [package.extras]
 all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
 
+[[package]]
+name = "filelock"
+version = "3.13.3"
+description = "A platform independent file lock."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "filelock-3.13.3-py3-none-any.whl", hash = "sha256:5ffa845303983e7a0b7ae17636509bc97997d58afeafa72fb141a17b152284cb"},
+    {file = "filelock-3.13.3.tar.gz", hash = "sha256:a79895a25bbefdf55d1a2a0a80968f7dbb28edcd6d4234a0afb3f37ecde4b546"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
+typing = ["typing-extensions (>=4.8)"]
+
 [[package]]
 name = "fonttools"
-version = "4.50.0"
+version = "4.51.0"
 description = "Tools to manipulate font files"
 optional = true
 python-versions = ">=3.8"
 files = [
-    {file = "fonttools-4.50.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effd303fb422f8ce06543a36ca69148471144c534cc25f30e5be752bc4f46736"},
-    {file = "fonttools-4.50.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7913992ab836f621d06aabac118fc258b9947a775a607e1a737eb3a91c360335"},
-    {file = "fonttools-4.50.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0a1c5bd2f63da4043b63888534b52c5a1fd7ae187c8ffc64cbb7ae475b9dab"},
-    {file = "fonttools-4.50.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d40fc98540fa5360e7ecf2c56ddf3c6e7dd04929543618fd7b5cc76e66390562"},
-    {file = "fonttools-4.50.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fff65fbb7afe137bac3113827855e0204482727bddd00a806034ab0d3951d0d"},
-    {file = "fonttools-4.50.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1aeae3dd2ee719074a9372c89ad94f7c581903306d76befdaca2a559f802472"},
-    {file = "fonttools-4.50.0-cp310-cp310-win32.whl", hash = "sha256:e9623afa319405da33b43c85cceb0585a6f5d3a1d7c604daf4f7e1dd55c03d1f"},
-    {file = "fonttools-4.50.0-cp310-cp310-win_amd64.whl", hash = "sha256:778c5f43e7e654ef7fe0605e80894930bc3a7772e2f496238e57218610140f54"},
-    {file = "fonttools-4.50.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3dfb102e7f63b78c832e4539969167ffcc0375b013080e6472350965a5fe8048"},
-    {file = "fonttools-4.50.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e58fe34cb379ba3d01d5d319d67dd3ce7ca9a47ad044ea2b22635cd2d1247fc"},
-    {file = "fonttools-4.50.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c673ab40d15a442a4e6eb09bf007c1dda47c84ac1e2eecbdf359adacb799c24"},
-    {file = "fonttools-4.50.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b3ac35cdcd1a4c90c23a5200212c1bb74fa05833cc7c14291d7043a52ca2aaa"},
-    {file = "fonttools-4.50.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8844e7a2c5f7ecf977e82eb6b3014f025c8b454e046d941ece05b768be5847ae"},
-    {file = "fonttools-4.50.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f849bd3c5c2249b49c98eca5aaebb920d2bfd92b3c69e84ca9bddf133e9f83f0"},
-    {file = "fonttools-4.50.0-cp311-cp311-win32.whl", hash = "sha256:39293ff231b36b035575e81c14626dfc14407a20de5262f9596c2cbb199c3625"},
-    {file = "fonttools-4.50.0-cp311-cp311-win_amd64.whl", hash = "sha256:c33d5023523b44d3481624f840c8646656a1def7630ca562f222eb3ead16c438"},
-    {file = "fonttools-4.50.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b4a886a6dbe60100ba1cd24de962f8cd18139bd32808da80de1fa9f9f27bf1dc"},
-    {file = "fonttools-4.50.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b2ca1837bfbe5eafa11313dbc7edada79052709a1fffa10cea691210af4aa1fa"},
-    {file = "fonttools-4.50.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0493dd97ac8977e48ffc1476b932b37c847cbb87fd68673dee5182004906828"},
-    {file = "fonttools-4.50.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77844e2f1b0889120b6c222fc49b2b75c3d88b930615e98893b899b9352a27ea"},
-    {file = "fonttools-4.50.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3566bfb8c55ed9100afe1ba6f0f12265cd63a1387b9661eb6031a1578a28bad1"},
-    {file = "fonttools-4.50.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:35e10ddbc129cf61775d58a14f2d44121178d89874d32cae1eac722e687d9019"},
-    {file = "fonttools-4.50.0-cp312-cp312-win32.whl", hash = "sha256:cc8140baf9fa8f9b903f2b393a6c413a220fa990264b215bf48484f3d0bf8710"},
-    {file = "fonttools-4.50.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ccc85fd96373ab73c59833b824d7a73846670a0cb1f3afbaee2b2c426a8f931"},
-    {file = "fonttools-4.50.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e270a406219af37581d96c810172001ec536e29e5593aa40d4c01cca3e145aa6"},
-    {file = "fonttools-4.50.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac2463de667233372e9e1c7e9de3d914b708437ef52a3199fdbf5a60184f190c"},
-    {file = "fonttools-4.50.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47abd6669195abe87c22750dbcd366dc3a0648f1b7c93c2baa97429c4dc1506e"},
-    {file = "fonttools-4.50.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:074841375e2e3d559aecc86e1224caf78e8b8417bb391e7d2506412538f21adc"},
-    {file = "fonttools-4.50.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0743fd2191ad7ab43d78cd747215b12033ddee24fa1e088605a3efe80d6984de"},
-    {file = "fonttools-4.50.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3d7080cce7be5ed65bee3496f09f79a82865a514863197ff4d4d177389e981b0"},
-    {file = "fonttools-4.50.0-cp38-cp38-win32.whl", hash = "sha256:a467ba4e2eadc1d5cc1a11d355abb945f680473fbe30d15617e104c81f483045"},
-    {file = "fonttools-4.50.0-cp38-cp38-win_amd64.whl", hash = "sha256:f77e048f805e00870659d6318fd89ef28ca4ee16a22b4c5e1905b735495fc422"},
-    {file = "fonttools-4.50.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b6245eafd553c4e9a0708e93be51392bd2288c773523892fbd616d33fd2fda59"},
-    {file = "fonttools-4.50.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a4062cc7e8de26f1603323ef3ae2171c9d29c8a9f5e067d555a2813cd5c7a7e0"},
-    {file = "fonttools-4.50.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34692850dfd64ba06af61e5791a441f664cb7d21e7b544e8f385718430e8f8e4"},
-    {file = "fonttools-4.50.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678dd95f26a67e02c50dcb5bf250f95231d455642afbc65a3b0bcdacd4e4dd38"},
-    {file = "fonttools-4.50.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f2ce7b0b295fe64ac0a85aef46a0f2614995774bd7bc643b85679c0283287f9"},
-    {file = "fonttools-4.50.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d346f4dc2221bfb7ab652d1e37d327578434ce559baf7113b0f55768437fe6a0"},
-    {file = "fonttools-4.50.0-cp39-cp39-win32.whl", hash = "sha256:a51eeaf52ba3afd70bf489be20e52fdfafe6c03d652b02477c6ce23c995222f4"},
-    {file = "fonttools-4.50.0-cp39-cp39-win_amd64.whl", hash = "sha256:8639be40d583e5d9da67795aa3eeeda0488fb577a1d42ae11a5036f18fb16d93"},
-    {file = "fonttools-4.50.0-py3-none-any.whl", hash = "sha256:48fa36da06247aa8282766cfd63efff1bb24e55f020f29a335939ed3844d20d3"},
-    {file = "fonttools-4.50.0.tar.gz", hash = "sha256:fa5cf61058c7dbb104c2ac4e782bf1b2016a8cf2f69de6e4dd6a865d2c969bb5"},
+    {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:84d7751f4468dd8cdd03ddada18b8b0857a5beec80bce9f435742abc9a851a74"},
+    {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8b4850fa2ef2cfbc1d1f689bc159ef0f45d8d83298c1425838095bf53ef46308"},
+    {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5b48a1121117047d82695d276c2af2ee3a24ffe0f502ed581acc2673ecf1037"},
+    {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:180194c7fe60c989bb627d7ed5011f2bef1c4d36ecf3ec64daec8302f1ae0716"},
+    {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:96a48e137c36be55e68845fc4284533bda2980f8d6f835e26bca79d7e2006438"},
+    {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:806e7912c32a657fa39d2d6eb1d3012d35f841387c8fc6cf349ed70b7c340039"},
+    {file = "fonttools-4.51.0-cp310-cp310-win32.whl", hash = "sha256:32b17504696f605e9e960647c5f64b35704782a502cc26a37b800b4d69ff3c77"},
+    {file = "fonttools-4.51.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7e91abdfae1b5c9e3a543f48ce96013f9a08c6c9668f1e6be0beabf0a569c1b"},
+    {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a8feca65bab31479d795b0d16c9a9852902e3a3c0630678efb0b2b7941ea9c74"},
+    {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ac27f436e8af7779f0bb4d5425aa3535270494d3bc5459ed27de3f03151e4c2"},
+    {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e19bd9e9964a09cd2433a4b100ca7f34e34731e0758e13ba9a1ed6e5468cc0f"},
+    {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2b92381f37b39ba2fc98c3a45a9d6383bfc9916a87d66ccb6553f7bdd129097"},
+    {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5f6bc991d1610f5c3bbe997b0233cbc234b8e82fa99fc0b2932dc1ca5e5afec0"},
+    {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9696fe9f3f0c32e9a321d5268208a7cc9205a52f99b89479d1b035ed54c923f1"},
+    {file = "fonttools-4.51.0-cp311-cp311-win32.whl", hash = "sha256:3bee3f3bd9fa1d5ee616ccfd13b27ca605c2b4270e45715bd2883e9504735034"},
+    {file = "fonttools-4.51.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f08c901d3866a8905363619e3741c33f0a83a680d92a9f0e575985c2634fcc1"},
+    {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4060acc2bfa2d8e98117828a238889f13b6f69d59f4f2d5857eece5277b829ba"},
+    {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1250e818b5f8a679ad79660855528120a8f0288f8f30ec88b83db51515411fcc"},
+    {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76f1777d8b3386479ffb4a282e74318e730014d86ce60f016908d9801af9ca2a"},
+    {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b5ad456813d93b9c4b7ee55302208db2b45324315129d85275c01f5cb7e61a2"},
+    {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:68b3fb7775a923be73e739f92f7e8a72725fd333eab24834041365d2278c3671"},
+    {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8e2f1a4499e3b5ee82c19b5ee57f0294673125c65b0a1ff3764ea1f9db2f9ef5"},
+    {file = "fonttools-4.51.0-cp312-cp312-win32.whl", hash = "sha256:278e50f6b003c6aed19bae2242b364e575bcb16304b53f2b64f6551b9c000e15"},
+    {file = "fonttools-4.51.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3c61423f22165541b9403ee39874dcae84cd57a9078b82e1dce8cb06b07fa2e"},
+    {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1621ee57da887c17312acc4b0e7ac30d3a4fb0fec6174b2e3754a74c26bbed1e"},
+    {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d9298be7a05bb4801f558522adbe2feea1b0b103d5294ebf24a92dd49b78e5"},
+    {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee1af4be1c5afe4c96ca23badd368d8dc75f611887fb0c0dac9f71ee5d6f110e"},
+    {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b49adc721a7d0b8dfe7c3130c89b8704baf599fb396396d07d4aa69b824a1"},
+    {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de7c29bdbdd35811f14493ffd2534b88f0ce1b9065316433b22d63ca1cd21f14"},
+    {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cadf4e12a608ef1d13e039864f484c8a968840afa0258b0b843a0556497ea9ed"},
+    {file = "fonttools-4.51.0-cp38-cp38-win32.whl", hash = "sha256:aefa011207ed36cd280babfaa8510b8176f1a77261833e895a9d96e57e44802f"},
+    {file = "fonttools-4.51.0-cp38-cp38-win_amd64.whl", hash = "sha256:865a58b6e60b0938874af0968cd0553bcd88e0b2cb6e588727117bd099eef836"},
+    {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:60a3409c9112aec02d5fb546f557bca6efa773dcb32ac147c6baf5f742e6258b"},
+    {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7e89853d8bea103c8e3514b9f9dc86b5b4120afb4583b57eb10dfa5afbe0936"},
+    {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fc244f2585d6c00b9bcc59e6593e646cf095a96fe68d62cd4da53dd1287b55"},
+    {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce"},
+    {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5b8cab0c137ca229433570151b5c1fc6af212680b58b15abd797dcdd9dd5051"},
+    {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:54dcf21a2f2d06ded676e3c3f9f74b2bafded3a8ff12f0983160b13e9f2fb4a7"},
+    {file = "fonttools-4.51.0-cp39-cp39-win32.whl", hash = "sha256:0118ef998a0699a96c7b28457f15546815015a2710a1b23a7bf6c1be60c01636"},
+    {file = "fonttools-4.51.0-cp39-cp39-win_amd64.whl", hash = "sha256:599bdb75e220241cedc6faebfafedd7670335d2e29620d207dd0378a4e9ccc5a"},
+    {file = "fonttools-4.51.0-py3-none-any.whl", hash = "sha256:15c94eeef6b095831067f72c825eb0e2d48bb4cea0647c1b05c981ecba2bf39f"},
+    {file = "fonttools-4.51.0.tar.gz", hash = "sha256:dc0673361331566d7a663d7ce0f6fdcbfbdc1f59c6e3ed1165ad7202ca183c68"},
 ]
 
 [package.extras]
@@ -974,6 +950,20 @@ colorama = ">=0.3.9"
 executing = ">=0.3.1"
 pygments = ">=2.2.0"
 
+[[package]]
+name = "identify"
+version = "2.5.35"
+description = "File identification library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"},
+    {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"},
+]
+
+[package.extras]
+license = ["ukkonen"]
+
 [[package]]
 name = "idna"
 version = "3.6"
@@ -1025,20 +1015,6 @@ files = [
     {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
 ]
 
-[[package]]
-name = "isort"
-version = "5.13.2"
-description = "A Python utility / library to sort Python imports."
-optional = false
-python-versions = ">=3.8.0"
-files = [
-    {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
-    {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
-]
-
-[package.extras]
-colors = ["colorama (>=0.4.6)"]
-
 [[package]]
 name = "itsdangerous"
 version = "2.1.2"
@@ -1461,6 +1437,20 @@ files = [
 [package.dependencies]
 nicegui = ">=1.4.0,<2.0.0"
 
+[[package]]
+name = "nodeenv"
+version = "1.8.0"
+description = "Node.js virtual environment builder"
+optional = false
+python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+files = [
+    {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
+    {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
+]
+
+[package.dependencies]
+setuptools = "*"
+
 [[package]]
 name = "numpy"
 version = "1.24.4"
@@ -1665,8 +1655,8 @@ files = [
 [package.dependencies]
 numpy = [
     {version = ">=1.20.3", markers = "python_version < \"3.10\""},
-    {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""},
     {version = ">=1.23.2", markers = "python_version >= \"3.11\""},
+    {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""},
 ]
 python-dateutil = ">=2.8.2"
 pytz = ">=2020.1"
@@ -1781,6 +1771,21 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
 typing = ["typing-extensions"]
 xmp = ["defusedxml"]
 
+[[package]]
+name = "platformdirs"
+version = "4.2.0"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"},
+    {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
+
 [[package]]
 name = "plotly"
 version = "5.20.0"
@@ -1811,6 +1816,24 @@ files = [
 dev = ["pre-commit", "tox"]
 testing = ["pytest", "pytest-benchmark"]
 
+[[package]]
+name = "pre-commit"
+version = "3.5.0"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"},
+    {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
 [[package]]
 name = "prettytable"
 version = "3.10.0"
@@ -2425,7 +2448,6 @@ files = [
     {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
     {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
     {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
-    {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
     {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
     {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
     {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
@@ -2498,6 +2520,32 @@ urllib3 = ">=1.21.1,<3"
 socks = ["PySocks (>=1.5.6,!=1.5.7)"]
 use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
+[[package]]
+name = "ruff"
+version = "0.3.5"
+description = "An extremely fast Python linter and code formatter, written in Rust."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:aef5bd3b89e657007e1be6b16553c8813b221ff6d92c7526b7e0227450981eac"},
+    {file = "ruff-0.3.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:89b1e92b3bd9fca249153a97d23f29bed3992cff414b222fcd361d763fc53f12"},
+    {file = "ruff-0.3.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e55771559c89272c3ebab23326dc23e7f813e492052391fe7950c1a5a139d89"},
+    {file = "ruff-0.3.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dabc62195bf54b8a7876add6e789caae0268f34582333cda340497c886111c39"},
+    {file = "ruff-0.3.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3a05f3793ba25f194f395578579c546ca5d83e0195f992edc32e5907d142bfa3"},
+    {file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dfd3504e881082959b4160ab02f7a205f0fadc0a9619cc481982b6837b2fd4c0"},
+    {file = "ruff-0.3.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87258e0d4b04046cf1d6cc1c56fadbf7a880cc3de1f7294938e923234cf9e498"},
+    {file = "ruff-0.3.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:712e71283fc7d9f95047ed5f793bc019b0b0a29849b14664a60fd66c23b96da1"},
+    {file = "ruff-0.3.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a532a90b4a18d3f722c124c513ffb5e5eaff0cc4f6d3aa4bda38e691b8600c9f"},
+    {file = "ruff-0.3.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:122de171a147c76ada00f76df533b54676f6e321e61bd8656ae54be326c10296"},
+    {file = "ruff-0.3.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d80a6b18a6c3b6ed25b71b05eba183f37d9bc8b16ace9e3d700997f00b74660b"},
+    {file = "ruff-0.3.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a7b6e63194c68bca8e71f81de30cfa6f58ff70393cf45aab4c20f158227d5936"},
+    {file = "ruff-0.3.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a759d33a20c72f2dfa54dae6e85e1225b8e302e8ac655773aff22e542a300985"},
+    {file = "ruff-0.3.5-py3-none-win32.whl", hash = "sha256:9d8605aa990045517c911726d21293ef4baa64f87265896e491a05461cae078d"},
+    {file = "ruff-0.3.5-py3-none-win_amd64.whl", hash = "sha256:dc56bb16a63c1303bd47563c60482a1512721053d93231cf7e9e1c6954395a0e"},
+    {file = "ruff-0.3.5-py3-none-win_arm64.whl", hash = "sha256:faeeae9905446b975dcf6d4499dc93439b131f1443ee264055c5716dd947af55"},
+    {file = "ruff-0.3.5.tar.gz", hash = "sha256:a067daaeb1dc2baf9b82a32dae67d154d95212080c80435eb052d95da647763d"},
+]
+
 [[package]]
 name = "secure"
 version = "0.3.0"
@@ -2527,6 +2575,22 @@ trio-websocket = ">=0.9,<1.0"
 typing_extensions = ">=4.9.0"
 urllib3 = {version = ">=1.26,<3", extras = ["socks"]}
 
+[[package]]
+name = "setuptools"
+version = "69.2.0"
+description = "Easily download, build, install, upgrade, and uninstall Python packages"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"},
+    {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"},
+]
+
+[package.extras]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
+testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
+
 [[package]]
 name = "simple-websocket"
 version = "1.0.0"
@@ -2787,13 +2851,13 @@ wsproto = ">=0.14"
 
 [[package]]
 name = "typing-extensions"
-version = "4.10.0"
+version = "4.11.0"
 description = "Backported and Experimental Type Hints for Python 3.8+"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"},
-    {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"},
+    {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
+    {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
 ]
 
 [[package]]
@@ -2911,6 +2975,26 @@ files = [
 [package.dependencies]
 pscript = ">=0.7.0,<0.8.0"
 
+[[package]]
+name = "virtualenv"
+version = "20.25.1"
+description = "Virtual Python Environment builder"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"},
+    {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+platformdirs = ">=3.9.1,<5"
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
+
 [[package]]
 name = "watchfiles"
 version = "0.21.0"
@@ -3248,4 +3332,4 @@ sass = ["libsass"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.8"
-content-hash = "c4016dbe5dd4e0f33395dc6b59e0b7d394728397d90f6b0b3d074299d0bcfa14"
+content-hash = "775423991786eb9a42428442cbd6bf503135f96ad346da78ade2aebc09573a06"

+ 15 - 1
pyproject.toml

@@ -48,7 +48,6 @@ pytest-selenium = "^4.0.0"
 pytest-asyncio = ">=0.19.0"
 pytest = ">=6.2.5,<8"
 itsdangerous = "^2.1.2" # required by SessionMiddleware (see https://fastapi.tiangolo.com/?h=itsdangerous#optional-dependencies)
-isort = "^5.11.4"
 pandas = "^2.0.0"
 secure = ">=0.3.0"
 webdriver-manager = "^3.8.6"
@@ -60,6 +59,8 @@ selenium = "^4.11.2"
 beautifulsoup4 = "^4.12.2"
 urllib3 = ">=1.26.18,^1.26 || >=2.0.7" # https://github.com/zauberzeug/nicegui/security/dependabot/23
 pyecharts = "^2.0.4"
+ruff = ">=0.3.5"
+pre-commit = ">=3.7.0"
 
 [build-system]
 requires = [
@@ -74,3 +75,16 @@ asyncio_mode = "auto"
 
 [tool.mypy]
 ignore_missing_imports = true
+
+[tool.ruff]
+indent-width = 4
+line-length = 120
+
+[tool.ruff.lint]
+# See complete list: https://beta.ruff.rs/docs/rules
+select = [
+    "I",  # isort
+]
+fixable = [
+    "I",  # isort
+]

+ 1 - 1
release.dockerfile

@@ -34,4 +34,4 @@ EXPOSE 8080
 ENV PYTHONUNBUFFERED True
 
 ENTRYPOINT ["/resources/docker-entrypoint.sh"]
-CMD ["python", "main.py"]
+CMD ["python", "main.py"]

+ 1 - 1
tests/test_favicon.py

@@ -13,7 +13,7 @@ LOGO_FAVICON_PATH = Path(__file__).parent.parent / 'website' / 'static' / 'logo_
 
 def assert_favicon_url_starts_with(screen: Screen, content: str):
     soup = BeautifulSoup(screen.selenium.page_source, 'html.parser')
-    icon_link = soup.find("link", rel="icon")
+    icon_link = soup.find('link', rel='icon')
     assert icon_link['href'].startswith(content)
 
 

+ 1 - 1
tests/test_markdown.py

@@ -34,7 +34,7 @@ def test_markdown_with_mermaid(screen: Screen):
 
     m.set_content('''
         New:
-        
+
         ```mermaid
         graph TD;
             Node_C --> Node_D;

+ 2 - 2
tests/test_serving_files.py

@@ -91,8 +91,8 @@ def test_auto_serving_file_from_image_source(screen: Screen):
     img = screen.find_by_tag('img')
     assert '/_nicegui/auto/static/' in img.get_attribute('src')
     assert screen.selenium.execute_script("""
-    return arguments[0].complete && 
-        typeof arguments[0].naturalWidth != "undefined" && 
+    return arguments[0].complete &&
+        typeof arguments[0].naturalWidth != "undefined" &&
         arguments[0].naturalWidth > 0
     """, img), 'image should load successfully'
 

+ 2 - 3
tests/test_storage.py

@@ -1,12 +1,11 @@
 import asyncio
 from pathlib import Path
-import pytest
 
 import httpx
+import pytest
 
-from nicegui import Client, app, background_tasks, context
+from nicegui import Client, app, background_tasks, context, ui
 from nicegui import storage as storage_module
-from nicegui import ui
 from nicegui.testing import Screen
 
 

+ 1 - 1
website/documentation/content/aggrid_documentation.py

@@ -77,7 +77,7 @@ def aggrid_with_selectable_rows():
 @doc.demo('Filter Rows using Mini Filters', '''
     You can add [mini filters](https://ag-grid.com/javascript-data-grid/filter-set-mini-filter/)
     to the header of each column to filter the rows.
-    
+
     Note how the "agTextColumnFilter" matches individual characters, like "a" in "Alice" and "Carol",
     while the "agNumberColumnFilter" matches the entire number, like "18" and "21", but not "1".
 ''')

+ 1 - 1
website/documentation/content/card_documentation.py

@@ -29,7 +29,7 @@ def card_without_shadow() -> None:
 
     - To get the original QCard behavior, use the `tight` method (second card).
         It removes the padding and the table border collapses with the card border.
-    
+
     - To preserve the padding _and_ the table border, move the table into another container like a `ui.row` (third card).
 
     See https://github.com/zauberzeug/nicegui/issues/726 for more information.

+ 2 - 2
website/documentation/content/code_documentation.py

@@ -7,9 +7,9 @@ from . import doc
 def main_demo() -> None:
     ui.code('''
         from nicegui import ui
-        
+
         ui.label('Code inception!')
-            
+
         ui.run()
     ''').classes('w-full')
 

+ 3 - 3
website/documentation/content/element_documentation.py

@@ -28,7 +28,7 @@ def move_elements() -> None:
 @doc.demo('Default props', '''
     You can set default props for all elements of a certain class.
     This way you can avoid repeating the same props over and over again.
-    
+
     Default props only apply to elements created after the default props were set.
     Subclasses inherit the default props of their parent class.
 ''')
@@ -43,7 +43,7 @@ def default_props() -> None:
 @doc.demo('Default classes', '''
     You can set default classes for all elements of a certain class.
     This way you can avoid repeating the same classes over and over again.
-    
+
     Default classes only apply to elements created after the default classes were set.
     Subclasses inherit the default classes of their parent class.
 ''')
@@ -58,7 +58,7 @@ def default_classes() -> None:
 @doc.demo('Default style', '''
     You can set a default style for all elements of a certain class.
     This way you can avoid repeating the same style over and over again.
-    
+
     A default style only applies to elements created after the default style was set.
     Subclasses inherit the default style of their parent class.
 ''')

+ 1 - 1
website/documentation/content/grid_documentation.py

@@ -19,7 +19,7 @@ def main_demo() -> None:
 @doc.demo('Custom grid layout', '''
     This demo shows how to create a custom grid layout passing a string with the grid-template-columns CSS property.
     You can use any valid CSS dimensions, such as 'auto', '1fr', '80px', etc.
-          
+
     - 'auto' will make the column as wide as its content.
     - '1fr' or '2fr' will make the corresponding columns fill the remaining space, with fractions in a 1:2 ratio.
     - '80px' will make the column 80 pixels wide.

+ 1 - 1
website/documentation/content/highchart_documentation.py

@@ -45,7 +45,7 @@ def extra_dependencies() -> None:
 @doc.demo('Chart with draggable points', '''
     This chart allows dragging the series points.
     You can register callbacks for the following events:
-    
+
     - `on_point_click`: called when a point is clicked
     - `on_point_drag_start`: called when a point drag starts
     - `on_point_drag`: called when a point is dragged

+ 2 - 2
website/documentation/content/icon_documentation.py

@@ -12,7 +12,7 @@ def main_demo() -> None:
     You can use different sets of Material icons and symbols.
     The [Quasar documentation](https://quasar.dev/vue-components/icon\#webfont-usage)
     gives an overview of all available icon sets and their name prefix:
-    
+
     * None for [filled icons](https://fonts.google.com/icons?icon.set=Material+Icons&icon.style=Filled)
     * "o\_" for [outline icons](https://fonts.google.com/icons?icon.set=Material+Icons&icon.style=Outlined)
     * "r\_" for [round icons](https://fonts.google.com/icons?icon.set=Material+Icons&icon.style=Rounded)
@@ -40,7 +40,7 @@ def eva_icons():
 
 
 @doc.demo('Other icon sets', '''
-    You can use the same approach for adding other icon sets to your app. 
+    You can use the same approach for adding other icon sets to your app.
     As a rule of thumb, you reference the corresponding CSS, and it in turn references font files.
     This demo shows how to include [Themify icons](https://themify.me/themify-icons).
 ''', lazy=False)

+ 1 - 1
website/documentation/content/input_documentation.py

@@ -21,7 +21,7 @@ def autocomplete_demo():
 
 
 @doc.demo('Clearable', '''
-    The `clearable` prop from [Quasar](https://quasar.dev/) adds a button to the input that clears the text.    
+    The `clearable` prop from [Quasar](https://quasar.dev/) adds a button to the input that clears the text.
 ''')
 def clearable():
     i = ui.input(value='some text').props('clearable')

+ 1 - 1
website/documentation/content/joystick_documentation.py

@@ -6,7 +6,7 @@ from . import doc
 @doc.demo(ui.joystick)
 def main_demo() -> None:
     ui.joystick(color='blue', size=50,
-                on_move=lambda e: coordinates.set_text(f"{e.x:.3f}, {e.y:.3f}"),
+                on_move=lambda e: coordinates.set_text(f'{e.x:.3f}, {e.y:.3f}'),
                 on_end=lambda _: coordinates.set_text('0, 0'))
     coordinates = ui.label('0, 0')
 

+ 1 - 1
website/documentation/content/label_documentation.py

@@ -9,7 +9,7 @@ def main_demo() -> None:
 
 
 @doc.demo('Change Appearance Depending on the Content', '''
-    You can overwrite the `_handle_text_change` method to update other attributes of a label depending on its content. 
+    You can overwrite the `_handle_text_change` method to update other attributes of a label depending on its content.
     This technique also works for bindings as shown in the example below.
 ''')
 def status():

+ 1 - 1
website/documentation/content/leaflet_documentation.py

@@ -37,7 +37,7 @@ def map_style() -> None:
 
 
 @doc.demo('Add Markers on Click', '''
-    You can add markers to the map with `marker`. 
+    You can add markers to the map with `marker`.
     The `center` argument is a tuple of latitude and longitude.
     This demo adds markers by clicking on the map.
     Note that the "map-click" event refers to the click event of the map object,

+ 1 - 1
website/documentation/content/link_documentation.py

@@ -48,7 +48,7 @@ def link_to_other_page():
 
 @doc.demo('Link from images and other elements', '''
     By nesting elements inside a link you can make the whole element clickable.
-    This works with all elements but is most useful for non-interactive elements like 
+    This works with all elements but is most useful for non-interactive elements like
     [ui.image](/documentation/image), [ui.avatar](/documentation/image) etc.
 ''')
 def link_from_elements():

+ 1 - 1
website/documentation/content/markdown_documentation.py

@@ -20,7 +20,7 @@ def markdown_with_indentation():
 
             This block is indented.
             Thus it is rendered as source code.
-        
+
         This is normal text again.
     ''')
 

+ 1 - 1
website/documentation/content/number_documentation.py

@@ -11,7 +11,7 @@ def main_demo() -> None:
 
 
 @doc.demo('Clearable', '''
-    The `clearable` prop from [Quasar](https://quasar.dev/) adds a button to the input that clears the text.    
+    The `clearable` prop from [Quasar](https://quasar.dev/) adds a button to the input that clears the text.
 ''')
 def clearable():
     i = ui.number(value=42).props('clearable')

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

@@ -1,8 +1,18 @@
 from nicegui import ui
 
-from . import (doc, section_action_events, section_audiovisual_elements, section_binding_properties,
-               section_configuration_deployment, section_controls, section_data_elements, section_page_layout,
-               section_pages_routing, section_styling_appearance, section_text_elements)
+from . import (
+    doc,
+    section_action_events,
+    section_audiovisual_elements,
+    section_binding_properties,
+    section_configuration_deployment,
+    section_controls,
+    section_data_elements,
+    section_page_layout,
+    section_pages_routing,
+    section_styling_appearance,
+    section_text_elements,
+)
 
 doc.title('*NiceGUI* Documentation', 'Reference, Demos and more')
 
@@ -11,8 +21,8 @@ doc.text('Overview', '''
     It has a very gentle learning curve while still offering the option for advanced customizations.
     NiceGUI follows a backend-first philosophy:
     It handles all the web development details.
-    You can focus on writing Python code. 
-    This makes it ideal for a wide range of projects including short 
+    You can focus on writing Python code.
+    This makes it ideal for a wide range of projects including short
     scripts, dashboards, robotics projects, IoT solutions, smart home automation, and machine learning.
 ''')
 
@@ -61,7 +71,7 @@ doc.text('Running NiceGUI Apps', '''
     Or you can run NiceGUI on a server that handles many clients - the website you're reading right now is served from NiceGUI.
 
     After creating your app pages with components, you call `ui.run()` to start the NiceGUI server.
-    Optional parameters to `ui.run` set things like the network address and port the server binds to, 
+    Optional parameters to `ui.run` set things like the network address and port the server binds to,
     whether the app runs in native mode, initial window size, and many other options.
     The section _Configuration and Deployment_ covers the options to the `ui.run()` function and the FastAPI framework it is based on.
 ''')

+ 1 - 1
website/documentation/content/query_documentation.py

@@ -16,7 +16,7 @@ def main_demo() -> None:
 
 
 @doc.demo('Set background gradient', '''
-    It's easy to set a background gradient, image or similar. 
+    It's easy to set a background gradient, image or similar.
     See [w3schools.com](https://www.w3schools.com/cssref/pr_background-image.php) for more information about setting background with CSS.
 ''')
 def background_image():

+ 1 - 1
website/documentation/content/restructured_text_documentation.py

@@ -36,7 +36,7 @@ def rst_with_indentation():
 def rst_with_code_blocks():
     ui.restructured_text('''
         .. code-block:: python3
-        
+
             from nicegui import ui
 
             ui.label('Hello World!')

+ 2 - 2
website/documentation/content/scene_documentation.py

@@ -64,12 +64,12 @@ def click_events() -> None:
     You can make objects draggable using the `.draggable` method.
     There is an optional `on_drag_start` and `on_drag_end` argument to `ui.scene` to handle drag events.
     The callbacks receive a `SceneDragEventArguments` object with the following attributes:
-    
+
     - `type`: the type of drag event ("dragstart" or "dragend").
     - `object_id`: the id of the object that was dragged.
     - `object_name`: the name of the object that was dragged.
     - `x`, `y`, `z`: the x, y and z coordinates of the dragged object.
-        
+
     You can also use the `drag_constraints` argument to set comma-separated JavaScript expressions
     for constraining positions of dragged objects.
 ''')

+ 10 - 2
website/documentation/content/section_action_events.py

@@ -1,7 +1,15 @@
 from nicegui import app, ui
 
-from . import (clipboard_documentation, doc, generic_events_documentation, keyboard_documentation,
-               refreshable_documentation, run_javascript_documentation, storage_documentation, timer_documentation)
+from . import (
+    clipboard_documentation,
+    doc,
+    generic_events_documentation,
+    keyboard_documentation,
+    refreshable_documentation,
+    run_javascript_documentation,
+    storage_documentation,
+    timer_documentation,
+)
 
 doc.title('Action & *Events*')
 

+ 9 - 2
website/documentation/content/section_audiovisual_elements.py

@@ -1,7 +1,14 @@
 from nicegui import ui
 
-from . import (audio_documentation, avatar_documentation, doc, icon_documentation, image_documentation,
-               interactive_image_documentation, video_documentation)
+from . import (
+    audio_documentation,
+    avatar_documentation,
+    doc,
+    icon_documentation,
+    image_documentation,
+    interactive_image_documentation,
+    video_documentation,
+)
 
 doc.title('*Audiovisual* Elements')
 

+ 4 - 4
website/documentation/content/section_configuration_deployment.py

@@ -55,7 +55,7 @@ def native_mode_demo():
 doc.text('', '''
     If webview has trouble finding required libraries, you may get an error relating to "WebView2Loader.dll".
     To work around this issue, try moving the DLL file up a directory, e.g.:
-    
+
     * from `.venv/Lib/site-packages/webview/lib/x64/WebView2Loader.dll`
     * to `.venv/Lib/site-packages/webview/lib/WebView2Loader.dll`
 ''')
@@ -209,7 +209,7 @@ doc.text('', '''
     and zip up the generated `dist` directory yourself, distribute it,
     and your end users can unzip once and be good to go,
     without the constant expansion of files due to the `--onefile` flag.
-    
+
     - Summary of user experience for different options:
 
         | PyInstaller              | `ui.run(...)`  | Explanation |
@@ -251,7 +251,7 @@ doc.text('', '''
 
 doc.text('', '''
     **macOS Packaging**
-    
+
     Add the following snippet before anything else in your main app's file, to prevent new processes from being spawned in an endless loop:
 
     ```python
@@ -261,7 +261,7 @@ doc.text('', '''
 
     # all your other imports and code
     ```
-    
+
     The `# noqa` comment instructs Pylance or autopep8 to not apply any PEP rule on those two lines, guaranteeing they remain on top of anything else.
     This is key to prevent process spawning.
 ''')

+ 24 - 6
website/documentation/content/section_controls.py

@@ -1,9 +1,27 @@
-from . import (badge_documentation, button_documentation, button_dropdown_documentation, button_group_documentation,
-               checkbox_documentation, color_input_documentation, color_picker_documentation, date_documentation, doc,
-               input_documentation, joystick_documentation, knob_documentation, number_documentation,
-               radio_documentation, range_documentation, select_documentation, slider_documentation,
-               switch_documentation, textarea_documentation, time_documentation, toggle_documentation,
-               upload_documentation)
+from . import (
+    badge_documentation,
+    button_documentation,
+    button_dropdown_documentation,
+    button_group_documentation,
+    checkbox_documentation,
+    color_input_documentation,
+    color_picker_documentation,
+    date_documentation,
+    doc,
+    input_documentation,
+    joystick_documentation,
+    knob_documentation,
+    number_documentation,
+    radio_documentation,
+    range_documentation,
+    select_documentation,
+    slider_documentation,
+    switch_documentation,
+    textarea_documentation,
+    time_documentation,
+    toggle_documentation,
+    upload_documentation,
+)
 
 doc.title('*Controls*')
 

+ 21 - 5
website/documentation/content/section_data_elements.py

@@ -1,10 +1,26 @@
 from nicegui import optional_features
 
-from . import (aggrid_documentation, circular_progress_documentation, code_documentation, doc, echart_documentation,
-               editor_documentation, highchart_documentation, json_editor_documentation, leaflet_documentation,
-               line_plot_documentation, linear_progress_documentation, log_documentation, matplotlib_documentation,
-               plotly_documentation, pyplot_documentation, scene_documentation, spinner_documentation,
-               table_documentation, tree_documentation)
+from . import (
+    aggrid_documentation,
+    circular_progress_documentation,
+    code_documentation,
+    doc,
+    echart_documentation,
+    editor_documentation,
+    highchart_documentation,
+    json_editor_documentation,
+    leaflet_documentation,
+    line_plot_documentation,
+    linear_progress_documentation,
+    log_documentation,
+    matplotlib_documentation,
+    plotly_documentation,
+    pyplot_documentation,
+    scene_documentation,
+    spinner_documentation,
+    table_documentation,
+    tree_documentation,
+)
 
 doc.title('*Data* Elements')
 

+ 25 - 7
website/documentation/content/section_page_layout.py

@@ -1,11 +1,29 @@
 from nicegui import ui
 
-from . import (card_documentation, carousel_documentation, column_documentation, context_menu_documentation,
-               dialog_documentation, doc, expansion_documentation, grid_documentation, list_documentation,
-               menu_documentation, notification_documentation, notify_documentation, pagination_documentation,
-               row_documentation, scroll_area_documentation, separator_documentation, space_documentation,
-               splitter_documentation, stepper_documentation, tabs_documentation, timeline_documentation,
-               tooltip_documentation)
+from . import (
+    card_documentation,
+    carousel_documentation,
+    column_documentation,
+    context_menu_documentation,
+    dialog_documentation,
+    doc,
+    expansion_documentation,
+    grid_documentation,
+    list_documentation,
+    menu_documentation,
+    notification_documentation,
+    notify_documentation,
+    pagination_documentation,
+    row_documentation,
+    scroll_area_documentation,
+    separator_documentation,
+    space_documentation,
+    splitter_documentation,
+    stepper_documentation,
+    tabs_documentation,
+    timeline_documentation,
+    tooltip_documentation,
+)
 
 doc.title('Page *Layout*')
 
@@ -44,7 +62,7 @@ doc.intro(list_documentation)
     ```
 
     Alternatively, you can remove individual elements by calling
-    
+
     - `container.remove(element: Element)`,
     - `container.remove(index: int)`, or
     - `element.delete()`.

+ 9 - 3
website/documentation/content/section_pages_routing.py

@@ -2,8 +2,14 @@ import uuid
 
 from nicegui import app, ui
 
-from . import (doc, download_documentation, navigate_documentation, page_documentation, page_layout_documentation,
-               page_title_documentation)
+from . import (
+    doc,
+    download_documentation,
+    navigate_documentation,
+    page_documentation,
+    page_layout_documentation,
+    page_title_documentation,
+)
 
 CONSTANT_UUID = str(uuid.uuid4())
 
@@ -41,7 +47,7 @@ doc.intro(page_layout_documentation)
 
 @doc.demo('Parameter injection', '''
     Thanks to FastAPI, a page function accepts optional parameters to provide
-    [path parameters](https://fastapi.tiangolo.com/tutorial/path-params/), 
+    [path parameters](https://fastapi.tiangolo.com/tutorial/path-params/),
     [query parameters](https://fastapi.tiangolo.com/tutorial/query-params/) or the whole incoming
     [request](https://fastapi.tiangolo.com/advanced/using-request-directly/) for accessing
     the body payload, headers, cookies and more.

+ 4 - 4
website/documentation/content/section_styling_appearance.py

@@ -91,14 +91,14 @@ def styling_demo():
                         ```
                     ''')
             with browser_window(classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window'):
-                element: ui.element = select_element.value("element")
+                element: ui.element = select_element.value('element')
     live_demo_ui()
 
 
 @doc.demo('Tailwind CSS', '''
     [Tailwind CSS](https://tailwindcss.com/) is a CSS framework for rapidly building custom user interfaces.
     NiceGUI provides a fluent, auto-complete friendly interface for adding Tailwind classes to UI elements.
-    
+
     You can discover available classes by navigating the methods of the `tailwind` property.
     The builder pattern allows you to chain multiple classes together (as shown with "Label A").
     You can also call the `tailwind` property with a list of classes (as shown with "Label B").
@@ -106,7 +106,7 @@ def styling_demo():
     Although this is very similar to using the `classes` method, it is more convenient for Tailwind classes due to auto-completion.
 
     Last but not least, you can also predefine a style and apply it to multiple elements (labels C and D).
-        
+
     Note that sometimes Tailwind is overruled by Quasar styles, e.g. when using `ui.button('Button').tailwind('bg-red-500')`.
     This is a known limitation and not fully in our control.
     But we try to provide solutions like the `color` parameter: `ui.button('Button', color='red-500')`.
@@ -152,7 +152,7 @@ doc.intro(colors_documentation)
 @doc.demo('CSS Variables', '''
     You can customize the appearance of NiceGUI by setting CSS variables.
     Currently, the following variables with their default values are available:
-    
+
     - `--nicegui-default-padding: 1rem`
     - `--nicegui-default-gap: 1rem`
 

+ 11 - 2
website/documentation/content/section_text_elements.py

@@ -1,5 +1,14 @@
-from . import (chat_message_documentation, doc, element_documentation, html_documentation, label_documentation,
-               link_documentation, markdown_documentation, mermaid_documentation, restructured_text_documentation)
+from . import (
+    chat_message_documentation,
+    doc,
+    element_documentation,
+    html_documentation,
+    label_documentation,
+    link_documentation,
+    markdown_documentation,
+    mermaid_documentation,
+    restructured_text_documentation,
+)
 
 doc.title('*Text* Elements')
 

+ 7 - 7
website/documentation/content/storage_documentation.py

@@ -13,20 +13,20 @@ doc.title('Storage')
 
 
 @doc.demo('Storage', '''
-    NiceGUI offers a straightforward mechanism for data persistence within your application. 
+    NiceGUI offers a straightforward mechanism for data persistence within your application.
     It features five built-in storage types:
 
     - `app.storage.tab`:
         Stored server-side in memory, this dictionary is unique to each tab session and can hold arbitrary objects.
         Data will be lost when restarting the server until <https://github.com/zauberzeug/nicegui/discussions/2841> is implemented.
-        This storage is only available within [page builder functions](/documentation/page) 
+        This storage is only available within [page builder functions](/documentation/page)
         and requires an established connection, obtainable via [`await client.connected()`](/documentation/page#wait_for_client_connection).
     - `app.storage.client`:
         Also stored server-side in memory, this dictionary is unique to each client connection and can hold arbitrary objects.
         Data will be discarded when the page is reloaded or the user navigates to another page.
-        Unlike data stored in `app.storage.tab` which can be persisted on the server even for days, 
-        `app.storage.client` helps caching resource-hungry objects such as a streaming or database connection you need to keep alive 
-        for dynamic site updates but would like to discard as soon as the user leaves the page or closes the browser. 
+        Unlike data stored in `app.storage.tab` which can be persisted on the server even for days,
+        `app.storage.client` helps caching resource-hungry objects such as a streaming or database connection you need to keep alive
+        for dynamic site updates but would like to discard as soon as the user leaves the page or closes the browser.
         This storage is only available within [page builder functions](/documentation/page).
     - `app.storage.user`:
         Stored server-side, each dictionary is associated with a unique identifier held in a browser session cookie.
@@ -42,7 +42,7 @@ doc.title('Storage')
     The user storage and browser storage are only available within `page builder functions </documentation/page>`_
     because they are accessing the underlying `Request` object from FastAPI.
     Additionally these two types require the `storage_secret` parameter in`ui.run()` to encrypt the browser session cookie.
-    
+
     | Storage type                | `tab`  | `client` | `user` | `general` | `browser` |
     |-----------------------------|--------|----------|--------|-----------|-----------|
     | Location                    | Server | Server   | Server | Server    | Browser   |
@@ -116,7 +116,7 @@ def ui_state():
     It is also more secure to use such a volatile storage for scenarios like logging into a bank account or accessing a password manager.
 ''')
 def tab_storage():
-    from nicegui import app, Client
+    from nicegui import Client, app
 
     # @ui.page('/')
     # async def index(client: Client):

+ 2 - 2
website/documentation/content/table_documentation.py

@@ -127,7 +127,7 @@ def table_with_drop_down_selection():
 
 
 @doc.demo('Table from Pandas DataFrame', '''
-    You can create a table from a Pandas DataFrame using the `from_pandas` method. 
+    You can create a table from a Pandas DataFrame using the `from_pandas` method.
     This method takes a Pandas DataFrame as input and returns a table.
 ''')
 def table_from_pandas_demo():
@@ -264,7 +264,7 @@ def computed_fields():
     You can use scoped slots to conditionally format the content of a cell.
     See the [Quasar documentation](https://quasar.dev/vue-components/table#example--body-cell-slot)
     for more information about body-cell slots.
-    
+
     In this demo we use a `q-badge` to display the age in red if the person is under 21 years old.
     We use the `body-cell-age` slot to insert the `q-badge` into the `age` column.
     The ":color" attribute of the `q-badge` is set to "red" if the age is under 21, otherwise it is set to "green".

+ 1 - 1
website/documentation/content/tabs_documentation.py

@@ -25,7 +25,7 @@ def main_demo() -> None:
 
 @doc.demo('Name, label, icon', '''
     The `ui.tab` element has a `label` property that can be used to display a different text than the `name`.
-    The `name` can also be used instead of the `ui.tab` objects to associate a `ui.tab` with a `ui.tab_panel`. 
+    The `name` can also be used instead of the `ui.tab` objects to associate a `ui.tab` with a `ui.tab_panel`.
     Additionally each tab can have an `icon`.
 ''')
 def name_and_label():

+ 1 - 1
website/documentation/content/textarea_documentation.py

@@ -11,7 +11,7 @@ def main_demo() -> None:
 
 
 @doc.demo('Clearable', '''
-    The `clearable` prop from [Quasar](https://quasar.dev/) adds a button to the input that clears the text.    
+    The `clearable` prop from [Quasar](https://quasar.dev/) adds a button to the input that clears the text.
 ''')
 def clearable():
     i = ui.textarea(value='some text').props('clearable')

+ 1 - 1
website/documentation/demo.py

@@ -20,7 +20,7 @@ def demo(f: Callable, *, lazy: bool = True, tab: Optional[Union[str, Callable]]
     """Render a callable as a demo with Python code and browser window."""
     with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
         code = inspect.getsource(f).split('# END OF DEMO', 1)[0].strip().splitlines()
-        code = [line for line in code if not line.endswith("# HIDE")]
+        code = [line for line in code if not line.endswith('# HIDE')]
         while not code[0].strip().startswith('def') and not code[0].strip().startswith('async def'):
             del code[0]
         del code[0]

+ 1 - 1
website/main_page.py

@@ -82,7 +82,7 @@ def create() -> None:
         with ui.expansion('...or use Docker to run your main.py').classes('w-full gap-2 bold-links arrow-links'):
             with ui.row().classes('mt-8 w-full justify-center items-center gap-8'):
                 ui.markdown('''
-                    With our [multi-arch Docker image](https://hub.docker.com/repository/docker/zauberzeug/nicegui) 
+                    With our [multi-arch Docker image](https://hub.docker.com/repository/docker/zauberzeug/nicegui)
                     you can start the server without installing any packages.
 
                     The command searches for `main.py` in in your current directory and makes the app available at http://localhost:8888.

+ 1 - 1
website/static/github.svg

@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>

+ 1 - 1
website/static/happy_face.svg

@@ -13,4 +13,4 @@
     <path class="svg_face" d="M7.3,13.78c-.44,2.28-.58,4.66-.61,7,.01-2.33,.15-4.71,.61-7Z"/>
     <path class="svg_face" d="M58.69,20.11s-.99-6.57-7.03-6.71c-2.46-.05-4.13,1.75-7.1,1.77-3.8,.02-5.11-1.97-5.11-1.97"/>
     <path class="svg_face" d="M14.82,51.45s4.92,6.49,15.65,6.49,16.74-6.49,16.74-6.49"/>
-</svg>
+</svg>

+ 1 - 1
website/static/nicegui_word.svg

@@ -12,4 +12,4 @@
     <path class="svg_letter2" d="M41.56,14.4c-.81,0-1.57-.16-2.27-.47-.71-.31-1.33-.74-1.86-1.3-.53-.56-.95-1.2-1.25-1.92-.3-.73-.45-1.5-.45-2.33s.15-1.59,.44-2.32c.29-.72,.71-1.36,1.25-1.91s1.16-.98,1.86-1.3c.71-.31,1.47-.47,2.29-.47,.72,0,1.38,.11,1.96,.32s1.16,.54,1.71,1c.09,.07,.14,.15,.16,.25,.01,.1,0,.19-.04,.27s-.25,.33-.33,.37-.17,.06-.28,.04c-.1,0-.21-.05-.31-.13-.39-.34-.81-.59-1.27-.76s-1-.25-1.61-.25c-.65,0-1.26,.13-1.82,.39-.57,.26-1.06,.62-1.49,1.07-.43,.45-.77,.97-1.01,1.56-.25,.59-.37,1.21-.37,1.88s.12,1.32,.37,1.91,.58,1.11,1.01,1.56c.43,.45,.93,.8,1.49,1.06,.56,.25,1.17,.38,1.82,.38,.55,0,1.07-.09,1.56-.27,.49-.18,.95-.44,1.39-.78,.12-.1,.25-.14,.38-.12,.13,.02,.25,.08,.35,.18,.1,.1,.15,.23,.15,.39,0,.08,0,.15-.03,.22-.02,.07-.07,.13-.14,.2-.51,.47-1.08,.8-1.71,1s-1.29,.29-1.96,.29Zm3.84-1.71l-1.14-.24v-3.02h-2.34c-.17,0-.31-.05-.42-.15-.11-.1-.17-.23-.17-.38s.05-.27,.17-.37c.11-.1,.25-.14,.42-.14h2.89c.17,0,.31,.05,.42,.16,.11,.1,.17,.24,.17,.41v3.72Z"/>
     <path class="svg_letter2" d="M52.05,14.49c-.91,0-1.72-.18-2.42-.53s-1.25-.83-1.65-1.46c-.39-.62-.59-1.33-.59-2.13V3.12c0-.17,.05-.31,.17-.42,.11-.11,.25-.17,.42-.17s.31,.05,.42,.17c.11,.11,.17,.25,.17,.42v7.26c0,.59,.15,1.11,.44,1.56,.29,.45,.71,.8,1.24,1.06,.53,.26,1.13,.38,1.82,.38s1.26-.13,1.79-.38c.52-.25,.93-.61,1.22-1.06,.29-.45,.44-.97,.44-1.56V3.12c0-.17,.05-.31,.16-.42,.11-.11,.25-.17,.42-.17,.18,0,.32,.05,.43,.17,.11,.11,.16,.25,.16,.42v7.26c0,.8-.2,1.51-.59,2.13-.4,.62-.94,1.11-1.63,1.46-.7,.35-1.49,.53-2.39,.53Z"/>
     <path class="svg_letter2" d="M59.36,14.25c-.17,0-.31-.05-.42-.17-.11-.11-.17-.25-.17-.42V3.12c0-.17,.05-.31,.17-.42,.11-.11,.25-.17,.42-.17s.31,.05,.42,.17c.11,.11,.17,.25,.17,.42V13.67c0,.17-.06,.31-.17,.42-.11,.11-.25,.17-.42,.17Z"/>
-</svg>
+</svg>