diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 03e3197..4ec60af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: name: Check valid YAML syntax - id: end-of-file-fixer name: Check for newline at end of file - exclude: '^LICENSE$' + exclude: "^LICENSE$" - id: debug-statements name: Check for debugger imports - id: trailing-whitespace @@ -35,7 +35,7 @@ repos: hooks: - id: docformatter name: Run docformatter (formatter for docstrings) - args: ["--in-place", "--config=./pyproject.toml", "-r", "src/fourc_webviewer/", "test/"] + args: ["--in-place", "--config=./pyproject.toml", "-r", "src/fourc_webviewer/", "tests/"] - repo: https://github.com/econchick/interrogate rev: 1.7.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index ebaa6f0..c242937 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,3 +18,9 @@ dependencies = { file = ["requirements.txt"] } [tool.setuptools] package-dir = { "" = "src" } + +# External tools +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = '-m "not gui"' # deactivate tests that require a GUI +markers = ["gui: Tests that require a GUI."] diff --git a/requirements.in b/requirements.in index efdbdf5..cf30e79 100644 --- a/requirements.in +++ b/requirements.in @@ -11,3 +11,7 @@ numpy # include fourcipp from github git+https://github.com/4C-multiphysics/fourcipp.git lnmmeshio>=5.6.3 + +# development +pytest +pre-commit diff --git a/requirements.txt b/requirements.txt index 3bfc3b5..7a74a0e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,12 @@ attrs==25.3.0 # fourcipp # jsonschema # referencing +certifi==2025.4.26 + # via requests +cfgv==3.4.0 + # via pre-commit +charset-normalizer==3.4.2 + # via requests contourpy==1.3.2 # via matplotlib cycler==0.12.1 @@ -24,6 +30,10 @@ deprecation==2.1.0 # via # fourcipp # rapidyaml +distlib==0.3.9 + # via virtualenv +filelock==3.18.0 + # via virtualenv fonttools==4.57.0 # via matplotlib fourcipp @ git+https://github.com/4C-multiphysics/fourcipp.git @@ -32,8 +42,14 @@ frozenlist==1.6.0 # via # aiohttp # aiosignal +identify==2.6.10 + # via pre-commit idna==3.10 - # via yarl + # via + # requests + # yarl +iniconfig==2.1.0 + # via pytest jsonschema==4.23.0 # via fourcipp jsonschema-rs==0.30.0 @@ -47,11 +63,15 @@ kiwisolver==1.4.8 lnmmeshio==5.6.3 # via -r requirements.in loguru==0.7.3 - # via fourcipp + # via + # fourcipp + # lnmmeshio markdown-it-py==3.0.0 # via rich matplotlib==3.10.1 - # via vtk + # via + # pyvista + # vtk mdurl==0.1.2 # via markdown-it-py meshio==5.3.5 @@ -66,6 +86,8 @@ multidict==6.4.3 # yarl narwhals==1.38.0 # via plotly +nodeenv==1.9.1 + # via pre-commit numpy==2.2.5 # via # -r requirements.in @@ -75,22 +97,35 @@ numpy==2.2.5 # matplotlib # meshio # pandas + # pyvista packaging==25.0 # via # deprecation # fourcipp # matplotlib # plotly + # pooch + # pytest pandas==2.2.3 # via -r requirements.in pillow==11.2.1 - # via matplotlib + # via + # matplotlib + # pyvista +platformdirs==4.3.8 + # via + # pooch + # virtualenv plotly==6.0.1 # via # -r requirements.in # trame-plotly -progress==1.6 - # via lnmmeshio +pluggy==1.6.0 + # via pytest +pooch==1.8.2 + # via pyvista +pre-commit==4.2.0 + # via -r requirements.in propcache==0.3.1 # via # aiohttp @@ -99,6 +134,8 @@ pygments==2.19.1 # via rich pyparsing==3.2.3 # via matplotlib +pytest==8.3.5 + # via -r requirements.in python-dateutil==2.9.0.post0 # via # matplotlib @@ -112,12 +149,18 @@ pyvista==0.45.0 pyyaml==6.0.2 # via trame rapidyaml==0.9.0 + # via + # lnmmeshio + # pre-commit + # trame # via fourcipp referencing==0.36.2 # via # fourcipp # jsonschema # jsonschema-specifications +requests==2.32.3 + # via pooch rich==14.0.0 # via meshio rpds-py==0.24.0 @@ -131,8 +174,12 @@ ruamel-yaml-clib==0.2.12 # via # fourcipp # ruamel-yaml +scooby==0.10.1 + # via pyvista six==1.17.0 # via python-dateutil +tqdm==4.66.5 + # via lnmmeshio trame==3.9.0 # via -r requirements.in trame-client==3.8.2 @@ -160,11 +207,18 @@ typing-extensions==4.13.2 # via # fourcipp # python-utils + # pyvista # referencing tzdata==2025.2 # via pandas +urllib3==2.4.0 + # via requests +virtualenv==20.31.2 + # via pre-commit vtk==9.4.2 - # via -r requirements.in + # via + # -r requirements.in + # pyvista wslink==2.3.3 # via # trame diff --git a/src/fourc_webviewer/fourc_webserver.py b/src/fourc_webviewer/fourc_webserver.py index 18a996d..6e0c93e 100644 --- a/src/fourc_webviewer/fourc_webserver.py +++ b/src/fourc_webviewer/fourc_webserver.py @@ -36,13 +36,17 @@ class FourCWebServer: components (e.g., state, controller) along with other relevant server-only variables.""" - def __init__(self, page_title, fourc_yaml_file): + def __init__( + self, + fourc_yaml_file, + page_title="4C Webviewer", + ): """Constructor. Args: + fourc_yaml_file (string|Path): path to the input fourc yaml file. page_title (string): page title appearing in the browser tab. - fourc_yaml_file (string|Path): path to the input fourc yaml file. """ self.server = get_server() diff --git a/src/fourc_webviewer/run_webserver.py b/src/fourc_webviewer/run_webserver.py index 334e504..79b91ad 100644 --- a/src/fourc_webviewer/run_webserver.py +++ b/src/fourc_webviewer/run_webserver.py @@ -1,9 +1,7 @@ """Utility to run the webserver on a defined port.""" from fourc_webviewer.fourc_webserver import FourCWebServer -from fourc_webviewer_default_files import ( - DEFAULT_INPUT_FILE, -) +from fourc_webviewer_default_files import DEFAULT_INPUT_FILE # specify server port for the app to run on SERVER_PORT = 12345 @@ -13,13 +11,11 @@ def run_webviewer(fourc_yaml_file=None): """Runs the webviewer by creating a dedicated webserver object, starting it and cleaning up afterwards.""" + # use the default input file if fourc_yaml_file is None: - fourc_yaml_file = str(DEFAULT_INPUT_FILE) + fourc_yaml_file = DEFAULT_INPUT_FILE - # create fourc webserver object - fourc_webserver = FourCWebServer( - page_title="4C Webviewer", fourc_yaml_file=fourc_yaml_file - ) + fourc_webserver = FourCWebServer(fourc_yaml_file) # start the server after everything is set up fourc_webserver.server.start(port=SERVER_PORT) diff --git a/test/test_pyvista.py b/test/test_pyvista.py deleted file mode 100644 index da498a7..0000000 --- a/test/test_pyvista.py +++ /dev/null @@ -1,64 +0,0 @@ -"""Tests related to pyvista plotting.""" - -import tempfile -from pathlib import Path - -import pyvista as pv -from pyvista.trame.ui import plotter_ui -from trame.app import get_server -from trame.app.file_upload import ClientFile -from trame.ui.vuetify3 import SinglePageLayout -from trame.widgets import vuetify3 - -pv.OFF_SCREEN = True - -server = get_server() -state, ctrl = server.state, server.controller - -pl = pv.Plotter() - - -@server.state.change("file_exchange") -def handle(file_exchange, **kwargs) -> None: - """Reaction to file exchange.""" - if file_exchange: - file = ClientFile(file_exchange[0]) - if file.content: - pl.remove_actor("mesh") - bytes = file.content # noqa: A001 - with tempfile.NamedTemporaryFile(suffix=file.name) as path: - with Path(path.name).open("wb") as f: - f.write(bytes) - ds = pv.read(path.name).extract_surface() - pl.add_mesh(ds, name="mesh") - pl.reset_camera() - else: - pl.clear_actors() - pl.reset_camera() - - -with SinglePageLayout(server) as layout: - with layout.toolbar: - vuetify3.VSpacer() - vuetify3.VFileInput( - multiple=False, - show_size=True, - small_chips=True, - truncate_length=25, - v_model=("file_exchange", None), - density="compact", - hide_details=True, - style="max-width: 300px;", - ) - vuetify3.VProgressLinear( - indeterminate=True, absolute=True, bottom=True, active=("trame__busy",) - ) - - with layout.content: # noqa: SIM117 - with vuetify3.VContainer( - fluid=True, classes="pa-0 fill-height", style="position: relative;" - ): - view = plotter_ui(pl) - ctrl.view_update = view.update -# Show UI -server.start() diff --git a/tests/test_fourcwebserver.py b/tests/test_fourcwebserver.py new file mode 100644 index 0000000..c900b9c --- /dev/null +++ b/tests/test_fourcwebserver.py @@ -0,0 +1,26 @@ +"""Test FourC webserver.""" + +import pytest +from fourcipp.fourc_input import FourCInput + +from fourc_webviewer.fourc_webserver import FourCWebServer +from fourc_webviewer_default_files import DEFAULT_INPUT_FILE + + +@pytest.fixture(name="fourc_webserver") +def fixture_fourc_webserver(): + """FourC webserver fixture.""" + return FourCWebServer(fourc_yaml_file=DEFAULT_INPUT_FILE) + + +@pytest.mark.parametrize( + "key, reference_value", + [ + ("render_count", {"change_selected_material": 0, "change_fourc_yaml_file": 0}), + ("fourc_yaml_content", FourCInput.from_4C_yaml(DEFAULT_INPUT_FILE)), + ("fourc_yaml_name", DEFAULT_INPUT_FILE.name), + ], +) +def test_webserver_server_variables(fourc_webserver, key, reference_value): + """Test if server variables are initialised correctly.""" + assert fourc_webserver._server_vars[key] == reference_value diff --git a/tests/test_pyvista.py b/tests/test_pyvista.py new file mode 100644 index 0000000..7fbe010 --- /dev/null +++ b/tests/test_pyvista.py @@ -0,0 +1,67 @@ +"""Tests related to pyvista plotting.""" + +import tempfile +from pathlib import Path + +import pytest +import pyvista as pv +from pyvista.trame.ui import plotter_ui +from trame.app import get_server +from trame.app.file_upload import ClientFile +from trame.ui.vuetify3 import SinglePageLayout +from trame.widgets import vuetify3 + + +@pytest.mark.gui +def test_pyvista_trame_popup(): + """Test pyvista trame combination.""" + pv.OFF_SCREEN = True + + server = get_server() + ctrl = server.controller + + pl = pv.Plotter() + + @server.state.change("file_exchange") + def handle(file_exchange, **kwargs) -> None: + """Reaction to file exchange.""" + if file_exchange: + file = ClientFile(file_exchange[0]) + if file.content: + pl.remove_actor("mesh") + bytes = file.content # noqa: A001 + with tempfile.NamedTemporaryFile(suffix=file.name) as path: + with Path(path.name).open("wb") as f: + f.write(bytes) + ds = pv.read(path.name).extract_surface() + pl.add_mesh(ds, name="mesh") + pl.reset_camera() + else: + pl.clear_actors() + pl.reset_camera() + + with SinglePageLayout(server) as layout: + with layout.toolbar: + vuetify3.VSpacer() + vuetify3.VFileInput( + multiple=False, + show_size=True, + small_chips=True, + truncate_length=25, + v_model=("file_exchange", None), + density="compact", + hide_details=True, + style="max-width: 300px;", + ) + vuetify3.VProgressLinear( + indeterminate=True, absolute=True, bottom=True, active=("trame__busy",) + ) + + with layout.content: # noqa: SIM117 + with vuetify3.VContainer( + fluid=True, classes="pa-0 fill-height", style="position: relative;" + ): + view = plotter_ui(pl) + ctrl.view_update = view.update + # Show UI + server.start(open_browser=False)