Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions nicegui/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from .outbox import Outbox
from .sub_pages_router import SubPagesRouter
from .translations import translations
from .version import __version__

if TYPE_CHECKING:
from .page import page
Expand Down Expand Up @@ -161,7 +160,7 @@ def build_response(self, request: Request, status_code: int = 200) -> Response:
name='index.html',
context={
'request': request,
'version': __version__,
'version': helpers.version_signature(),
'elements': elements.translate(HTML_ESCAPE_TABLE),
'head_html': self.head_html,
'body_html': '<style>' + '\n'.join(vue_styles) + '</style>\n' + self.body_html + '\n' + '\n'.join(vue_html),
Expand Down
13 changes: 6 additions & 7 deletions nicegui/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
from typing import TYPE_CHECKING, Callable

from .dataclasses import KWONLY_SLOTS
from .helpers import hash_file_path
from .helpers import hash_file_path, version_signature
from .vbuild import VBuild
from .version import __version__

if TYPE_CHECKING:
from .element import Element
Expand Down Expand Up @@ -172,19 +171,19 @@ def generate_resources(prefix: str, elements: Iterable[Element]) -> tuple[list[s
# build the importmap structure for libraries
for key, library in libraries.items():
if key not in done_libraries:
imports[library.name] = f'{prefix}/_nicegui/{__version__}/libraries/{key}'
imports[library.name] = f'{prefix}/_nicegui/{version_signature()}/libraries/{key}'
done_libraries.add(key)

# build the importmap structure for ESM modules
for key, esm_module in esm_modules.items():
imports[f'{esm_module.name}'] = f'{prefix}/_nicegui/{__version__}/esm/{key}/index.js'
imports[f'{esm_module.name}/'] = f'{prefix}/_nicegui/{__version__}/esm/{key}/'
imports[f'{esm_module.name}'] = f'{prefix}/_nicegui/{version_signature()}/esm/{key}/index.js'
imports[f'{esm_module.name}/'] = f'{prefix}/_nicegui/{version_signature()}/esm/{key}/'

# build the none-optimized component (i.e. the Vue component)
for key, vue_component in vue_components.items():
if key not in done_components:
vue_html.append(vue_component.html)
url = f'{prefix}/_nicegui/{__version__}/components/{vue_component.key}'
url = f'{prefix}/_nicegui/{version_signature()}/components/{vue_component.key}'
js_imports.append(f'import {{ default as {vue_component.name} }} from "{url}";')
js_imports.append(f"{vue_component.name}.template = '#tpl-{vue_component.name}';")
js_imports.append(f'app.component("{vue_component.tag}", {vue_component.name});')
Expand All @@ -196,7 +195,7 @@ def generate_resources(prefix: str, elements: Iterable[Element]) -> tuple[list[s
if element.component:
js_component = element.component
if js_component.key not in done_components and js_component.path.suffix.lower() == '.js':
url = f'{prefix}/_nicegui/{__version__}/components/{js_component.key}'
url = f'{prefix}/_nicegui/{version_signature()}/components/{js_component.key}'
js_imports.append(f'import {{ default as {js_component.name} }} from "{url}";')
js_imports.append(f'app.component("{js_component.tag}", {js_component.name});')
js_imports_urls.append(url)
Expand Down
5 changes: 2 additions & 3 deletions nicegui/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
from .props import Props
from .slot import Slot
from .style import Style
from .version import __version__

if TYPE_CHECKING:
from .client import Client
Expand Down Expand Up @@ -156,7 +155,7 @@ def add_resource(self, path: str | Path) -> None:
"""
path_ = Path(path)
resource = register_resource(path_, max_time=path_.stat().st_mtime)
self._props['resource_path'] = f'/_nicegui/{__version__}/resources/{resource.key}'
self._props['resource_path'] = f'/_nicegui/{helpers.version_signature()}/resources/{resource.key}'

def add_dynamic_resource(self, name: str, function: Callable) -> None:
"""Add a dynamic resource to the element which returns the result of a function.
Expand All @@ -165,7 +164,7 @@ def add_dynamic_resource(self, name: str, function: Callable) -> None:
:param function: function that returns the resource response
"""
register_dynamic_resource(name, function)
self._props['dynamic_resource_path'] = f'/_nicegui/{__version__}/dynamic_resources'
self._props['dynamic_resource_path'] = f'/_nicegui/{helpers.version_signature()}/dynamic_resources'

def add_slot(self, name: str, template: str | None = None) -> Slot:
"""Add a slot to the element.
Expand Down
5 changes: 2 additions & 3 deletions nicegui/favicon.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
from fastapi.responses import FileResponse, Response, StreamingResponse

from . import core
from .helpers import is_file
from .version import __version__
from .helpers import is_file, version_signature

if TYPE_CHECKING:
from .page import page
Expand All @@ -27,7 +26,7 @@ def get_favicon_url(page: page, prefix: str) -> str:
"""Return the URL of the favicon for a given page."""
favicon = page.favicon or core.app.config.favicon
if not favicon:
return f'{prefix}/_nicegui/{__version__}/static/favicon.ico'
return f'{prefix}/_nicegui/{version_signature()}/static/favicon.ico'

favicon = str(favicon).strip()
if _is_remote_url(favicon):
Expand Down
15 changes: 15 additions & 0 deletions nicegui/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import functools
import hashlib
import hmac
import os
import socket
import struct
Expand All @@ -14,8 +15,10 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any

from . import core
from .context import context
from .logging import log
from .version import __version__

if TYPE_CHECKING:
from .element import Element
Expand Down Expand Up @@ -146,3 +149,15 @@ def require_top_level_layout(element: Element) -> None:
f'Found top level layout element "{element.__class__.__name__}" inside element "{parent.__class__.__name__}". '
'Top level layout elements can not be nested but must be direct children of the page content.',
)


@functools.cache
def version_signature() -> str:
"""Compute a version signature based on the storage secret and the NiceGUI version.

If the storage secret is not available, the plain-text NiceGUI version is returned."""
storage_secret = core.app.storage.secret

if storage_secret:
return hmac.new(storage_secret.encode(), str(__version__).encode(), hashlib.sha256).hexdigest()
return __version__
7 changes: 4 additions & 3 deletions nicegui/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
from starlette.requests import Request
from starlette.responses import Response

from nicegui.helpers import version_signature

from . import core
from .version import __version__


class RedirectWithPrefixMiddleware(BaseHTTPMiddleware):
Expand All @@ -21,7 +22,7 @@ class SetCacheControlMiddleware(BaseHTTPMiddleware):

async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
response = await call_next(request)
if request.url.path.startswith(f'/_nicegui/{__version__}/') \
and not request.url.path.startswith(f'/_nicegui/{__version__}/dynamic_resources/'):
if request.url.path.startswith(f'/_nicegui/{version_signature()}/') \
and not request.url.path.startswith(f'/_nicegui/{version_signature()}/dynamic_resources/'):
response.headers['Cache-Control'] = core.app.config.cache_control_directives
return response
14 changes: 7 additions & 7 deletions nicegui/nicegui.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
from .client import Client
from .dependencies import dynamic_resources, esm_modules, js_components, libraries, resources, vue_components
from .error import error_content
from .helpers import version_signature
from .json import NiceGUIJSONResponse
from .logging import log
from .page import page
from .page_arguments import PageArguments
from .persistence import PersistentDict
from .slot import Slot
from .staticfiles import CacheControlledStaticFiles
from .version import __version__


@asynccontextmanager
Expand Down Expand Up @@ -60,10 +60,10 @@ async def __call__(self, scope, receive, send):
directory=(Path(__file__).parent / 'static').resolve(),
follow_symlink=True,
)
app.mount(f'/_nicegui/{__version__}/static', static_files, name='static')
app.mount(f'/_nicegui/{version_signature()}/static', static_files, name='static')


@app.get(f'/_nicegui/{__version__}' + '/libraries/{key:path}')
@app.get(f'/_nicegui/{version_signature()}' + '/libraries/{key:path}')
def _get_library(key: str) -> FileResponse:
is_map = key.endswith('.map')
dict_key = key[:-4] if is_map else key
Expand All @@ -76,7 +76,7 @@ def _get_library(key: str) -> FileResponse:
raise HTTPException(status_code=404, detail=f'library "{key}" not found')


@app.get(f'/_nicegui/{__version__}' + '/components/{key:path}')
@app.get(f'/_nicegui/{version_signature()}' + '/components/{key:path}')
def _get_component(key: str) -> Response:
if key in js_components and js_components[key].path.exists():
return FileResponse(js_components[key].path, media_type='text/javascript')
Expand All @@ -85,7 +85,7 @@ def _get_component(key: str) -> Response:
raise HTTPException(status_code=404, detail=f'component "{key}" not found')


@app.get(f'/_nicegui/{__version__}' + '/resources/{key}/{path:path}')
@app.get(f'/_nicegui/{version_signature()}' + '/resources/{key}/{path:path}')
def _get_resource(key: str, path: str) -> FileResponse:
if key in resources:
filepath = resources[key].path / path
Expand All @@ -97,14 +97,14 @@ def _get_resource(key: str, path: str) -> FileResponse:
raise HTTPException(status_code=404, detail=f'resource "{key}" not found')


@app.get(f'/_nicegui/{__version__}' + '/dynamic_resources/{name}')
@app.get(f'/_nicegui/{version_signature()}' + '/dynamic_resources/{name}')
def _get_dynamic_resource(name: str) -> Response:
if name in dynamic_resources:
return dynamic_resources[name].function()
raise HTTPException(status_code=404, detail=f'dynamic resource "{name}" not found')


@app.get(f'/_nicegui/{__version__}' + '/esm/{key}/{path:path}')
@app.get(f'/_nicegui/{version_signature()}' + '/esm/{key}/{path:path}')
def _get_esm(key: str, path: str) -> FileResponse:
if key in esm_modules:
filepath = esm_modules[key].path / path
Expand Down
23 changes: 12 additions & 11 deletions tests/test_endpoint_docs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import httpx
import pytest

from nicegui import __version__, ui
from nicegui import ui
from nicegui.helpers import version_signature
from nicegui.testing import Screen


Expand Down Expand Up @@ -43,11 +44,11 @@ def page():

screen.open('/')
assert get_openapi_paths() == {
f'/_nicegui/{__version__}/libraries/{{key}}',
f'/_nicegui/{__version__}/components/{{key}}',
f'/_nicegui/{__version__}/resources/{{key}}/{{path}}',
f'/_nicegui/{__version__}/dynamic_resources/{{name}}',
f'/_nicegui/{__version__}/esm/{{key}}/{{path}}',
f'/_nicegui/{version_signature()}/libraries/{{key}}',
f'/_nicegui/{version_signature()}/components/{{key}}',
f'/_nicegui/{version_signature()}/resources/{{key}}/{{path}}',
f'/_nicegui/{version_signature()}/dynamic_resources/{{name}}',
f'/_nicegui/{version_signature()}/esm/{{key}}/{{path}}',
}


Expand All @@ -61,9 +62,9 @@ def page():
screen.open('/')
assert get_openapi_paths() == {
'/',
f'/_nicegui/{__version__}/libraries/{{key}}',
f'/_nicegui/{__version__}/components/{{key}}',
f'/_nicegui/{__version__}/resources/{{key}}/{{path}}',
f'/_nicegui/{__version__}/dynamic_resources/{{name}}',
f'/_nicegui/{__version__}/esm/{{key}}/{{path}}',
f'/_nicegui/{version_signature()}/libraries/{{key}}',
f'/_nicegui/{version_signature()}/components/{{key}}',
f'/_nicegui/{version_signature()}/resources/{{key}}/{{path}}',
f'/_nicegui/{version_signature()}/dynamic_resources/{{name}}',
f'/_nicegui/{version_signature()}/esm/{{key}}/{{path}}',
}
13 changes: 8 additions & 5 deletions tests/test_prod_js.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from selenium.webdriver.common.by import By

from nicegui import __version__, ui
from nicegui import ui
from nicegui.helpers import version_signature
from nicegui.testing import Screen


Expand All @@ -12,8 +13,8 @@ def page():
ui.label('Hello, world!')

screen.open('/')
screen.selenium.find_element(By.XPATH, f'//script[@src="/_nicegui/{__version__}/static/vue.global.js"]')
screen.selenium.find_element(By.XPATH, f'//script[@src="/_nicegui/{__version__}/static/quasar.umd.js"]')
screen.selenium.find_element(By.XPATH, f'//script[@src="/_nicegui/{version_signature()}/static/vue.global.js"]')
screen.selenium.find_element(By.XPATH, f'//script[@src="/_nicegui/{version_signature()}/static/quasar.umd.js"]')


def test_prod_mode(screen: Screen):
Expand All @@ -24,5 +25,7 @@ def page():
ui.label('Hello, world!')

screen.open('/')
screen.selenium.find_element(By.XPATH, f'//script[@src="/_nicegui/{__version__}/static/vue.global.prod.js"]')
screen.selenium.find_element(By.XPATH, f'//script[@src="/_nicegui/{__version__}/static/quasar.umd.prod.js"]')
screen.selenium.find_element(
By.XPATH, f'//script[@src="/_nicegui/{version_signature()}/static/vue.global.prod.js"]')
screen.selenium.find_element(
By.XPATH, f'//script[@src="/_nicegui/{version_signature()}/static/quasar.umd.prod.js"]')
13 changes: 8 additions & 5 deletions tests/test_serving_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import httpx
import pytest

from nicegui import __version__, app, ui
from nicegui import app, ui
from nicegui.helpers import version_signature
from nicegui.testing import Screen

from .test_helpers import TEST_DIR
Expand Down Expand Up @@ -137,11 +138,12 @@ def page():

screen.open('/')

response = httpx.get(f'http://localhost:{Screen.PORT}/_nicegui/{__version__}/static/vue.global.js', timeout=5)
response = httpx.get(
f'http://localhost:{Screen.PORT}/_nicegui/{version_signature()}/static/vue.global.js', timeout=5)
assert response.status_code == 200
assert response.headers['Content-Type'].startswith('text/javascript')

response = httpx.get(f'http://localhost:{Screen.PORT}/_nicegui/{__version__}/static/nicegui.css', timeout=5)
response = httpx.get(f'http://localhost:{Screen.PORT}/_nicegui/{version_signature()}/static/nicegui.css', timeout=5)
assert response.status_code == 200
assert response.headers['Content-Type'].startswith('text/css')

Expand All @@ -156,12 +158,13 @@ def page():
screen.open('/')

# resources are served with cache-control headers from `ui.run`
response1 = httpx.get(f'http://localhost:{Screen.PORT}/_nicegui/{__version__}/static/nicegui.css', timeout=5)
response1 = httpx.get(
f'http://localhost:{Screen.PORT}/_nicegui/{version_signature()}/static/nicegui.css', timeout=5)
assert 'immutable' in response1.headers.get('Cache-Control', '')

# dynamic resources are _not_ served with cache-control headers from `ui.run`
response2 = httpx.get(
f'http://localhost:{Screen.PORT}/_nicegui/{__version__}/dynamic_resources/codehilite.css', timeout=5)
f'http://localhost:{Screen.PORT}/_nicegui/{version_signature()}/dynamic_resources/codehilite.css', timeout=5)
assert 'immutable' not in response2.headers.get('Cache-Control', '')

# static resources are _not_ served with cache-control headers from `ui.run`
Expand Down
5 changes: 3 additions & 2 deletions website/search.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from nicegui import __version__, background_tasks, events, ui
from nicegui import background_tasks, events, ui
from nicegui.helpers import version_signature

from .documentation import CustomRestructuredText as custom_restructured_text
from .documentation.search import search_index
Expand All @@ -10,7 +11,7 @@ def __init__(self) -> None:
ui.add_head_html(r'''
<script>
async function loadSearchData() {
const response = await fetch("/static/search_index.json?version=''' + __version__ + r'''");
const response = await fetch("/static/search_index.json?version=''' + version_signature() + r'''");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
Expand Down