Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
9 changes: 6 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ jobs:
test:
strategy:
matrix:
python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "pypy3.10", "pypy3.11"]
fail-fast: false
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 120
steps:
- uses: actions/checkout@v5
- name: set up Poetry
Expand All @@ -46,10 +46,13 @@ jobs:
set -x
poetry install --all-extras
# install packages to run the examples
poetry run pip install opencv-python opencv-contrib-python-headless httpx isort replicate langchain openai simpy tortoise-orm
poetry run pip install httpx isort replicate openai simpy tortoise-orm
poetry run pip install -r tests/requirements.txt
# try fix issue with importlib_resources
poetry run pip install importlib-resources
- name: ensure pydantic v1 for pypy
if: ${{ startsWith(matrix.python, 'pypy') }}
run: poetry run pip install 'pydantic<2.0.0'
- name: test startup
run: poetry run ./test_startup.sh
- name: setup chromedriver
Expand Down
4 changes: 2 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ vbuild = [
]
jinja2 = "^3.1.6" # https://github.com/zauberzeug/nicegui/security/dependabot/44
python-multipart = ">=0.0.18"
orjson = {version = ">=3.9.15", markers = "platform_machine != 'i386' and platform_machine != 'i686'"} # https://github.com/zauberzeug/nicegui/security/dependabot/29, orjson does not support 32bit
orjson = {version = ">=3.9.15", markers = "platform_machine != 'i386' and platform_machine != 'i686' and platform_python_implementation != 'PyPy'"} # https://github.com/zauberzeug/nicegui/security/dependabot/29, orjson does not support 32bit
itsdangerous = "^2.1.2"
aiofiles = ">=23.1.0"
httpx = ">=0.24.0"
Expand Down
20 changes: 20 additions & 0 deletions test_startup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,26 @@ do
continue # because there is no serial port in github actions
fi

# skip if path is examples/chat_with_ai
if test $path = "examples/chat_with_ai"; then
continue # because it requires langchain which requires Pydantic v2 and PyPy appears not compatible
fi

# skip if path is examples/opencv_webcam
if test $path = "examples/opencv_webcam"; then
continue # because it requires opencv-python and PyPy appears not compatible
fi

# skip if path is examples/pandas_dataframe
if test $path = "examples/pandas_dataframe"; then
continue # because numpy is not available on PyPy, which is required by pandas
fi

# skip if path is examples/zeromq
if test $path = "examples/zeromq"; then
continue # because it uses ui.line_plot which uses matplotlib which requires numpy which is not available on PyPy
fi

# install all requirements except nicegui
if test -f $path/requirements.txt; then
sed '/^nicegui/d' $path/requirements.txt > $path/requirements.tmp.txt || exit 1 # remove nicegui from requirements.txt
Expand Down
11 changes: 9 additions & 2 deletions tests/test_aggrid.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import platform
import sys
from datetime import datetime, timedelta, timezone

import pandas as pd
import polars as pl
import pytest
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
Expand Down Expand Up @@ -169,13 +169,16 @@ def replace():


@pytest.mark.parametrize('df_type', ['pandas', 'polars'])
@pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy no pandas, assuming no polars either')
def test_create_from_dataframe(screen: Screen, df_type: str):
@ui.page('/')
def page():
if df_type == 'pandas':
import pandas as pd
df = pd.DataFrame({'name': ['Alice', 'Bob'], 'age': [18, 21], 42: 'answer'})
ui.aggrid.from_pandas(df)
else:
import polars as pl
df = pl.DataFrame({'name': ['Alice', 'Bob'], 'age': [18, 21], '42': 'answer'})
ui.aggrid.from_polars(df)

Expand Down Expand Up @@ -211,10 +214,13 @@ def page():


@pytest.mark.parametrize('df_type', ['pandas', 'polars'])
@pytest.mark.skipif(sys.version_info[:2] == (3, 8), reason='Skipping test for Python 3.8')
@pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy no pandas, assuming no polars either')
def test_problematic_datatypes(screen: Screen, df_type: str):
@ui.page('/')
def page():
if df_type == 'pandas':
import pandas as pd
df = pd.DataFrame({
'datetime_col': [datetime(2020, 1, 1)],
'datetime_col_tz': [datetime(2020, 1, 2, tzinfo=timezone.utc)],
Expand All @@ -224,6 +230,7 @@ def page():
})
ui.aggrid.from_pandas(df)
else:
import polars as pl
df = pl.DataFrame({
'datetime_col': [datetime(2020, 1, 1)],
'datetime_col_tz': [datetime(2020, 1, 2, tzinfo=timezone.utc)],
Expand Down
2 changes: 2 additions & 0 deletions tests/test_binding.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import copy
import platform
import weakref
from typing import Optional

Expand Down Expand Up @@ -176,6 +177,7 @@ def page():
await user.should_see('y=2')


@pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy no reference counting')
def test_automatic_cleanup(screen: Screen):
class Model:
value = binding.BindableProperty()
Expand Down
23 changes: 17 additions & 6 deletions tests/test_chat_message.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import platform

import pytest
from html_sanitizer import Sanitizer
from selenium.webdriver.common.by import By

from nicegui import ui
Expand All @@ -11,28 +12,38 @@ def test_text_vs_html(screen: Screen):
def page():
ui.chat_message('10&euro;')
ui.chat_message('20&euro;', text_html=True, sanitize=False)
ui.chat_message('30&euro;', text_html=True, sanitize=Sanitizer().sanitize)
ui.chat_message('<img src=x onerror=Quasar.Notify.create({message:"40&euro;"})>', text_html=True,
sanitize=False)
ui.chat_message('<img src=x onerror=Quasar.Notify.create({message:"50&euro;"})>', text_html=True, sanitize=str)
ui.chat_message('<img src=x onerror=Quasar.Notify.create({message:"60&euro;"})>', text_html=True,
sanitize=lambda x: x.replace('&euro;', 'EUR'))
ui.chat_message('<img src=x onerror=Quasar.Notify.create({message:"70&euro;"})>', text_html=True,
sanitize=Sanitizer().sanitize)
with pytest.raises(ValueError):
ui.chat_message('80&euro;', text_html=True)

screen.open('/')
screen.should_contain('10&euro;')
screen.should_contain('20€')
screen.should_contain('30€')
screen.should_contain('40€')
screen.should_contain('50€')
screen.should_contain('60EUR')
screen.should_not_contain('70€')
screen.should_not_contain('80€')


@pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='Under pytest, PyPy fails to import html_sanitizer')
def test_sanitize_using_sanitizer(screen: Screen):
from html_sanitizer import Sanitizer # pylint: disable=import-outside-toplevel

@ui.page('/')
def page():
ui.chat_message('30&euro;', text_html=True, sanitize=Sanitizer().sanitize)
ui.chat_message('<img src=x onerror=Quasar.Notify.create({message:"70&euro;"})>', text_html=True,
sanitize=Sanitizer().sanitize)

screen.open('/')
screen.should_contain('30€')
screen.should_not_contain('70€')


def test_newline(screen: Screen):
@ui.page('/')
def page():
Expand Down
3 changes: 3 additions & 0 deletions tests/test_element.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import platform
import weakref
from typing import Optional

Expand Down Expand Up @@ -395,6 +396,7 @@ async def update():
screen.should_contain('Hello again!')


@pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy no reference counting')
def test_no_cyclic_references_when_deleting_elements(screen: Screen):
elements: weakref.WeakSet = weakref.WeakSet()

Expand All @@ -413,6 +415,7 @@ def page():
assert len(elements) == 0, 'all elements should be deleted immediately'


@pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy no reference counting')
def test_no_cyclic_references_when_deleting_clients(screen: Screen):
labels = weakref.WeakSet()

Expand Down
11 changes: 6 additions & 5 deletions tests/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,17 @@ def page():
screen.open('/')
screen.click('Test')
screen.click('Test')
screen.click('Test')
assert events == [1, 2, 1, 1]
screen.wait(0.5)
assert events == [1, 2, 1]

screen.wait(1.1)
assert events == [1, 2, 1, 1, 2]
screen.wait(1)
assert events == [1, 2, 1, 2]

screen.click('Test')
screen.click('Test')
screen.click('Test')
assert events == [1, 2, 1, 1, 2, 1, 2, 1, 1]
screen.wait(0.5)
assert events == [1, 2, 1, 2, 1, 2, 1, 1]


def test_throttling_variants(screen: Screen):
Expand Down
16 changes: 14 additions & 2 deletions tests/test_html.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from html_sanitizer import Sanitizer
import platform

import pytest

from nicegui import html, ui
from nicegui.testing import Screen
Expand Down Expand Up @@ -29,10 +31,20 @@ def page():
ui.html('<img src=x onerror=Quasar.Notify.create({message:"A"})>', sanitize=False)
ui.html('<img src=x onerror=Quasar.Notify.create({message:"B"})>', sanitize=str)
ui.html('<img src=x onerror=Quasar.Notify.create({message:"C"})>', sanitize=lambda x: x.replace('C', 'C!'))
ui.html('<img src=x onerror=Quasar.Notify.create({message:"D"})>', sanitize=Sanitizer().sanitize)

screen.open('/')
screen.should_contain('A')
screen.should_contain('B')
screen.should_contain('C!')


@pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='Under pytest, PyPy fails to import html_sanitizer')
def test_sanitize_using_sanitizer(screen: Screen):
from html_sanitizer import Sanitizer # pylint: disable=import-outside-toplevel

@ui.page('/')
def page():
ui.html('<img src=x onerror=Quasar.Notify.create({message:"D"})>', sanitize=Sanitizer().sanitize)

screen.open('/')
screen.should_not_contain('D')
5 changes: 4 additions & 1 deletion tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
Need to ensure that we get the same output regardless of the serializer used.
"""

import platform
import sys
from datetime import date, datetime

import numpy as np
import pytest

try:
Expand All @@ -18,11 +18,14 @@


@pytest.mark.skipif('orjson' not in sys.modules, reason='requires the orjson library.')
@pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy no numpy')
def test_json():
# only run test if orjson is available to not break it on 32 bit systems
# or architectures where orjson is not supported.

# pylint: disable=import-outside-toplevel
import numpy as np

from nicegui.json.builtin_wrapper import dumps as builtin_dumps
from nicegui.json.orjson_wrapper import dumps as orjson_dumps

Expand Down
6 changes: 5 additions & 1 deletion tests/test_plotly.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import numpy as np
import platform

import plotly.graph_objects as go
import pytest

from nicegui import ui
from nicegui.testing import Screen


@pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy no numpy')
def test_plotly(screen: Screen):
import numpy as np
@ui.page('/')
def page():
fig = go.Figure(go.Scatter(x=[1, 2, 3], y=[1, 2, 3], name='Trace 1'))
Expand Down
2 changes: 1 addition & 1 deletion tests/test_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def raise_unpicklable_exception():

@ui.page('/')
async def index():
with pytest.raises((AttributeError, PicklingError), match="Can't pickle local object|Can't get local object"):
with pytest.raises((AttributeError, PicklingError), match=r"Can't pickle|Can't get local object"):
ui.label(await run.cpu_bound(raise_unpicklable_exception))

await user.open('/')
Expand Down
6 changes: 5 additions & 1 deletion tests/test_scene.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import platform
import weakref

import numpy as np
import pytest
from selenium.common.exceptions import JavascriptException

from nicegui import ui
Expand Down Expand Up @@ -133,7 +134,9 @@ def page():
assert screen.find_by_tag('canvas')


@pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy no numpy')
def test_rotation_matrix_from_euler():
import numpy as np
omega, phi, kappa = 0.1, 0.2, 0.3
Rx = np.array([[1, 0, 0], [0, np.cos(omega), -np.sin(omega)], [0, np.sin(omega), np.cos(omega)]])
Ry = np.array([[np.cos(phi), 0, np.sin(phi)], [0, 1, 0], [-np.sin(phi), 0, np.cos(phi)]])
Expand Down Expand Up @@ -204,6 +207,7 @@ def page():
assert screen.selenium.execute_script(f'return scene_{scene.html_id}.children.length') == 5


@pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy no reference counting')
def test_no_cyclic_references(screen: Screen):
objects: weakref.WeakSet = weakref.WeakSet()
scene = None
Expand Down
6 changes: 5 additions & 1 deletion tests/test_scene_view.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import numpy as np
import platform

import pytest

from nicegui import ui
from nicegui.testing import Screen
Expand Down Expand Up @@ -40,7 +42,9 @@ def page():
assert screen.selenium.execute_script(f'return getElement({scene_view.id}).scene == getElement({scene.id}).scene')


@pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy no numpy')
def test_camera_move(screen: Screen):
import numpy as np
scene = scene_view = None

@ui.page('/')
Expand Down
Loading