From 9a434264a68e0d237a3d7851daa2f64cc0502901 Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Wed, 20 May 2026 15:57:14 +0200 Subject: [PATCH 1/9] Squashed commit for release 1.0.0b0 --- .gitignore | 11 + .vscode/settings.json | 3 +- doc/images/shots/empty.png | Bin 40272 -> 33526 bytes doc/images/shots/guidata.moduletester.png | Bin 159455 -> 101155 bytes doc/index.rst | 4 +- doc/installation.rst | 17 + doc/locale/fr/LC_MESSAGES/example.po | 34 ++ doc/locale/fr/LC_MESSAGES/index.po | 86 +++ doc/locale/fr/LC_MESSAGES/installation.po | 235 ++++++++ doc/locale/fr/LC_MESSAGES/requirements.po | 189 ++++++ doc/locale/fr/LC_MESSAGES/usage.po | 30 + doc/requirements.rst | 14 +- moduletester/__init__.py | 2 +- moduletester/config.py | 368 +++++++++++- moduletester/data/icons/dock.svg | 18 + moduletester/data/icons/file-notify.png | Bin 0 -> 13344 bytes moduletester/data/icons/file-notify.svg | 9 + moduletester/data/icons/file-selected.png | Bin 0 -> 15988 bytes moduletester/data/icons/file-selected.svg | 12 + moduletester/data/icons/file.png | Bin 0 -> 14024 bytes moduletester/data/icons/file.svg | 12 + moduletester/data/icons/folder.png | Bin 0 -> 11008 bytes moduletester/data/icons/folder.svg | 4 + moduletester/data/icons/gear.png | Bin 0 -> 25498 bytes moduletester/data/icons/gear.svg | 4 + .../data/icons/green-check-square.png | Bin 0 -> 29170 bytes .../data/icons/green-check-square.svg | 7 + .../data/icons/libre-gui-action-edit.png | Bin 0 -> 327 bytes .../data/icons/libre-gui-action-edit.svg | 14 + .../data/icons/libre-gui-address-book.png | Bin 0 -> 404 bytes .../data/icons/libre-gui-address-book.svg | 6 + .../data/icons/libre-gui-export-doc.png | Bin 0 -> 353 bytes .../data/icons/libre-gui-export-doc.svg | 9 + .../data/icons/libre-gui-file-document.png | Bin 0 -> 288 bytes .../data/icons/libre-gui-file-document.svg | 6 + .../data/icons/libre-gui-folder-open.png | Bin 0 -> 265 bytes .../data/icons/libre-gui-folder-open.svg | 1 + .../icons/libre-gui-new-file-document.png | Bin 0 -> 362 bytes .../icons/libre-gui-new-file-document.svg | 13 + moduletester/data/icons/libre-gui-refresh.png | Bin 0 -> 348 bytes moduletester/data/icons/libre-gui-refresh.svg | 4 + moduletester/data/icons/libre-gui-save-as.png | Bin 0 -> 398 bytes moduletester/data/icons/libre-gui-save-as.svg | 19 + moduletester/data/icons/libre-gui-save.png | Bin 0 -> 297 bytes moduletester/data/icons/libre-gui-save.svg | 1 + .../data/icons/libre-gui-view-carousel.png | Bin 0 -> 215 bytes moduletester/data/icons/notification.png | Bin 0 -> 10946 bytes moduletester/data/icons/notification.svg | 2 + moduletester/data/icons/pause.png | Bin 0 -> 5697 bytes moduletester/data/icons/pause.svg | 4 + moduletester/data/icons/play.png | Bin 0 -> 14565 bytes moduletester/data/icons/play.svg | 4 + moduletester/data/icons/rejected.png | Bin 0 -> 32005 bytes moduletester/data/icons/rejected.svg | 12 + moduletester/data/icons/reload.png | Bin 0 -> 28256 bytes moduletester/data/icons/running.png | Bin 0 -> 18033 bytes moduletester/data/icons/running.svg | 7 + moduletester/data/icons/skip.png | Bin 0 -> 20875 bytes moduletester/data/icons/skip.svg | 12 + moduletester/data/icons/stop.png | Bin 0 -> 25885 bytes moduletester/data/icons/stop.svg | 4 + moduletester/data/icons/test-error.png | Bin 0 -> 29863 bytes moduletester/data/icons/test-error.svg | 9 + moduletester/data/icons/unknown.png | Bin 0 -> 39475 bytes moduletester/data/icons/unknown.svg | 9 + .../data/icons/yellow-check-square.png | Bin 0 -> 24760 bytes .../data/icons/yellow-check-square.svg | 12 + moduletester/exporter.py | 16 +- moduletester/gui/__init__.py | 2 +- moduletester/gui/components/body_component.py | 284 +++++++-- .../gui/components/result_information.py | 94 ++- .../gui/components/status_bar_component.py | 56 +- .../gui/components/test_information.py | 131 ++-- .../gui/components/test_list_component.py | 55 ++ .../gui/components/tool_bar_component.py | 88 ++- moduletester/gui/external/__init__.py | 0 .../gui/external/pyqtspinner/__init__.py | 1 + .../gui/external/pyqtspinner/configurator.py | 209 +++++++ .../gui/external/pyqtspinner/spinner.py | 312 ++++++++++ moduletester/gui/main.py | 35 +- moduletester/gui/widgets/abstract_widget.py | 11 + moduletester/gui/widgets/cli_widget.py | 58 +- moduletester/gui/widgets/config_editor.py | 123 ++++ moduletester/gui/widgets/dock_wrapper.py | 56 ++ moduletester/gui/widgets/dockable_widget.py | 24 + moduletester/gui/widgets/editor_widget.py | 154 +++++ moduletester/gui/widgets/result_comment.py | 120 +++- .../gui/widgets/result_error_widget.py | 15 +- .../gui/widgets/result_props_widget.py | 101 ++-- .../gui/widgets/test_description_widget.py | 61 +- moduletester/gui/widgets/test_list_widget.py | 445 ++++++++++++-- moduletester/gui/widgets/test_prop_widget.py | 99 ++-- moduletester/gui/widgets/toolbox_widget.py | 47 ++ moduletester/gui/widgets/web_engine.py | 128 ++++ moduletester/gui/window.py | 557 +++++++++++++++--- .../locale/fr/LC_MESSAGES/moduletester.po | 112 ++++ moduletester/manager.py | 253 ++------ moduletester/model.py | 518 +++++++++++++--- moduletester/module_not_found.py | 14 + moduletester/new_exporter.py | 305 ++++++++++ moduletester/python_helpers.py | 13 +- moduletester/serializer.py | 12 +- moduletester/test_exporter.py | 63 ++ pyproject.toml | 24 +- requirements.txt | 4 +- 105 files changed, 5098 insertions(+), 709 deletions(-) create mode 100644 doc/locale/fr/LC_MESSAGES/example.po create mode 100644 doc/locale/fr/LC_MESSAGES/index.po create mode 100644 doc/locale/fr/LC_MESSAGES/installation.po create mode 100644 doc/locale/fr/LC_MESSAGES/requirements.po create mode 100644 doc/locale/fr/LC_MESSAGES/usage.po create mode 100644 moduletester/data/icons/dock.svg create mode 100644 moduletester/data/icons/file-notify.png create mode 100644 moduletester/data/icons/file-notify.svg create mode 100644 moduletester/data/icons/file-selected.png create mode 100644 moduletester/data/icons/file-selected.svg create mode 100644 moduletester/data/icons/file.png create mode 100644 moduletester/data/icons/file.svg create mode 100644 moduletester/data/icons/folder.png create mode 100644 moduletester/data/icons/folder.svg create mode 100644 moduletester/data/icons/gear.png create mode 100644 moduletester/data/icons/gear.svg create mode 100644 moduletester/data/icons/green-check-square.png create mode 100644 moduletester/data/icons/green-check-square.svg create mode 100644 moduletester/data/icons/libre-gui-action-edit.png create mode 100644 moduletester/data/icons/libre-gui-action-edit.svg create mode 100644 moduletester/data/icons/libre-gui-address-book.png create mode 100644 moduletester/data/icons/libre-gui-address-book.svg create mode 100644 moduletester/data/icons/libre-gui-export-doc.png create mode 100644 moduletester/data/icons/libre-gui-export-doc.svg create mode 100644 moduletester/data/icons/libre-gui-file-document.png create mode 100644 moduletester/data/icons/libre-gui-file-document.svg create mode 100644 moduletester/data/icons/libre-gui-folder-open.png create mode 100644 moduletester/data/icons/libre-gui-folder-open.svg create mode 100644 moduletester/data/icons/libre-gui-new-file-document.png create mode 100644 moduletester/data/icons/libre-gui-new-file-document.svg create mode 100644 moduletester/data/icons/libre-gui-refresh.png create mode 100644 moduletester/data/icons/libre-gui-refresh.svg create mode 100644 moduletester/data/icons/libre-gui-save-as.png create mode 100644 moduletester/data/icons/libre-gui-save-as.svg create mode 100644 moduletester/data/icons/libre-gui-save.png create mode 100644 moduletester/data/icons/libre-gui-save.svg create mode 100644 moduletester/data/icons/libre-gui-view-carousel.png create mode 100644 moduletester/data/icons/notification.png create mode 100644 moduletester/data/icons/notification.svg create mode 100644 moduletester/data/icons/pause.png create mode 100644 moduletester/data/icons/pause.svg create mode 100644 moduletester/data/icons/play.png create mode 100644 moduletester/data/icons/play.svg create mode 100644 moduletester/data/icons/rejected.png create mode 100644 moduletester/data/icons/rejected.svg create mode 100644 moduletester/data/icons/reload.png create mode 100644 moduletester/data/icons/running.png create mode 100644 moduletester/data/icons/running.svg create mode 100644 moduletester/data/icons/skip.png create mode 100644 moduletester/data/icons/skip.svg create mode 100644 moduletester/data/icons/stop.png create mode 100644 moduletester/data/icons/stop.svg create mode 100644 moduletester/data/icons/test-error.png create mode 100644 moduletester/data/icons/test-error.svg create mode 100644 moduletester/data/icons/unknown.png create mode 100644 moduletester/data/icons/unknown.svg create mode 100644 moduletester/data/icons/yellow-check-square.png create mode 100644 moduletester/data/icons/yellow-check-square.svg create mode 100644 moduletester/gui/components/test_list_component.py create mode 100644 moduletester/gui/external/__init__.py create mode 100644 moduletester/gui/external/pyqtspinner/__init__.py create mode 100644 moduletester/gui/external/pyqtspinner/configurator.py create mode 100644 moduletester/gui/external/pyqtspinner/spinner.py create mode 100644 moduletester/gui/widgets/abstract_widget.py create mode 100644 moduletester/gui/widgets/config_editor.py create mode 100644 moduletester/gui/widgets/dock_wrapper.py create mode 100644 moduletester/gui/widgets/dockable_widget.py create mode 100644 moduletester/gui/widgets/editor_widget.py create mode 100644 moduletester/gui/widgets/toolbox_widget.py create mode 100644 moduletester/gui/widgets/web_engine.py create mode 100644 moduletester/locale/fr/LC_MESSAGES/moduletester.po create mode 100644 moduletester/module_not_found.py create mode 100644 moduletester/new_exporter.py create mode 100644 moduletester/test_exporter.py diff --git a/.gitignore b/.gitignore index 9a34176..b8eafe1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ doc/install_requires.txt doc/extras_require-dev.txt doc/extras_require-doc.txt *.bak +*.moduletester +tmp/* +rtv/* +dtv/* # Created by https://www.gitignore.io/api/python @@ -28,6 +32,7 @@ __pycache__/ # Distribution / packaging .Python env/ +.venv/ build/ _build/ develop-eggs/ @@ -79,3 +84,9 @@ cdl/data/doc/ target/ /.spyproject + +# document generation +*/rtv/* +*/dtv/* +moduletester/data/icons/*.py +*codra*.docx diff --git a/.vscode/settings.json b/.vscode/settings.json index c5797b3..824675c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,7 +20,7 @@ ], "[python]": { "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" }, "editor.defaultFormatter": "ms-python.black-formatter" }, @@ -41,4 +41,5 @@ ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, + "svg.preview.background": "editor", } \ No newline at end of file diff --git a/doc/images/shots/empty.png b/doc/images/shots/empty.png index 4899d684c6ed6c53a164e24b08986063ccd6f9b0..43c87e566ad3aa523bec0c73a2e3f138825dcf36 100644 GIT binary patch literal 33526 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 U`BrU7qK|yCgK|w>o!9v~%GMSr2 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

yM1FHKJ3x9;*h&~nd8+MGl0 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 diff --git a/doc/index.rst b/doc/index.rst index 1f3a478..60d4c61 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -16,8 +16,8 @@ ModuleTester is a spin-off of the `DataLab`_ project, mainly used to test :align: center :width: 300 px - ModuleTester is powered by `PlotPyStack `_, - the scientific Python-Qt visualization and graphical user interface stack. +ModuleTester is powered by `PlotPyStack `_, +the scientific Python-Qt visualization and graphical user interface stack. .. note:: ModuleTester was created by `Codra`_ in 2023. It is developed and maintained by ModuleTester open-source project team diff --git a/doc/installation.rst b/doc/installation.rst index 7e3403e..ad66224 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -24,6 +24,23 @@ install ModuleTester on an existing Python distribution: $ pip install --upgrade ModuleTester-1.0.0-py2.py3-none-any.whl +ModuleTester uses Pandoc and PyPandoc bindings to generate documents and display test +descriptions. You can get `Pandoc `_ from +`here `_ or by executing the following python code ( +All instructions are available on the +`PyPandoc documentation `_). + +.. code-block:: python + + pip install pypandoc + from pypandoc.pandoc_download import download_pandoc + # see the documentation how to customize the installation path + # but be aware that you then need to include it in the `PATH` + download_pandoc() + # check the install path with + print(pypandoc.get_pandoc_path()) + + Source package: ^^^^^^^^^^^^^^^ diff --git a/doc/locale/fr/LC_MESSAGES/example.po b/doc/locale/fr/LC_MESSAGES/example.po new file mode 100644 index 0000000..48b2bc6 --- /dev/null +++ b/doc/locale/fr/LC_MESSAGES/example.po @@ -0,0 +1,34 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2023, Codra - Pierre Raybaut +# This file is distributed under the same license as the ModuleTester +# package. +# FIRST AUTHOR , 2024. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: ModuleTester \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-01-24 18:08+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: fr\n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + +#: ../../example.rst:2 +msgid "Example" +msgstr "" + +#: ../../example.rst:7 +msgid "Running ModuleTester lead to an empty window" +msgstr "" + +#: ../../example.rst:12 +msgid "Using ModuleTester on `guidata` Python package" +msgstr "" + diff --git a/doc/locale/fr/LC_MESSAGES/index.po b/doc/locale/fr/LC_MESSAGES/index.po new file mode 100644 index 0000000..1902db4 --- /dev/null +++ b/doc/locale/fr/LC_MESSAGES/index.po @@ -0,0 +1,86 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2023, Codra - Pierre Raybaut +# This file is distributed under the same license as the ModuleTester +# package. +# FIRST AUTHOR , 2024. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: ModuleTester \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-02-29 16:45+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: fr\n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + +#: ../../index.rst:35 +msgid "Contents:" +msgstr "" + +#: ../../index.rst:2 +msgid "ModuleTester User Guide" +msgstr "" + +#: ../../index.rst:4 +msgid "ModuleTester is a library for executing and managing Python package tests." +msgstr "" + +#: ../../index.rst:9 +msgid "" +"ModuleTester is a spin-off of the `DataLab`_ project, mainly used to test" +" `PlotPyStack`_ libraries." +msgstr "" + +#: ../../index.rst:19 +msgid "" +"ModuleTester is powered by `PlotPyStack " +"`_, the scientific Python-Qt " +"visualization and graphical user interface stack." +msgstr "" + +#: ../../index.rst:22 +msgid "" +"ModuleTester was created by `Codra`_ in 2023. It is developed and " +"maintained by ModuleTester open-source project team with the support of " +"`Codra`_." +msgstr "" + +#: ../../index.rst:33 +msgid "External resources:" +msgstr "" + +#: ../../index.rst:30 +msgid "`GitHub`_" +msgstr "" + +#: ../../index.rst:31 +msgid "Project home page" +msgstr "" + +#: ../../index.rst:32 +msgid "`PyPI`_" +msgstr "" + +#: ../../index.rst:33 +msgid "Python Package Index" +msgstr "" + +#: ../../index.rst:44 +msgid "Copyrights and licensing" +msgstr "" + +#: ../../index.rst:46 +msgid "Copyright © 2023 `Codra`_" +msgstr "" + +#: ../../index.rst:47 +msgid "Licensed under the terms of the `BSD 3-Clause`_" +msgstr "" + diff --git a/doc/locale/fr/LC_MESSAGES/installation.po b/doc/locale/fr/LC_MESSAGES/installation.po new file mode 100644 index 0000000..8cb12aa --- /dev/null +++ b/doc/locale/fr/LC_MESSAGES/installation.po @@ -0,0 +1,235 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2023, Codra - Pierre Raybaut +# This file is distributed under the same license as the ModuleTester +# package. +# FIRST AUTHOR , 2024. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: ModuleTester \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-13 16:12+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: fr\n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + +#: ../../installation.rst:2 +msgid "Installation" +msgstr "" + +#: ../../installation.rst:5 +msgid "Dependencies" +msgstr "" + +#: ../../requirements.rst:1 +msgid "The :mod:`moduletester` package requires the following Python modules:" +msgstr "" + +#: ../../requirements.rst:7 ../../requirements.rst:44 ../../requirements.rst:66 +msgid "Name" +msgstr "" + +#: ../../requirements.rst:8 ../../requirements.rst:45 ../../requirements.rst:67 +msgid "Version" +msgstr "" + +#: ../../requirements.rst:9 ../../requirements.rst:46 ../../requirements.rst:68 +msgid "Summary" +msgstr "" + +#: ../../requirements.rst:10 +msgid "Python" +msgstr "" + +#: ../../requirements.rst:11 +msgid ">=3.8, <4" +msgstr "" + +#: ../../requirements.rst:13 +msgid "guidata" +msgstr "" + +#: ../../requirements.rst:14 +msgid ">= 3.3" +msgstr "" + +#: ../../requirements.rst:16 +msgid "QtPy" +msgstr "" + +#: ../../requirements.rst:17 +msgid ">= 1.9" +msgstr "" + +#: ../../requirements.rst:19 +msgid "click" +msgstr "" + +#: ../../requirements.rst:21 +msgid "Composable command line interface toolkit" +msgstr "" + +#: ../../requirements.rst:22 +msgid "pyqtwebengine" +msgstr "" + +#: ../../requirements.rst:24 +msgid "Python bindings for the Qt WebEngine framework" +msgstr "" + +#: ../../requirements.rst:25 +msgid "pypandoc" +msgstr "" + +#: ../../requirements.rst:27 +msgid "Thin wrapper for pandoc." +msgstr "" + +#: ../../requirements.rst:28 +msgid "jinja2" +msgstr "" + +#: ../../requirements.rst:30 +msgid "A very fast and expressive template engine." +msgstr "" + +#: ../../requirements.rst:31 +msgid "beautifulsoup4" +msgstr "" + +#: ../../requirements.rst:33 +msgid "Screen-scraping library" +msgstr "" + +#: ../../requirements.rst:34 ../../requirements.rst:69 +msgid "PyQt5" +msgstr "" + +#: ../../requirements.rst:35 +msgid ">=5.11" +msgstr "" + +#: ../../requirements.rst:36 ../../requirements.rst:71 +msgid "Python bindings for the Qt cross platform application toolkit" +msgstr "" + +#: ../../requirements.rst:38 +msgid "Optional modules for development:" +msgstr "" + +#: ../../requirements.rst:47 +msgid "black" +msgstr "" + +#: ../../requirements.rst:49 +msgid "The uncompromising code formatter." +msgstr "" + +#: ../../requirements.rst:50 +msgid "isort" +msgstr "" + +#: ../../requirements.rst:52 +msgid "A Python utility / library to sort Python imports." +msgstr "" + +#: ../../requirements.rst:53 +msgid "pylint" +msgstr "" + +#: ../../requirements.rst:55 +msgid "python code static checker" +msgstr "" + +#: ../../requirements.rst:56 +msgid "Coverage" +msgstr "" + +#: ../../requirements.rst:58 +msgid "Code coverage measurement for Python" +msgstr "" + +#: ../../requirements.rst:60 +msgid "Optional modules for building the documentation:" +msgstr "" + +#: ../../requirements.rst:72 +msgid "sphinx" +msgstr "" + +#: ../../requirements.rst:73 +msgid ">6" +msgstr "" + +#: ../../requirements.rst:74 +msgid "Python documentation generator" +msgstr "" + +#: ../../requirements.rst:75 +msgid "pydata_sphinx_theme" +msgstr "" + +#: ../../requirements.rst:77 +msgid "Bootstrap-based Sphinx theme from the PyData community" +msgstr "" + +#: ../../installation.rst:11 +msgid "Python 3.11 and PyQt5 are the reference for production release" +msgstr "" + +#: ../../installation.rst:15 +msgid "How to install" +msgstr "" + +#: ../../installation.rst:18 +msgid "Wheel package:" +msgstr "" + +#: ../../installation.rst:20 +msgid "" +"On any operating system, using pip and the Wheel package is the easiest " +"way to install ModuleTester on an existing Python distribution:" +msgstr "" + +#: ../../installation.rst:27 +msgid "" +"ModuleTester uses Pandoc and PyPandoc bindings to generate documents and " +"display test descriptions. You can get `Pandoc " +"`_ from `here " +"`_ or by executing the following " +"python code ( All instructions are available on the `PyPandoc " +"documentation `_)." +msgstr "" + +#: ../../installation.rst:46 +msgid "Source package:" +msgstr "" + +#: ../../installation.rst:48 +msgid "" +"Installing ModuleTester directly from the source package is " +"straigthforward:" +msgstr "" + +#~ msgid "pyqtspinner" +#~ msgstr "" + +#~ msgid ">= 3.1" +#~ msgstr "" + +#~ msgid "beautifulsoup4" +#~ msgstr "" + +#~ msgid "Screen-scraping library" +#~ msgstr "" + +#~ msgid "Python bindings for the Qt WebEngine library" +#~ msgstr "" + diff --git a/doc/locale/fr/LC_MESSAGES/requirements.po b/doc/locale/fr/LC_MESSAGES/requirements.po new file mode 100644 index 0000000..2ffe0ca --- /dev/null +++ b/doc/locale/fr/LC_MESSAGES/requirements.po @@ -0,0 +1,189 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2023, Codra - Pierre Raybaut +# This file is distributed under the same license as the ModuleTester +# package. +# FIRST AUTHOR , 2024. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: ModuleTester \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-03-13 16:12+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: fr\n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + +#: ../../requirements.rst:1 +msgid "The :mod:`moduletester` package requires the following Python modules:" +msgstr "" + +#: ../../requirements.rst:7 ../../requirements.rst:44 ../../requirements.rst:66 +msgid "Name" +msgstr "" + +#: ../../requirements.rst:8 ../../requirements.rst:45 ../../requirements.rst:67 +msgid "Version" +msgstr "" + +#: ../../requirements.rst:9 ../../requirements.rst:46 ../../requirements.rst:68 +msgid "Summary" +msgstr "" + +#: ../../requirements.rst:10 +msgid "Python" +msgstr "" + +#: ../../requirements.rst:11 +msgid ">=3.8, <4" +msgstr "" + +#: ../../requirements.rst:13 +msgid "guidata" +msgstr "" + +#: ../../requirements.rst:14 +msgid ">= 3.3" +msgstr "" + +#: ../../requirements.rst:16 +msgid "QtPy" +msgstr "" + +#: ../../requirements.rst:17 +msgid ">= 1.9" +msgstr "" + +#: ../../requirements.rst:19 +msgid "click" +msgstr "" + +#: ../../requirements.rst:21 +msgid "Composable command line interface toolkit" +msgstr "" + +#: ../../requirements.rst:22 +msgid "pyqtwebengine" +msgstr "" + +#: ../../requirements.rst:24 +msgid "Python bindings for the Qt WebEngine framework" +msgstr "" + +#: ../../requirements.rst:25 +msgid "pypandoc" +msgstr "" + +#: ../../requirements.rst:27 +msgid "Thin wrapper for pandoc." +msgstr "" + +#: ../../requirements.rst:28 +msgid "jinja2" +msgstr "" + +#: ../../requirements.rst:30 +msgid "A very fast and expressive template engine." +msgstr "" + +#: ../../requirements.rst:31 +msgid "beautifulsoup4" +msgstr "" + +#: ../../requirements.rst:33 +msgid "Screen-scraping library" +msgstr "" + +#: ../../requirements.rst:34 ../../requirements.rst:69 +msgid "PyQt5" +msgstr "" + +#: ../../requirements.rst:35 +msgid ">=5.11" +msgstr "" + +#: ../../requirements.rst:36 ../../requirements.rst:71 +msgid "Python bindings for the Qt cross platform application toolkit" +msgstr "" + +#: ../../requirements.rst:38 +msgid "Optional modules for development:" +msgstr "" + +#: ../../requirements.rst:47 +msgid "black" +msgstr "" + +#: ../../requirements.rst:49 +msgid "The uncompromising code formatter." +msgstr "" + +#: ../../requirements.rst:50 +msgid "isort" +msgstr "" + +#: ../../requirements.rst:52 +msgid "A Python utility / library to sort Python imports." +msgstr "" + +#: ../../requirements.rst:53 +msgid "pylint" +msgstr "" + +#: ../../requirements.rst:55 +msgid "python code static checker" +msgstr "" + +#: ../../requirements.rst:56 +msgid "Coverage" +msgstr "" + +#: ../../requirements.rst:58 +msgid "Code coverage measurement for Python" +msgstr "" + +#: ../../requirements.rst:60 +msgid "Optional modules for building the documentation:" +msgstr "" + +#: ../../requirements.rst:72 +msgid "sphinx" +msgstr "" + +#: ../../requirements.rst:73 +msgid ">6" +msgstr "" + +#: ../../requirements.rst:74 +msgid "Python documentation generator" +msgstr "" + +#: ../../requirements.rst:75 +msgid "pydata_sphinx_theme" +msgstr "" + +#: ../../requirements.rst:77 +msgid "Bootstrap-based Sphinx theme from the PyData community" +msgstr "" + +#~ msgid "pyqtspinner" +#~ msgstr "" + +#~ msgid ">= 3.1" +#~ msgstr "" + +#~ msgid "beautifulsoup4" +#~ msgstr "" + +#~ msgid "Screen-scraping library" +#~ msgstr "" + +#~ msgid "Python bindings for the Qt WebEngine library" +#~ msgstr "" + diff --git a/doc/locale/fr/LC_MESSAGES/usage.po b/doc/locale/fr/LC_MESSAGES/usage.po new file mode 100644 index 0000000..653a184 --- /dev/null +++ b/doc/locale/fr/LC_MESSAGES/usage.po @@ -0,0 +1,30 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) 2023, Codra - Pierre Raybaut +# This file is distributed under the same license as the ModuleTester +# package. +# FIRST AUTHOR , 2024. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: ModuleTester \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-01-24 18:08+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language: fr\n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.14.0\n" + +#: ../../usage.rst:2 +msgid "Usage" +msgstr "" + +#: ../../usage.rst:6 +msgid "Work in progress." +msgstr "" + diff --git a/doc/requirements.rst b/doc/requirements.rst index 80dec21..76482c9 100644 --- a/doc/requirements.rst +++ b/doc/requirements.rst @@ -11,11 +11,23 @@ The :mod:`moduletester` package requires the following Python modules: - >=3.8, <4 - * - guidata - - >= 3.1 + - >= 3.3 - * - QtPy - >= 1.9 - + * - click + - + - Composable command line interface toolkit + * - pyqtwebengine + - + - Python bindings for the Qt WebEngine framework + * - pypandoc + - + - Thin wrapper for pandoc. + * - jinja2 + - + - A very fast and expressive template engine. * - beautifulsoup4 - - Screen-scraping library diff --git a/moduletester/__init__.py b/moduletester/__init__.py index a5bd1f2..3c49739 100644 --- a/moduletester/__init__.py +++ b/moduletester/__init__.py @@ -16,7 +16,7 @@ .. _PlotPyStack: https://github.com/PlotPyStack """ -__version__ = "0.1.0" +__version__ = "1.0.0" __docurl__ = "https://moduletester.readthedocs.io/en/latest/" __homeurl__ = "https://codra-ingenierie-informatique.github.io/moduletester/" __supporturl__ = ( diff --git a/moduletester/config.py b/moduletester/config.py index db80855..7e17704 100644 --- a/moduletester/config.py +++ b/moduletester/config.py @@ -1,12 +1,378 @@ -# -*- coding: utf-8 -*- +from __future__ import annotations +import configparser +import os import os.path as osp +from dataclasses import dataclass, field, fields +from typing import Any, Collection, TypedDict from guidata import configtools +from guidata.configtools import get_translation APP_NAME = "ModuleTester" MOD_NAME = "moduletester" +MODULETESTER_CONFIG_NAME = "moduletester.ini" +MODULETESTER_CONFIG_DIR = os.path.dirname(__file__) configtools.add_image_module_path(MOD_NAME, osp.join("data", "logo")) +configtools.add_image_module_path(MOD_NAME, osp.join("data", "icons")) DATAPATH = configtools.get_module_data_path(MOD_NAME, "data") + +_ = get_translation(MOD_NAME) + + +class InvalidDataError(Exception): + """Exception raised for invalid data in configuration file. + + Args: + message: Explanation of the error + key: The key of the invalid data + value: The invalid value + """ + + def __init__(self, message: str, key: str, value: Any) -> None: + super().__init__(message) + self.key = key + self.value = value + + +class InvalidPathError(InvalidDataError): + """Exception raised for invalid path in configuration file.""" + + pass + + +class ConfigConflictError(Exception): + """Exception raised for conflicting configuration arguments. + + Args: + message: Explanation of the error + missing_args: The missing arguments + extra_args: The extra arguments + """ + + def __init__( + self, message: str, missing_args: Collection[str], extra_args: Collection[str] + ): + super().__init__(message) + self.missing_args = missing_args + self.extra_args = extra_args + + +def _validate_path(path: str, key, value) -> str: + """Validate a path. + + Args: + path: path to validate + key: key of the path + value: value of the path + + Raises: + InvalidPathError: If the path is not valid + + Returns: + returns the path if it is valid + """ + if not osp.exists(path): + raise InvalidPathError(f"Path {path} is not valid", key, value) + return path + + +def _serialize_field(value: Any) -> str: + """Serialize a field value to a string. + + Args: + value: value to serialize + + Returns: + serialized value + """ + if isinstance(value, (list, tuple)): + return ", ".join(map(_serialize_field, value)) + if isinstance(value, bool): + return str(int(value)) + return str(value) + + +def _check_section(conf_dataclass, **kwargs) -> tuple[set[str], set[str]]: + """Check if the section arguments are valid. + + Args: + conf_dataclass: the dataclass of the section + + Returns: + missing_args: the missing arguments + extra_args: the extra arguments + """ + if len(kwargs) == 0: + return set(), set() + + field_set = set(map(lambda f: f.name, fields(conf_dataclass))) + arg_set = set(kwargs.keys()) + + missing_args = field_set - arg_set + extra_args = arg_set - field_set + return missing_args, extra_args + + +def _resolve_conflicts( + conf_dataclass, + section: configparser.SectionProxy, + missing_args: Collection[str], + extra_args: Collection[str], +) -> configparser.SectionProxy: + """Try to resolve conflicting arguments in a section. + + Args: + conf_dataclass: configuration dataclass + section: Configparser section to resolve + missing_args: missing configuration fields in file + extra_args: extra configuration fields in file + + Returns: + Resolved section + """ + if len(missing_args) > 0: + for arg in missing_args: + default_value = getattr(conf_dataclass, arg) + section[arg] = _serialize_field(default_value) + + if len(extra_args) > 0: + for arg in extra_args: + del section[arg] + + return section + + +def _load_conf(config: configparser.ConfigParser, resolve=False) -> None: + """Load the configuration from a ConfigParser object. + + Args: + config: ConfigParser object + resolve: If True, tries to resolve conflicts in the configuration + """ + for section_name, section_obj in PACKAGE_CONF.items(): + section_values: configparser.SectionProxy = config.setdefault(section_name, {}) # type: ignore + missing_args, extra_args = _check_section(section_obj, **section_values) + if len(missing_args) > 0 or len(extra_args) > 0: + if resolve: + section_values = _resolve_conflicts( + section_obj, section_values, missing_args, extra_args + ) + else: + raise ConfigConflictError( + f"Conflicting arguments in section {section_name}", + missing_args, + extra_args, + ) + PACKAGE_CONF[section_name] = type(section_obj)( + **section_values + ) # reset section + + +def load_package_conf( + package_path: str, filename=MODULETESTER_CONFIG_NAME, resolve=False +) -> None: + """Tries to load the configuration from a file. + + Args: + package_path: path to the package where the configuration file should be located + filename: File name to search in package directory. Defaults to + MODULETESTER_CONFIG_NAME. + resolve: Try to resolve confilcts if some are found. Defaults to False. + """ + global MODULETESTER_CONFIG_DIR + custom_config_file = os.path.join(package_path, filename) + + config = configparser.ConfigParser() + MODULETESTER_CONFIG_DIR = os.path.abspath(package_path) + if os.path.isfile(custom_config_file): + config.read(custom_config_file) + + _load_conf(config, resolve) + + +def load_conf_from_string(conf_str: str, resolve=False) -> None: + """Load the configuration from a string. + + Args: + conf_str: Configuration string + resolve: Try to resolve conflicts if some are found. Defaults to False. + """ + config = configparser.ConfigParser() + config.read_string(conf_str) + _load_conf(config, resolve) + + +@dataclass +class GeneralConf: + """Dataclass for general moduletester parameters""" + + docstring_fmt: str = "rst" + category: str = "visible" + + +@dataclass +class ExporterConf: + """Dataclass for moduletester exporter parameters""" + + template_dir: str = os.path.join(MODULETESTER_CONFIG_DIR, "default_templates") + test_results_template_name: str = "test_results_template.j2" + test_list_template_name: str = "test_list_template.j2" + docx_reference: str = "custom-reference.docx" + odt_reference: str = "custom-reference.odt" + css_style: str = "default_style.css" + export_fmts: list[str] = field(default_factory=lambda: ["html", "docx"]) + reload_templates_on_export: bool = False + docstrings_header_shift: int = 3 + toc_depth: int = 2 + + def __post_init__(self): + self.template_dir = _validate_path( + self.get_template_dir(), key="template_dir", value=self.template_dir + ) + _ = _validate_path( + self.get_docx_ref(), key="docx_reference", value=self.docx_reference + ) + _ = _validate_path( + self.get_odt_ref(), key="odt_reference", value=self.odt_reference + ) + _ = _validate_path(self.get_css_style(), key="css_style", value=self.css_style) + + _ = _validate_path( + osp.join(self.template_dir, self.test_results_template_name), + key="test_results_template_name", + value=self.test_results_template_name, + ) + _ = _validate_path( + osp.join(self.template_dir, self.test_list_template_name), + key="dv_template_name", + value=self.test_list_template_name, + ) + self.reload_templates_on_export = bool(int(self.reload_templates_on_export)) + self.docstrings_header_shift = int(self.docstrings_header_shift) + self.toc_depth = int(self.toc_depth) + + export_fmts = self.export_fmts + if isinstance(export_fmts, str): + export_fmts = export_fmts.replace(" ", "").split(",") + + self.export_fmts = export_fmts + + def get_template_dir(self) -> str: + return os.path.join(MODULETESTER_CONFIG_DIR, self.template_dir) + + def get_docx_ref(self) -> str: + return osp.join(self.template_dir, self.docx_reference) + + def get_odt_ref(self) -> str: + return osp.join(self.template_dir, self.odt_reference) + + def get_css_style(self) -> str: + return osp.join(self.template_dir, self.css_style) + + def _to_abs_path(self, relative_path: str) -> str: + return osp.join(osp.abspath(self.template_dir), relative_path) + + +@dataclass +class GuiConf: + """Dataclass for moduletester GUI parameters""" + + test_list_visible: bool = True + test_list_pos: str = "left" + test_props_visible: bool = True + test_props_pos: str = "right" + result_tab_visible: bool = True + result_tab_pos: str = "bottom" + result_props_visible: bool = True + result_props_pos: str = "right" + cli_visible: bool = False + cli_pos: str = "bottom" + toolbox_visible: bool = False + toolbox_pos: str = "bottom" + + def __post_init__(self): + self.test_list_visible = bool(int(self.test_list_visible)) + self.test_props_visible = bool(int(self.test_props_visible)) + self.result_tab_visible = bool(int(self.result_tab_visible)) + self.result_props_visible = bool(int(self.result_props_visible)) + self.cli_visible = bool(int(self.cli_visible)) + self.toolbox_visible = bool(int(self.toolbox_visible)) + + +class ConfModel(TypedDict): + """Dict of package configuration parameters to use as a model""" + + general: GeneralConf + export: ExporterConf + gui: GuiConf + + +def new_config() -> ConfModel: + """Returns a new default config + + Returns: + dict: default configuration + """ + return { + "general": GeneralConf(), + "export": ExporterConf(), + "gui": GuiConf(), + } + + +# Default initialization +PACKAGE_CONF: ConfModel = new_config() + + +def serialize_conf_obj(conf: ConfModel) -> configparser.ConfigParser: + """Serialize a ConfModel TypedDict to a ConfigParser object. + + Args: + conf: ConfModel TypedDict to transform to ConfigParser object + + Returns: + ConfigParser object + """ + config = configparser.ConfigParser() + for section_name, section_obj in conf.items(): + config.add_section(section_name) + for key, value in section_obj.__dict__.items(): + config.set(section_name, key, _serialize_field(value)) + return config + + +def conf_obj_to_str(conf: ConfModel) -> str: + """Serialize a ConfModel TypedDict object to a string. + + Args: + conf: ConfModel TypedDict to serialize to string + + Returns: + serialized string + """ + lines = [] + for section_name, section_obj in conf.items(): + lines.append(f"[{section_name}]") + for key, value in section_obj.__dict__.items(): + if isinstance(value, (list, tuple)): + value = ", ".join(value) + if isinstance(value, bool): + value = str(int(value)) + lines.append(f"{key} = {value}") + lines.append("") + return "\n".join(lines) + + +def save_config(conf: ConfModel, filename: str) -> None: + global PACKAGE_CONF + PACKAGE_CONF.update(conf) + with open(filename, "w") as f: + serialize_conf_obj(conf).write(f) + + +def reset_config(self): + global PACKAGE_CONF + PACKAGE_CONF.update(new_config()) diff --git a/moduletester/data/icons/dock.svg b/moduletester/data/icons/dock.svg new file mode 100644 index 0000000..550fd93 --- /dev/null +++ b/moduletester/data/icons/dock.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/moduletester/data/icons/file-notify.png b/moduletester/data/icons/file-notify.png new file mode 100644 index 0000000000000000000000000000000000000000..5484a66ff26d2a8a495d5021484bbcc88c99bedc GIT binary patch literal 13344 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 + + + + + + + + \ No newline at end of file diff --git a/moduletester/data/icons/file-selected.png b/moduletester/data/icons/file-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..0b0a35506b308a644187db3bcc57bc2afeb235a8 GIT binary patch literal 15988 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 sarEp0P>Wr?*IS* literal 0 HcmV?d00001 diff --git a/moduletester/data/icons/file-selected.svg b/moduletester/data/icons/file-selected.svg new file mode 100644 index 0000000..05c9d47 --- /dev/null +++ b/moduletester/data/icons/file-selected.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/moduletester/data/icons/file.png b/moduletester/data/icons/file.png new file mode 100644 index 0000000000000000000000000000000000000000..de2172a7ce7a4fe7a9aee32157dcbf380efab3ce GIT binary patch literal 14024 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 + + + + + + + + + + + \ No newline at end of file diff --git a/moduletester/data/icons/folder.png b/moduletester/data/icons/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..614aaf402298dae05e383b5697a74d8c816f2e6d GIT binary patch literal 11008 zcmeHNc_5T)+kb{MqSQGplC{(6sA!eFu^gwI-WrrW22mvYo?)0$M=N@r6l19rnX#+v zOcBbKrpJ;7m7>R%c*tn@u1BZ$d*464@B8=r;}DJex$f(}uHSv%*ZN#AGcjH!u}%U( zkY)RS|Lrh>h$<03;)`LV+jsE^_;0cE0ps70dE)zKWoA5rC?WfQGqgCBJi-sX?`3T` zFzd5U#8BLjZuHyUB`M2lpLd2BtXA4`Mvy8l*i?Qv+G^$>@|HFU2}e#`Yv);OSY#<} zFDpneW2D}`Lyz>*OYC*NPu42&UP)`V(pnL*RC-> z__S(wYyoNh{`!B4K*irSH;{aj#a62CGf-vAkX*e|9iIt7 z3^nSj9c@4>?BKypbL*gEn`aF0DJ%!I)2NS*zAMTU+oa^lljnq4PTw1~;@{0Wkz71O z>8oRu(2wI6vG=UJF~uZt_*xuA5LT|jSzz=cCV9MD~XDf7D?D5vslFSd;=9TorOcqw@l8!J%{8umx@hG&$ zm`qv|6w3RYotE9RM@teR-w^TNk?cUp7QDf3KeA;OMWb! z%v%Y2QQBgdHg+F#K#MI0EVgrU*CvTG8B|i2zV{(vH}(-dm&g|}X`&{1A098kHc%T2 z_Z*dH=BWM7SFCy{c!Eu(MfRU4CmuR7^2`BaWKx*m zyebYCuroGVhbYJ|>RW5#K|l3|!Wpo(zH&vByDgu5EF~gaz{iq7!+4Z()o^>xM%H3RYhi<{UERNX6iWEv|=A~K+ zCxFkxzKfVFtEU6$6W6#E8{&;eqt`*M{EcoFj&?#Cp7h8Tv5^rwmffw%uljtOm@zzw z{eY6$*2LCd+@$m_*<83E*M_x+ukzT@mWo+(&VaMkuOyjw!x?Vjdz=DSFe$-D@V<;` ziY$+0X0W(qKdy^L?IV+Ho=aO4=Qx0Th|6ZBMx9FfKX_ZWkff4kEsH(M#up>xKSc_& z)3T}OA;afevquk8G&e4Cax^!XsuQx^`oD@KpKs05 zQzypM40@_qR*}yGFYQN6n%u0VEn2g3W#FV!ZZbW4BiU7S&_O!_&wXzh=18a~ zZiglU;Q8_iPG0q*ocTnVjm*l=lh{t%7_t0`r8i9ZGU%9fh@AW)C&luRppJ}k7vf2c z>LZ#xd`Keo94CRu?@J;{TYEak;i?EZfARKv z3bqVSxC`FNq{*i%!mP{Tr+_r3SxHbU zEKk;CstaCYPfB&ybJ@UiXWFaD*UO6|7TM%g=O|cJh5~7BtMZKU2gl5CB%7aUxjmTKu}-%+B2LAXWrA@`8&!`gLgaqeTcV@ay9xt!{M+SHL9nsW*9zxpy$9C{<+A0I?NE=%4T^wq?xA_gfd1yboc~x`jun3D1%+T zRW?38u=s1Sv#!b1K&yJQ{(#hULGyHW=~sohv67OkrSmTJb6ue$ej~ln>zkObON#rW z+capFm2dLjrJ%i*V_kCBV=pZZ!&*=ycC__!ud=XslCI|BwJ=Dm=rSX-6#v2K}(!;rg83tcW}Tw^J+oiip3bv^rJAKir}H zeuhv!Du5gDsRXNbOV`Pt&CPXqai869i-y(L9bL9(iLeun98>=Dos0=Q=8?8j^%op_ zgzCfL_PyA0V%>(X_B{EAsk}fak^megeaX{LhUx^;0p}wsfQHPIa`{s#-%wd^n&uZ|pHOgX;1f9#O#L<+h+R*FQf&Ee>0kLrVV3Z)tDHG-8a@1aIL*Cp+3wh=)! zsd1l42L$z)A^1>aDxJqM*5JSVkcehpxl-eB*dS(9FI_adlgCM0D=#8-vymNb(JBvS zWO<4eh3&509?$NM0YWn%Q~<)4vJ>GVYd?O*+R(8WFj)28hZ%O}Kg^^y>lc}BQZmkD z+V@(bE}U3Eh5ULo+F{wc7Pyg$vm&bK4E$PDQT|4W20Y~uZHG>+1f_tE4z4gzXDEHT zLOt*kN$O0w`c^~65%bMQqe~^*aefZHad3so#AwFhskSyl=7!`$l)LmMxZVc@vl-=n zdSQVpOk!l>hRTNq5tT{N&5HT^tgU?i;83Wg)9ysK6xYv+aQd5WftRh!1(zc%I zQdR~*06CN-_j7FS9Q)%(YG)HB3CC(dm5#R zPDBBs_MMYI-)JRhyxf225WS*9B)C zF?puGc|G@0kSp%U@@^Sl5>@28Dpl0~j-91+tW?2IAt%Z@H1b&{d8zi4U~p=N4i1$2 zf=`1=O-)R>r^EHbQGmt}xDqm7$d-1yT)ai1;i`Rdk!GwE;`!vk##(*>GYU&Z53tLx zNIm|Ir2S%KAKB(=wX8hSndEVx0gjf)js_aL`W|t#BpQPFCj*(QL#GE3u9ZA{?l`Ze z43WBRn(bi206$Ak5%IqhKrooT4E*`GuyT$gagO5bELsmNk@*MkFNO1SUS>sST5eX= z1Hlu3c5$gKo{UWxleO=yirbJ#N=zi377m%=6m*_AHE}G>YjI&QQvNG8D;XD(k4;qp zW|SZ*K7z)>9a`J3fhwwdnYqFCUIoxLri&tBqh@$Y*9|E-`;gqZk}@Gkb^#=_iGaSs248gzLGV-5(ea_$3wtuyI6G`e1BeXgz;sgPx9*0g3ssj$EMf`sj zJLWvITJE4e8pv``G(gC0QEWvS`L{}NF(#LQVzqZ+^_s80@zddr;MW@=O%jRrK{_@C%25r}K4aaAw#ma667C5SjipVwI${??aF!Ht`$;s(k zGU^6b%$@^L7J0ZXUR1KP^r6#(Im>8>{H)>9SfSD;4eh8;`Il77g5=_NS9E6gspHf>Q=$=44L#&5K(55BqpLk1HGdsB(d zx82w8H-%KE^N&c@_~ep+$F40;4w6VyFFe>}O-k)2%_NOn(c#;Y>^uc|=Z>|3tzbG- zW08bUNmv2OXQ%lbVT4}v2&1uhmPYWLUl874fYg7SH70k0$!#jVVS7gG@?;xU%#PlD?dlk3&i}+uOxdYc zcKKzg0+pp@ZmtHd2~H%B@g8h|J!_TLec!sj(r6{gzV{;vMx#LG`*~8QTk;&PPO!^0 znlD%6mxUkO#Jo~(?FL*Vzmq(e?aoNNO16Z2HiLec_eU`EhZY~Y>DztD+B!rIp(eJd zbK`Z(>SAs7=<`79q)SN&UbK=Kb!O!h6_wlX>m{5Tsm^Jo|^RR0M(XzE^MTT2r}ET8tvAof1v53l_O}v zI7u3ZBkOLQn0@S(B*8zhk8G~4)%r}8HJ_DBR8i>jrryJs8XqYj% ziv($GzLkVHgwSts^!lB%uWwu`tewBNY6XIrymYi)o;!CmFm82uP->>X#RA#8WmHpP zl`{+$EgjmlK-YeN6RECKdyyX&$W%XUfFN3{t8FM=f#c>loG zHd6cVAaS1s&7MJwOlG0Y#?dyiVv7)_*C`YHaoc*U50luLw)O4%fX>OTs=AC0yhAnT>_cc4jJSc5i?kG_ub8P;=vRB#kH(3>+Fu5qWnttB++n`z-#Kv zM%eED9fUAphNQ$4wSSQ@D(>6gtBaIY zerNNZ(CP6nlYc!?Q;IJF?N<^SR{B2$S^dCmwf{z-lXruZxJfB`K~9zHge!IPlp=Pm zQx^=2YQrB33LY|m!}`B{EOmwe;*f#Hf<_l*z&jP*Vgh{Dgw^=(j{{841>vu8jLA-n z1>$G72(3!OYAzrk%_gNH9z+_F3!)NsFK*d4shdIS+keeotp-Ns*uv(*1hsdg%albx zGazsJPC?wCZ%eIaJqI`ZP4=DOs78Bye**1PK)a>?t4ak+82{n{3;rLg{O5vod|8u4 zu%Azs{$HyAvfqjK5;0Pj8DP(RLZHNTCIE8UNo;=pT7UtuA&wiON?ZhF4RQF9OT@L-w)lVxp!b;v{={ zvuj4oWG)42jQB1i0HDD89L`p2(!FwKalwVI9U_n z!FN+*kRVS7gXyH0>SAyFc^ z)_&*%BwWbs<1UB~&-#5h3K_I}M!X$#>X2c{x>YM1^Q6+5av$FjiLv$q>6x8RpvG_+ za`8wa*=>=R2L~(N#5}0CYax~~E`!?IwNHPYoBn?;#2(uJyyW8FXfKO!v$QMAo( zgDeBwtnZe~8(r&eKr|FHNrWZ+@y28X2~H}g9ae-Z1o74HV+s(;jp`Ab#sgv^vZDe* zrCP4VSZR1V&|1*jGDIPZiC1XW(@Jn+@=)?p!QG8 z0?Qr<`(ABQ>VTZMYT=wHGrAz;`5Xc;LnV^|ne=Jf>Lp0>jc%yt`??arZKHfY;QtGuzjNL5d<*~>BTPA#ag!qS6~(5!&Uw$$ftDF8gf|Yx3%Gd zM25+|U0=oY2*&zQL>c2E@Yn07~1*6H_j-hKBtUWX@l!p&m-`Ik!}*JC12 zCMBpBEkT&~9~Kfb4SKRx)}_CDGkha;4}pIbDMe1++}xr*_37ioJ^ zLw_8YP(T`zN>fQcj z)KqxJQBMVX8tS9AsqW#y$E=5xwIf~GSn^E~T^IIVK>&dn`y@=oME<$@5X z`)qLAUD?H-U0Xf>2xFkpoCBN#xfA%?W@yzma$>#bdY(`{w&_FHL#KCQ1A0yQDt@9@ z5{aqrN_o8%^Lep*gI@{DtI840$(sZ?(r$%oI6|~J zW-Xw+0?~iUCtK7J5;gD*<1?&`Sw~TXTTqcBxD1J73oSpUbqs2k2p6MxK)uCjasL?UEiF%;^H>qH0RCooJCI;V)!jm(Vj!GocB-yUZSF2n|C8Q_Dj$^b7UvP) z{b@sgKnBvPMh@)ZK1i6#p^w2jtoN|x$Lva|dKoRe27&b5`PzuhzzH>mUU2B~pa|Z5 zFu~>Tve4yVK^)-)fMRc}%91ju5hF-UqoW3;d&C8Rwgz(D>W?!@#j^5{M9WUT!2&oT zOIr%^y3STj_gFHFc$7ON#3qx&HohA^tQ}^MFg}&xUO|RJJHkE0h{U_ojpPO77>;GwOA=)a4IDpA)!}U(VZ6cAM%rCX4K9(MHp(qzU+ZxfO+iLaeaOQ0k}R zH}V57ur%vDDA^-@NT?UDDf;uy^CG+2$(-$34!g9-2-omaA=b#Mb6R?vc{#)nkHeM| zXm{2}RJ(Vf@N$Iq;A=4t`P}T(LkxfKHc2Rpm~!TdvCnOWW(Mv3NmwbB#Z%ORUOqQs zToPXU0DBB=2ir9LSK-39ukS8J`i?>xGb8pu*S~omc90rFkh({ zz@fNYT^JjX2mSyFuC-$uTlvVzpG^;t&JGIm;6=o_K_4B-7frR}>%VtFhe@{8DoGLz z_3Z6b_eDraE{8L3!*IjLAK8}tkl-|uK(?%Q;Jd?CGOwNT!Ar1n=~2GnP0Znh>YRuN zf?Dh`v@?-?VXpWmY#BWI3XgK5t>!Crh?hQ?=NLELONma{;|pq{h2?W)f(K210N{;k zEWPs#d{8M_PEBdHro*Dzf`62lK>0j}>9V0A{y1x=XrX>Zso*{K3?re5gw6GAUX5;t z*DxxBeb+n$qcIl{fv-eeKKDu^owVpAg}tAm&~CEZ(MwCsrt>bb;WY_gg6GP~&)6*I zI`Etdy~m$}HGV(q)|$AJUrzP zr)U~Yn3th^UB8Gv2JP2#B99Bgo$+hbuzgh(Wx`G^ge&#M0Kc6xUAHhHTF##Ktf~qP zsHhV3m7Ew>UVK*deKux7J>LHnYhk^MK}*pbje2cxYCC@U1sPVEx1bejZ{vqomJCT> z39%f5*OyktNBu@Rn$zOBWy64m^q*=;7oXa~J8GVD!$iA&u4QZNxG+~3ri>-9#w3yY zAvRVR|0T?N_v$~=OYeWoJsdrscp%B!CoB|iYp$Mtk#WXv{KIsf-UT`2@2~&A2)GF6 YPdnax|Llz@g%EQ8UX$N)e|0+ZUx&i7-~a#s literal 0 HcmV?d00001 diff --git a/moduletester/data/icons/folder.svg b/moduletester/data/icons/folder.svg new file mode 100644 index 0000000..15941e5 --- /dev/null +++ b/moduletester/data/icons/folder.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/moduletester/data/icons/gear.png b/moduletester/data/icons/gear.png new file mode 100644 index 0000000000000000000000000000000000000000..9959714b7d7b32db972ccf64052ff5be6a3c654d GIT binary patch literal 25498 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 literal 0 HcmV?d00001 diff --git a/moduletester/data/icons/gear.svg b/moduletester/data/icons/gear.svg new file mode 100644 index 0000000..2d3d3b2 --- /dev/null +++ b/moduletester/data/icons/gear.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/moduletester/data/icons/green-check-square.png b/moduletester/data/icons/green-check-square.png new file mode 100644 index 0000000000000000000000000000000000000000..5a9574e042782286f94b68af287cbd22af070106 GIT binary patch literal 29170 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 literal 0 HcmV?d00001 diff --git a/moduletester/data/icons/green-check-square.svg b/moduletester/data/icons/green-check-square.svg new file mode 100644 index 0000000..3a38e08 --- /dev/null +++ b/moduletester/data/icons/green-check-square.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/moduletester/data/icons/libre-gui-action-edit.png b/moduletester/data/icons/libre-gui-action-edit.png new file mode 100644 index 0000000000000000000000000000000000000000..ecab4482ff2f05313ac1c5272f9ddb5643e2fe44 GIT binary patch literal 327 zcmV-N0l5B&P)&pU> zq97xOwgl_;iKo6hlcbS>fq_9)n2A+ef_3|ZQ@`zq)_CLfKZaem|1jKt_m5Tf$>p3! zWo1%CY5etTy< z!S`M;1M|P14BS6&Tz3!lT|-c#^q0d7{&xcySpI%z*rhOs;gm$}4VWAYE=}zJKQLIo zt7TyM`<-E@;yi|%f)Ob4I5qydzlGr$j}XIY)m04ld3`YCaccap6vJ?YAqJ;6hzeDz Z0stT$QgxDxtA_vp002ovPDHLkV1ii9le_=` literal 0 HcmV?d00001 diff --git a/moduletester/data/icons/libre-gui-action-edit.svg b/moduletester/data/icons/libre-gui-action-edit.svg new file mode 100644 index 0000000..28a9468 --- /dev/null +++ b/moduletester/data/icons/libre-gui-action-edit.svg @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/moduletester/data/icons/libre-gui-address-book.png b/moduletester/data/icons/libre-gui-address-book.png new file mode 100644 index 0000000000000000000000000000000000000000..5f9df524a8b9d5d92514db95e1601af300ab09c5 GIT binary patch literal 404 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 + + + + + + + + + + + \ No newline at end of file diff --git a/moduletester/exporter.py b/moduletester/exporter.py index 02989d8..58428c5 100644 --- a/moduletester/exporter.py +++ b/moduletester/exporter.py @@ -42,7 +42,7 @@ def export_description(self, package: Module, description: str) -> str: """ """ desc_content = description + "\n\n|\n" - file_name = f"{package.module.__name__}_dtv.rst" + file_name = f"{package.module.__name__}_test_list.rst" path = os.path.join(self.temp_path, file_name) with open(path, "w", encoding="utf-8") as tempfile: @@ -96,14 +96,14 @@ def export(self, package: Module, result: TestResult) -> str: desc = " - \n\n" title = f"\t{name:<{self.padding_name}}" - result = f"{result_name:<{self.padding_result}}" + result_str = f"{result_name:<{self.padding_result}}" - export = " ".join([title, result, desc]) + export = " ".join([title, result_str, desc]) return export def export_comment(self, package: Module, result: TestResult) -> str: """ """ - name = f"{package.module.__name__}_rtv.rst" + name = f"{package.module.__name__}_test_results.rst" path = os.path.join(self.temp_path, name) if result is not None: @@ -132,7 +132,6 @@ def export(self, rst_path: str, temp_path: str, section_callback) -> None: content = [] header = format_header(self.test_suite.package_name, "=") grouped_tests = self.test_suite.group_tests() - for group_package, test_list in grouped_tests.items(): section = section_callback(group_package, test_list, temp_path) content.append(section) @@ -145,7 +144,7 @@ def write_rst(self, rst_path: str, rst_content: str) -> None: with open(rst_path, "w", encoding="utf-8") as index_rst: index_rst.write(rst_content) - def export_section_dtv( + def export_section_test_list( self, package: str, tests: List[Test], temp_rst_path: str ) -> str: """ """ @@ -198,7 +197,7 @@ def export_tests_table( return table - def export_section_rtv( + def export_section_test_results( self, package: str, tests: List[Test], temp_path: str ) -> str: """ """ @@ -221,7 +220,8 @@ def export_results_table( table_content = "" result_exporter = TestResultExporter(temp_path, title_len, result_len) for test in tests: - table_content += result_exporter.export(test.package, test.result) + if test.result is not None: + table_content += result_exporter.export(test.package, test.result) # Building the table table_directive = ".. table::\n\t:widths: 15, 20, 45\n\n" diff --git a/moduletester/gui/__init__.py b/moduletester/gui/__init__.py index e386ba9..c961dae 100644 --- a/moduletester/gui/__init__.py +++ b/moduletester/gui/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- - +# pylint: disable=unused-import from moduletester import config # pylint: disable=unused-import diff --git a/moduletester/gui/components/body_component.py b/moduletester/gui/components/body_component.py index 4608ad8..ee2a5e7 100644 --- a/moduletester/gui/components/body_component.py +++ b/moduletester/gui/components/body_component.py @@ -9,22 +9,27 @@ from qtpy import QtCore as QC from qtpy import QtWidgets as QW -from ...model import ResultEnum, TestSuite +from moduletester.config import PACKAGE_CONF +from moduletester.gui.components.test_list_component import TestListComponent +from moduletester.gui.widgets.dockable_widget import DockableQWidget +from moduletester.gui.widgets.toolbox_widget import Toolbox + +from ...model import Test, TestSuite from ..states.runner import QSubprocess from ..states.signals import TMSignals from ..widgets.cli_widget import CLIWidget -from ..widgets.test_list_widget import TestListWidget +from ..widgets.dock_wrapper import QDockWrapper from .result_information import ResultInformation from .test_information import TestInformation -class TMWidget(QW.QWidget): +class TMWidget(DockableQWidget): def __init__( self, signals: TMSignals, test_suite: TestSuite, moduletester_path: Optional[str] = None, - parent: Optional[QW.QWidget] = None, + parent: Optional[QW.QMainWindow] = None, ) -> None: super().__init__(parent) # Fields @@ -33,92 +38,228 @@ def __init__( self.moduletester_path = moduletester_path self.signals = signals self._run_thread: Optional[QSubprocess] = None + self._running_test: Optional[Test] = None # Widgets - self.test_list = TestListWidget(self.test_suite.tests, self) - self.run_btn = QW.QPushButton(get_icon("apply.png"), "Run Script", self) + self.h_splitter = QW.QSplitter(QC.Qt.Orientation.Horizontal, self) + self.v_splitter = QW.QSplitter(QC.Qt.Orientation.Vertical, self) + + self.test_list_comp = TestListComponent(self.test_suite.tests, parent=self) + self.test_list = self.test_list_comp.test_list_widget + self.run_btn = self.test_list_comp.run_btn self.test_information = TestInformation(self.signals, self) - self.result_information = ResultInformation(self.signals, self) + self.result_information = ResultInformation(self.signals, parent=self) self.cli_group = CLIWidget(self) + self.toolbox = Toolbox(self, signals=signals, title="Toolbox") + + self.dock_widgets: list[QDockWrapper] = [] # Layouts - self.glayout = QW.QGridLayout(self) + self.glayout = QW.QHBoxLayout(self) + + self.view_menu = QW.QMenu() self.setup() + self.setup_dock_widgets() + + def update_widget( + self, test_suite: TestSuite, moduletester_path: Optional[str] = None + ): + """Update widget with new test_suite + + Args: + test_suite: The new TestSuite object to update the widget with. + moduletester_path: The new path to the moduletester file. + """ + self.test_suite = test_suite + self.origin_path = self.test_suite.package.root_path + self.moduletester_path = moduletester_path + self.test_list_comp.test_list_widget.reset_widget(self.test_suite.tests) @property def run_thread(self): return self._run_thread - def setup(self): - # Widget setup - self.set_item(False) + def get_main_window(self) -> QW.QMainWindow: + """Get the main window of the widget - # Layout setup - list_layout = QW.QVBoxLayout() - list_layout.addWidget(self.test_list) - list_layout.addWidget(self.run_btn) + Returns: + The main window of the widget. + """ + window = self.window() + assert isinstance(window, QW.QMainWindow) + return window - self.glayout.addLayout(list_layout, 0, 0, 9, 2) - self.glayout.addWidget(self.test_information, 0, 2, 4, 4) - self.glayout.addWidget(self.result_information, 4, 2, 4, 4) - self.glayout.addWidget(self.cli_group, 8, 2, 1, 4) - - for ind in range(self.glayout.columnCount()): - self.glayout.setColumnMinimumWidth(ind, 250) - for ind in range(self.glayout.rowCount()): - self.glayout.setRowMinimumHeight(ind, 85) + def setup(self) -> None: + """Setup the widget layout and event handlers.""" + self.glayout.addWidget(self.test_information) # Event Handlers self.run_btn.clicked.connect(self.run_test) + self.test_list.itemDoubleClicked.connect(self._run_on_double_click) self.test_list.currentItemChanged.connect( - lambda current, previous: self.set_item(False, current) + lambda: self.set_item(is_test_modified=False) ) - self.result_information.result_enum.currentTextChanged.connect( + self.result_information.result_enum.currentIndexChanged.connect( self.update_result ) self.test_list.menu.run_script.triggered.connect(self.run_test) self.test_list.menu.code_snippet.triggered.connect(self.pop_code_snippet) - self.test_information.table_group.table.itemChanged.connect(self.update_test) + self.test_information.table_group.dataset_gbox.SIG_APPLY_BUTTON_CLICKED.connect( + self.update_test + ) + self.signals.SIG_PROJECT_LOADED.connect( + lambda: self.test_list.reset_widget(self.test_suite.tests) + ) + self.set_item(is_test_modified=False) + + def setup_dock_widgets(self): + """Setup the dock widgets for the widget layout using the configuration and the + main window. + """ + window = self.get_main_window() + for widget, str_area, visible in ( + ( + self.cli_group, + PACKAGE_CONF["gui"].cli_pos, + PACKAGE_CONF["gui"].cli_visible, + ), + ( + self.result_information, + PACKAGE_CONF["gui"].result_tab_pos, + PACKAGE_CONF["gui"].result_tab_visible, + ), + ( + self.test_information.table_group, + PACKAGE_CONF["gui"].test_props_pos, + PACKAGE_CONF["gui"].test_props_visible, + ), + ( + self.result_information.prop_group, + PACKAGE_CONF["gui"].result_props_pos, + PACKAGE_CONF["gui"].result_props_visible, + ), + ( + self.test_list_comp, + PACKAGE_CONF["gui"].test_list_pos, + PACKAGE_CONF["gui"].test_list_visible, + ), + ( + self.toolbox, + PACKAGE_CONF["gui"].toolbox_pos, + PACKAGE_CONF["gui"].toolbox_visible, + ), + ): + dock_widget = QDockWrapper( + self, + widget, + ) + area = QDockWrapper.get_area_from_str(str_area) + self.dock_widgets.append(dock_widget) + self.view_menu.addAction(dock_widget.toggleViewAction()) + window.addDockWidget(area, dock_widget) + dock_widget.setVisible(visible) + self.view_menu.addAction(dock_widget.toggleViewAction()) + + def _run_on_double_click(self, clicked_item: QW.QTreeWidgetItem): + """Run the test when the item is double clicked. + + Args: + clicked_item: The item that was double clicked in the test list. + """ + self.test_list.setCurrentItem(clicked_item) + selected_item = self.test_list.current_item + selected_test = self.test_list.get_selected_test() + if ( + selected_item is clicked_item + and self.run_btn.isEnabled() + and selected_test is not None + and self.validate_command(selected_test) + ): + + self.set_item(is_test_modified=False) + self.run_test() + + def validate_command(self, test: Test) -> bool: + """Validate the command for the test. + + Args: + test: The test to validate the command for. + + Returns: + True if the command is valid, otherwise False. + """ + return self.test_information.validate_command(test) def set_item( self, + test: Optional[Test] = None, is_test_modified: bool = True, - current_item: Optional[QW.QTreeWidgetItem] = None, ): - self.test_list.setup_list(current_item) - - test = self.test_list.get_selected_test() - - self.test_information.set_item(test, self.origin_path) + """Set the item for the widget. + + Args: + test: The test to set the item for. + is_test_modified: Whether the test was modified. + """ + test = test or self.test_list.get_selected_test() + if test is None: + return + self.test_information.set_item(test, self.origin_path or "None") self.result_information.set_item(test) self.cli_group.set_item(test) if is_test_modified: + self.test_list.update_result(test) self.signals.SIG_PROJECT_MODIFIED.emit() - def update_result(self, result_value: str): + def update_result(self, index: int): + """Update the result for the test. + + Args: + index: The index of the result to update. Unused. + """ test = self.test_list.get_selected_test() - if test.result is not None: - test.result.result = ResultEnum(result_value) - self.test_list.setup_list(self.test_list.current_item) - self.signals.SIG_PROJECT_MODIFIED.emit() + if test is not None and test.result is not None: + new_result = self.result_information.result_enum.currentData() + test.result.result = new_result - def update_test(self, item: QW.QTreeWidgetItem, column: int): - if column == 1: - test = self.test_list.get_selected_test() + self.test_list.update_result(test) + self.signals.SIG_PROJECT_MODIFIED.emit() - self.test_information.update_command(item, test) - self.set_item(current_item=self.test_list.current_item) + def update_test(self): + """Update the test.""" + test = self.test_list.get_selected_test() + if test is None: + return + self.test_information.update_command(test) + self.test_list.update_result(test) + self.cli_group.set_item(test) def run_test(self): + """Run the test.""" if self._run_thread is None: test = self.test_list.get_selected_test() + if test is None: + return + test_name = test.package.last_name + test_item = self.test_list.current_item + + if not self.validate_command(test): + return self._run_thread = QSubprocess(self.test_suite, test_name) + self._running_test = test + self.result_information.result_enum.setEnabled(False) + self.result_information.comment_widget.readonly(False) + self.result_information.comment_widget.comment_label.clear() + + if test_item is not None: + self.test_list.start_test_spinner(test_item) + self._run_thread.run_ended.connect(self.handle_thread_end) self._run_thread.result_modified.connect(self.handle_result_modified) self._run_thread.SIG_RUN_STARTED.connect(self.signals.SIG_RUN_STARTED.emit) @@ -131,6 +272,7 @@ def run_test(self): ).exec() def stop_thread(self): + """Stop the test thread.""" if self._run_thread is not None: self._run_thread.stop(forced=True) else: @@ -139,6 +281,7 @@ def stop_thread(self): ).exec() def restart_thread(self): + """Restart the test thread.""" if self._run_thread is not None: self.stop_thread() self.run_test() @@ -149,32 +292,71 @@ def restart_thread(self): "No test currently paused or running", ).exec() + def notify_test(self, test: Optional[Test]): + """Update the test so other widgets know what to display + (e.g. notification icon in the treeview). + + Args: + test: The test to notify. + """ + if test is not None: + result = test.result + is_message_new = False + is_error_new = False + if result is not None: + is_message_new = result.output_msg not in ("", None) + is_error_new = result.error_msg not in ("", None) + + test.set_message_state(is_message_new) + test.set_error_state(is_error_new) + def handle_thread_end(self): - if self._run_thread is not None: + """Handle the end of the test thread.""" + if self._run_thread is not None and self._running_test is not None: self._run_thread.result_modified.disconnect() self._run_thread.run_ended.disconnect() self._run_thread.SIG_RUN_STARTED.disconnect() - self._run_thread = None + + self.notify_test(self._running_test) + + if self._running_test is self.test_list.get_selected_test(): + self.set_item(self._running_test, is_test_modified=True) + else: + self.test_list.update_result(self._running_test) + self.test_list.set_test_icon(self._running_test, "file-notify.svg") + + self._running_test = None self.signals.SIG_RUN_STOPPED.emit() - current_item = self.test_list.current_item - self.set_item(current_item=current_item) def handle_result_modified(self, _outs, _errs): - current_item = self.test_list.selectedItems()[0] - self.set_item(current_item=current_item) + """Handle the modification of the result. + + Args: + _outs: stdouts, unused + _errs: stderrs, unused + """ + if self._running_test is not None: + self.test_list.update_result(self._running_test) def pop_code_snippet(self): + """Show the code snippet for the test in a popup dialog.""" test = self.test_list.get_selected_test() + if test is None: + return + test_package = get_test_package(self.test_suite.package.module) code_snippet = test.get_code_snippet(test_package) editor = CodeEditor( - self, columns=100, rows=45, language="python", font=self.font() + self, + columns=100, + rows=45, + language="python", # font=self.font() ) editor.setReadOnly(True) editor.setPlainText(code_snippet) - editor.setWindowFlags(QC.Qt.Window) + editor.setWindowFlags(QC.Qt.WindowType.Window) editor.setWindowTitle(f"Code snippet - {test.package.last_name}") editor.setWindowIcon(get_icon("python.png")) editor.show() diff --git a/moduletester/gui/components/result_information.py b/moduletester/gui/components/result_information.py index e22b4c9..cb7c128 100644 --- a/moduletester/gui/components/result_information.py +++ b/moduletester/gui/components/result_information.py @@ -1,11 +1,14 @@ # pylint: disable=missing-module-docstring, missing-function-docstring # pylint: disable=missing-class-docstring -from typing import Any, Dict, Optional +from typing import Any, Callable, Dict, Optional +from guidata.qthelpers import get_icon +from qtpy import QtGui as QG from qtpy import QtWidgets as QW from moduletester.gui.states.signals import TMSignals +from moduletester.gui.widgets.dockable_widget import DockableQWidget from moduletester.gui.widgets.result_comment import TestCommentWidget from moduletester.gui.widgets.result_error_widget import ResultError from moduletester.gui.widgets.result_output_widget import ResultOutput @@ -13,25 +16,35 @@ from moduletester.model import Test -class ResultInformation(QW.QGroupBox): - def __init__(self, signals: TMSignals, parent: Optional[QW.QWidget] = None): - super().__init__("Results", parent) +class ResultInformation(DockableQWidget): + def __init__( + self, + signals: TMSignals, + title: str = "Test Result", + parent: Optional[QW.QWidget] = None, + ): + super().__init__(parent, title) self.signals = signals # Widgets self.tab_widget = QW.QTabWidget() - self.prop_group = ResultProps("Properties") + self.prop_group = ResultProps(self) + self.comment_widget = TestCommentWidget(self.signals) + self.output_widget = ResultOutput() + self.error_widget = ResultError() # Layouts - self.vlayout = QW.QHBoxLayout(self) + self.vlayout = QW.QVBoxLayout(self) + self.vlayout.addWidget(self.title_label) self.vlayout.addWidget(self.tab_widget) - self.vlayout.addWidget(self.prop_group) - # Config - self.prop_group.setFixedWidth(350) + # Additional + self._notification_icon = get_icon("notification.svg") + + self._tab_bar_connected = False @property def comment(self) -> str: @@ -46,26 +59,71 @@ def props(self) -> Dict[str, Any]: return self.prop_group.props def set_item(self, test: Test): + """Set the item to be displayed in the widget. + + Args: + test: The test to be displayed. + """ self.prop_group.set_item(test) self.set_tabs(test) + def _reset_tab_icon(self, index: int): + """Reset the icon of the tab at the given index. + + Args: + index: The index of the tab to reset. + """ + if index in (1, 2) and self.tab_widget.tabIcon(index) is not None: + self.tab_widget.setTabIcon(index, QG.QIcon()) + + def _remove_tab_notif(self, test: Test) -> Callable[[int], None]: + """Create a callback to remove the notification icon from the tab. + + Args: + test: The test to remove the notification from. + + Returns: + The callback function that encapsulate the given test. + """ + + def callback(index: int): + self.tab_widget.setTabIcon(index, QG.QIcon()) + if index == 1: + test.set_message_state(False) + elif index == 2: + test.set_error_state(False) + + if not (test.is_message_new() or test.is_error_new()): + self.tab_widget.currentChanged.disconnect(callback) + + return callback + def set_tabs(self, test: Test): - self.comment_widget = TestCommentWidget(self.signals) + """Set the tabs of the widget. + + Args: + test: The test to display in the tabs. + """ self.comment_widget.set_item(test) current_tab_ind = self.tab_widget.currentIndex() - output_widget = ResultOutput() - output_widget.set_item(test) + self.output_widget.set_item(test) - error_widget = ResultError() - error_widget.set_item(test) + self.error_widget.set_item(test) - for _index in range(self.tab_widget.count()): - self.tab_widget.removeTab(0) + self.tab_widget.clear() self.tab_widget.insertTab(0, self.comment_widget, "Comment") - self.tab_widget.insertTab(1, output_widget, "Output message") - self.tab_widget.insertTab(2, error_widget, "Error message") + self.tab_widget.insertTab(1, self.output_widget, "Output message") + self.tab_widget.insertTab(2, self.error_widget, "Error message") + + if test.is_message_new(): + self.tab_widget.setTabIcon(1, self._notification_icon) + + if test.is_error_new(): + self.tab_widget.setTabIcon(2, self._notification_icon) + + self.tab_widget.currentChanged.connect(self._remove_tab_notif(test)) self.tab_widget.setCurrentIndex(current_tab_ind) diff --git a/moduletester/gui/components/status_bar_component.py b/moduletester/gui/components/status_bar_component.py index 99f2cf9..eded308 100644 --- a/moduletester/gui/components/status_bar_component.py +++ b/moduletester/gui/components/status_bar_component.py @@ -1,8 +1,11 @@ # pylint: disable=missing-module-docstring, missing-class-docstring # pylint: disable=missing-function-docstring - +from qtpy import QtCore as QC +from qtpy import QtGui as QG from qtpy import QtWidgets as QW +from moduletester.gui.external.pyqtspinner import WaitingSpinner + class TMStatusBar(QW.QStatusBar): def __init__(self, parent: QW.QWidget = None): @@ -10,14 +13,42 @@ def __init__(self, parent: QW.QWidget = None): self.state_label = QW.QLabel() self.path_label = QW.QLabel() + self.export_label = QW.QLabel() + + self.export_widget = QW.QWidget() + export_layout = QW.QHBoxLayout() + export_layout.setAlignment(QC.Qt.AlignmentFlag.AlignHCenter) + export_layout.setContentsMargins(0, 0, 0, 0) + self.export_spinner = WaitingSpinner( + self.export_widget, + False, + radius=4, + roundness=0, + lines=25, + line_length=4, + line_width=2, + fade=100, + speed=3.1415 / 4, + color=QG.QColor("#0671D5"), + ) + + export_layout.addWidget(self.export_label) + export_layout.addWidget(self.export_spinner) + self.export_widget.setLayout(export_layout) if parent is not None: self.setFont(parent.font()) - self.insertWidget(0, self.state_label) - self.insertWidget(1, self.path_label) + self.addWidget(self.state_label) + self.addWidget(self.path_label) + self.addWidget(self.export_widget) def set_state_label(self, state_name: str): + """Set the state label text and visibility. + + Args: + state_name: The state name to set. + """ if state_name != "": self.state_label.setVisible(True) self.state_label.setText(state_name) @@ -25,8 +56,27 @@ def set_state_label(self, state_name: str): self.state_label.setVisible(False) def set_path_label(self, path: str): + """Set the path label text and visibility. + + Args: + path: The path to set. + """ if path and path != "": self.path_label.setVisible(True) self.path_label.setText(path) else: self.path_label.setVisible(False) + + def set_export_label(self, export: str): + """Set the export label text and spinner visibility. + + Args: + export: The export label text to set (path and formats). + """ + if export and export != "": + self.export_label.setText(export) + self.export_spinner.start() + self.export_widget.setVisible(True) + else: + self.export_widget.setVisible(False) + self.export_spinner.stop() diff --git a/moduletester/gui/components/test_information.py b/moduletester/gui/components/test_information.py index 212a478..679122c 100644 --- a/moduletester/gui/components/test_information.py +++ b/moduletester/gui/components/test_information.py @@ -7,14 +7,19 @@ from qtpy import QtWidgets as QW from moduletester.gui.states.signals import TMSignals +from moduletester.gui.widgets.dockable_widget import DockableQWidget from moduletester.gui.widgets.tab_image_widget import TabImageWidget from moduletester.gui.widgets.test_description_widget import TestDescriptionWidget from moduletester.gui.widgets.test_prop_widget import TestProps from moduletester.model import Test -class TestInformation(QW.QGroupBox): - def __init__(self, signals: TMSignals, parent: Optional[QW.QWidget] = None): +class TestInformation(QW.QWidget): + def __init__( + self, + signals: TMSignals, + parent: QW.QWidget, + ): super().__init__(parent) self.props = { "name": "", @@ -26,86 +31,102 @@ def __init__(self, signals: TMSignals, parent: Optional[QW.QWidget] = None): self.test = None self.signals = signals # Widgets - self.tab_widget = QW.QTabWidget() + self.tab_widget = QW.QTabWidget(parent=self) + self.description_tab = TestDescriptionWidget(self) self.table_group = TestProps() - self.description_tab = TestDescriptionWidget(self.signals) # Layouts - self.hlayout = QW.QHBoxLayout(self) + self.vlayout = QW.QVBoxLayout(self) + self.vlayout.addWidget(self.tab_widget) - self.hlayout.addWidget(self.tab_widget) - self.hlayout.addWidget(self.table_group) - - self.table_group.setFixedWidth(350) self.table_group.setup() - @property - def description(self) -> str: - return self.description_tab.desc_label.toPlainText() - def set_item(self, test: Test, origin_path: str): - self.setTitle(test.package.full_name) - text = self.description + """Set the item to be displayed in the description and properties widgets. - current_tab_ind = self.tab_widget.currentIndex() + Args: + test: The test to be displayed. + origin_path: _description_ + """ - self.hlayout.removeWidget(self.tab_widget) + current_tab_ind = self.tab_widget.currentIndex() - self.description_tab = TestDescriptionWidget(self.signals) self.description_tab.set_item(test) - if not self.has_test_changed(test): - self.description_tab.desc_label.setText(text) - - self.tab_widget = TabImageWidget(origin_path) - self.tab_widget.create_tab(test) - self.tab_widget.insertTab(0, self.description_tab, "Test description") - self.tab_widget.setCurrentIndex(0) - self.tab_widget.menu.open_image.triggered.connect( # type: ignore + new_tab_widget = TabImageWidget(origin_path) + new_tab_widget.create_tab(test) + new_tab_widget.insertTab(0, self.description_tab, test.package.last_name) + new_tab_widget.menu.open_image.triggered.connect( # type: ignore self.open_image ) + self.vlayout.removeWidget(self.tab_widget) - self.hlayout.insertWidget(0, self.tab_widget) + self.vlayout.insertWidget(0, new_tab_widget) - self.tab_widget.setCurrentIndex(current_tab_ind) + new_tab_widget.setCurrentIndex(current_tab_ind) self.table_group.set_props(test) + self.tab_widget = new_tab_widget + def has_test_changed(self, test: Test): + """Check if the current test has changed. + + Args: + test: The test to check. + + Returns: + True if the test has changed, False otherwise. + """ if test.package.last_name == self.props["name"]: return False return True def open_image(self): + """Open the image in the current tab if the tab is a TabImageWidget.""" + if not isinstance(self.tab_widget, TabImageWidget): + return tab_index = self.tab_widget.currentIndex() - 1 # Compensate for test desc image = self.tab_widget.images[tab_index] QG.QDesktopServices.openUrl(QC.QUrl.fromLocalFile(image)) - def update_command(self, item: QW.QTreeWidgetItem, test: Test): - if item.text(0) == "args": - test.command_args = item.text(1) - elif item.text(0) == "timeout": - try: - if item.text(1) != "0": - test.command_timeout = int(item.text(1)) - else: - test.command_timeout = 86400 - except ValueError: - item.setText(1, str(test.command_timeout)) - - if item.text(0) in ( - "timeout", - "category", - "save_path", - "pattern", - ) and item.text(1) not in ("", "0"): - if item.text(0) in test.run_opts: - opt_index = test.run_opts.index(item.text(0)) - test.run_opts[opt_index + 1] = item.text(1) - else: - test.run_opts.extend([item.text(0), item.text(1)]) - elif item.text(0) in ("timeout", "category", "save_path", "pattern"): - if item.text(0) in test.run_opts: - opt_index = test.run_opts.index(item.text(0)) + def update_command(self, test: Test): + """Update the test command arguments of the given test. + + Args: + test: The test to update. + """ + info_dataset = self.table_group.dataset_gbox.dataset + test.command_args = info_dataset.args # type: ignore + test.command_timeout = info_dataset.timeout # type: ignore + + for s in test.run_opts: + value = self.table_group.props.get(s, None) + is_zero_value = value in ("", "0", 0) + + if not is_zero_value and value in test.run_opts: + opt_index = test.run_opts.index(s) + test.run_opts[opt_index + 1] = value + elif not is_zero_value: + test.run_opts.extend((s, value)) + elif is_zero_value and value in test.run_opts: + opt_index = test.run_opts.index(s) test.run_opts.remove(test.run_opts[opt_index + 1]) - test.run_opts.remove(item.text(0)) + test.run_opts.remove(s) + + self.validate_command(test) + + def validate_command(self, test: Test) -> bool: + """Validate the command line arguments of the given test. If the command line + arguments are invalid, a message box will be shown to the user.""" + try: + test.build_command() + return True + except ValueError as e: + QW.QMessageBox( + QW.QMessageBox.NoIcon, + "Command Error", + "The following error occured while parsing the command " + f"line arguments:\n\n\t{str(e)}\n\nPlease check the command line arguments.", + ).exec() + return False diff --git a/moduletester/gui/components/test_list_component.py b/moduletester/gui/components/test_list_component.py new file mode 100644 index 0000000..5e06dca --- /dev/null +++ b/moduletester/gui/components/test_list_component.py @@ -0,0 +1,55 @@ +from __future__ import annotations + +from typing import Optional + +import qtpy.QtCore as QC +import qtpy.QtWidgets as QW +from guidata.configtools import get_icon + +from moduletester.gui.widgets import test_list_widget +from moduletester.gui.widgets.dockable_widget import DockableQWidget +from moduletester.model import Test + + +class TestListComponent(DockableQWidget): + """Wrapper for the TestListWidget and the additional butons and search bar. + + Args: + tests: List of Test objects. Defaults to None. + title: Title of the widget (and dock widget). Defaults to "Tests". + parent: Parent widget. Defaults to None. + """ + + def __init__( + self, + tests: Optional[list[Test]] = None, + title: str = "Tests", + parent: Optional[QW.QWidget] = None, + ) -> None: + super().__init__(parent, title) + + self.test_list_widget = test_list_widget.TestListWidget(tests, self) + self.list_layout = QW.QVBoxLayout() + self.collapse_all_btn = QW.QPushButton("Collapse all", self) + self.expand_all_btn = QW.QPushButton("Expand all", self) + self.search_bar = QW.QLineEdit(self) + self.search_bar.setPlaceholderText("Search test...") + self.run_btn = QW.QPushButton(get_icon("apply.png"), "Run Script", self) + + self.setup() + + def setup(self): + """Setup the layout and signal connections for the widget.""" + self.list_layout = QW.QGridLayout() + top_controls_layout = QW.QHBoxLayout() + top_controls_layout.addWidget(self.collapse_all_btn) + top_controls_layout.addWidget(self.expand_all_btn) + top_controls_layout.addWidget(self.search_bar) + self.list_layout.addLayout(top_controls_layout, 0, 0, 1, 1) + self.list_layout.addWidget(self.test_list_widget, 1, 0, 7, 1) + self.list_layout.addWidget(self.run_btn, 8, 0, 1, 1) + self.setLayout(self.list_layout) + + self.collapse_all_btn.clicked.connect(self.test_list_widget.collapseAll) + self.expand_all_btn.clicked.connect(self.test_list_widget.expandAll) + self.search_bar.textChanged.connect(self.test_list_widget.filter_items) diff --git a/moduletester/gui/components/tool_bar_component.py b/moduletester/gui/components/tool_bar_component.py index 4fcc537..494551a 100644 --- a/moduletester/gui/components/tool_bar_component.py +++ b/moduletester/gui/components/tool_bar_component.py @@ -6,45 +6,63 @@ from guidata.configtools import get_icon # type: ignore from qtpy import QtCore as QC from qtpy import QtWidgets as QW +from qtpy.QtWidgets import QAction -CTRL = QC.Qt.CTRL -SHIFT = QC.Qt.SHIFT +CTRL = QC.Qt.Modifier.CTRL +SHIFT = QC.Qt.Modifier.SHIFT class TestManagerToolbar(QW.QToolBar): def __init__(self, parent: Optional[QW.QWidget] = None): + """Top toolbar. + + Args: + parent: Parent widget. Defaults to None. + """ super().__init__(parent) # Fields - self.file_actions: List[QW.QAction] = [] - self.test_actions: List[QW.QAction] = [] + self.file_actions: List[QAction] = [] + self.test_actions: List[QAction] = [] # File Actions - self.save_action = QW.QAction(get_icon("filesave.png"), "Save") - self.save_as_action = QW.QAction(get_icon("filesaveas.png"), "Save As") - self.open_action = QW.QAction(get_icon("fileopen.png"), "Open") - self.new_file_action = QW.QAction(get_icon("filenew.png"), "New") + self.save_action = QAction(get_icon("libre-gui-save.svg"), "Save") + self.save_as_action = QAction(get_icon("libre-gui-save-as.svg"), "Save As") + self.open_action = QAction(get_icon("libre-gui-folder-open.svg"), "Open") + self.update_action = QAction(get_icon("libre-gui-refresh.svg"), "Reload tests") + self.new_file_action = QAction( + get_icon("libre-gui-new-file-document.svg"), "New" + ) # Expt action self.export_menu = QW.QMenu() - self.export_action = QW.QAction("Export") - self.export_dtv_action = QW.QAction("Export dtv") - self.export_rtv_action = QW.QAction("Export rtv") + self.export_action = QAction("Export all documents") + self.export_test_list_action = QAction("Export Test List Document") + self.export_test_results_action = QAction("Export Test Results Document") self.export_tool_btn = QW.QToolButton() # Test Actions - self.run_action = QW.QAction("Run") - self.stop_action = QW.QAction("Stop") - self.restart_action = QW.QAction("Restart") + self.run_action = QAction("Run") + self.stop_action = QAction("Stop") + self.restart_action = QAction("Restart") + + # Other actions + self.view_menu = QW.QMenu("View") + self.view_tool_btn = QW.QToolButton() + self.view_tool_btn.setIcon(get_icon("dock.svg")) + self.view_tool_btn.setDisabled(True) + self.setContextMenuPolicy(QC.Qt.ContextMenuPolicy.PreventContextMenu) # Setup self.setup() def setup(self): + """Setup the toolbar menus and actions.""" self.setup_export() # Actions self.file_actions = [ self.new_file_action, self.open_action, + self.update_action, self.save_action, self.save_as_action, ] @@ -63,30 +81,48 @@ def setup(self): self.addWidget(self.export_tool_btn) self.addSeparator() self.addActions(self.test_actions) + self.addSeparator() + self.addWidget(self.view_tool_btn) + + def setup_view(self, view_menu: QW.QMenu) -> None: + """Setup the view (docks) menu for the toolbar. + + Args: + view_menu: The view menu to be added to the toolbar. + """ + self.view_menu = view_menu + self.view_tool_btn.setMenu(self.view_menu) + self.view_tool_btn.setPopupMode(QW.QToolButton.InstantPopup) def setup_export(self): + """Setup the export menu for the toolbar.""" self.export_menu.addAction(self.export_action) self.export_menu.addSeparator() - self.export_menu.addActions([self.export_dtv_action, self.export_rtv_action]) + self.export_menu.addActions( + [self.export_test_list_action, self.export_test_results_action] + ) self.export_tool_btn.setMenu(self.export_menu) self.export_tool_btn.setPopupMode(QW.QToolButton.InstantPopup) - self.export_tool_btn.setIcon(get_icon("edit.png")) + self.export_tool_btn.setIcon(get_icon("libre-gui-export-doc.svg")) def setup_shortcuts(self): - self.new_file_action.setShortcut(CTRL + QC.Qt.Key_N) - self.save_action.setShortcut(CTRL + QC.Qt.Key_S) - self.open_action.setShortcut(CTRL + QC.Qt.Key_O) - self.save_as_action.setShortcut(CTRL + SHIFT + QC.Qt.Key_S) + """Setup the shortcuts for the toolbar actions.""" + self.new_file_action.setShortcut(CTRL + QC.Qt.Key.Key_N) + self.save_action.setShortcut(CTRL + QC.Qt.Key.Key_S) + self.open_action.setShortcut(CTRL + QC.Qt.Key.Key_O) + self.update_action.setShortcut(CTRL + QC.Qt.Key.Key_R) + self.save_as_action.setShortcut(CTRL + SHIFT + QC.Qt.Key.Key_S) - self.export_action.setShortcut(CTRL + QC.Qt.Key_E) - self.export_dtv_action.setShortcut(CTRL + QC.Qt.Key_D) - self.export_rtv_action.setShortcut(CTRL + QC.Qt.Key_R) + self.export_action.setShortcut(CTRL + QC.Qt.Key.Key_E) + self.export_test_list_action.setShortcut(CTRL + QC.Qt.Key.Key_D) + self.export_test_results_action.setShortcut(CTRL + QC.Qt.Key.Key_R) - self.run_action.setShortcut(QC.Qt.Key_F5) - self.stop_action.setShortcut(SHIFT + QC.Qt.Key_F5) - self.restart_action.setShortcut(CTRL + SHIFT + QC.Qt.Key_F5) + self.run_action.setShortcut(QC.Qt.Key.Key_F5) + self.stop_action.setShortcut(SHIFT + QC.Qt.Key.Key_F5) + self.restart_action.setShortcut(CTRL + SHIFT + QC.Qt.Key.Key_F5) def setup_tooltips(self): + """Setup the tooltips for the toolbar actions.""" for action in [*self.file_actions, *self.test_actions]: tooltip = f"{action.text()} ({action.shortcut().toString()})" action.setToolTip(tooltip) diff --git a/moduletester/gui/external/__init__.py b/moduletester/gui/external/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/moduletester/gui/external/pyqtspinner/__init__.py b/moduletester/gui/external/pyqtspinner/__init__.py new file mode 100644 index 0000000..b938dfc --- /dev/null +++ b/moduletester/gui/external/pyqtspinner/__init__.py @@ -0,0 +1 @@ +from .spinner import WaitingSpinner # noqa: F401 diff --git a/moduletester/gui/external/pyqtspinner/configurator.py b/moduletester/gui/external/pyqtspinner/configurator.py new file mode 100644 index 0000000..ac5e6e6 --- /dev/null +++ b/moduletester/gui/external/pyqtspinner/configurator.py @@ -0,0 +1,209 @@ +# noqa +import math +import sys +from random import random + +from qtpy.QtCore import Qt, Slot +from qtpy.QtWidgets import ( + QApplication, + QColorDialog, + QDoubleSpinBox, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QMessageBox, + QPushButton, + QSpinBox, + QWidget, +) + +from .spinner import WaitingSpinner + + +# pylint: disable=too-many-instance-attributes,too-many-statements +class SpinnerConfigurator(QWidget): + sb_roundness = None + sb_opacity = None + sb_fadeperc = None + sb_lines = None + sb_line_length = None + sb_line_width = None + sb_inner_radius = None + sb_rev_s = None + + btn_start = None + btn_stop = None + btn_pick_color = None + + spinner = None + + def __init__(self) -> None: + super().__init__() + self.init_ui() + + def init_ui(self) -> None: + """Initialize ui.""" + grid = QGridLayout() + groupbox1 = QGroupBox() + groupbox1_layout = QHBoxLayout() + groupbox2 = QGroupBox() + groupbox2_layout = QGridLayout() + button_hbox = QHBoxLayout() + self.setLayout(grid) + self.setWindowTitle("QtWaitingSpinner Configurator") + self.setWindowFlags(Qt.Dialog) + + # SPINNER + self.spinner = WaitingSpinner(self) + + # Spinboxes + self.sb_roundness = QDoubleSpinBox() + self.sb_opacity = QDoubleSpinBox() + self.sb_fadeperc = QDoubleSpinBox() + self.sb_lines = QSpinBox() + self.sb_line_length = QSpinBox() + self.sb_line_width = QSpinBox() + self.sb_inner_radius = QSpinBox() + self.sb_rev_s = QDoubleSpinBox() + + # set spinbox default values + self.sb_roundness.setValue(100) + self.sb_roundness.setRange(0, 9999) + self.sb_opacity.setValue(math.pi) + self.sb_opacity.setRange(0, 9999) + self.sb_fadeperc.setValue(80) + self.sb_fadeperc.setRange(0, 9999) + self.sb_lines.setValue(20) + self.sb_lines.setRange(1, 9999) + self.sb_line_length.setValue(10) + self.sb_line_length.setRange(0, 9999) + self.sb_line_width.setValue(2) + self.sb_line_width.setRange(0, 9999) + self.sb_inner_radius.setValue(10) + self.sb_inner_radius.setRange(0, 9999) + self.sb_rev_s.setValue(math.pi / 2) + self.sb_rev_s.setRange(0.1, 9999) + + # Buttons + self.btn_start = QPushButton("Start") + self.btn_stop = QPushButton("Stop") + self.btn_pick_color = QPushButton("Pick Color") + self.btn_randomize = QPushButton("Randomize") + self.btn_show_init = QPushButton("Show init args") + + # Connects + self.sb_roundness.valueChanged.connect( + lambda x: setattr(self.spinner, "roundness", x) + ) + self.sb_opacity.valueChanged.connect( + lambda x: setattr(self.spinner, "minimum_trail_opacity", x) + ) + self.sb_fadeperc.valueChanged.connect( + lambda x: setattr(self.spinner, "trail_fade_percentage", x) + ) + self.sb_lines.valueChanged.connect( + lambda x: setattr(self.spinner, "number_of_lines", x) + ) + self.sb_line_length.valueChanged.connect( + lambda x: setattr(self.spinner, "line_length", x) + ) + self.sb_line_width.valueChanged.connect( + lambda x: setattr(self.spinner, "line_width", x) + ) + self.sb_inner_radius.valueChanged.connect( + lambda x: setattr(self.spinner, "inner_radius", x) + ) + self.sb_rev_s.valueChanged.connect( + lambda x: setattr(self.spinner, "revolutions_per_second", x) + ) + + self.btn_start.clicked.connect(self.spinner.start) + self.btn_stop.clicked.connect(self.spinner.stop) + self.btn_pick_color.clicked.connect(self.show_color_picker) + self.btn_randomize.clicked.connect(self._randomize) + self.btn_show_init.clicked.connect(self.show_init_args) + + # Layout adds + groupbox1_layout.addWidget(self.spinner) + groupbox1.setLayout(groupbox1_layout) + + groupbox2_layout.addWidget(QLabel("Roundness:"), *(1, 1)) + groupbox2_layout.addWidget(self.sb_roundness, *(1, 2)) + groupbox2_layout.addWidget(QLabel("Opacity:"), *(2, 1)) + groupbox2_layout.addWidget(self.sb_opacity, *(2, 2)) + groupbox2_layout.addWidget(QLabel("Fade Perc:"), *(3, 1)) + groupbox2_layout.addWidget(self.sb_fadeperc, *(3, 2)) + groupbox2_layout.addWidget(QLabel("Lines:"), *(4, 1)) + groupbox2_layout.addWidget(self.sb_lines, *(4, 2)) + groupbox2_layout.addWidget(QLabel("Line Length:"), *(5, 1)) + groupbox2_layout.addWidget(self.sb_line_length, *(5, 2)) + groupbox2_layout.addWidget(QLabel("Line Width:"), *(6, 1)) + groupbox2_layout.addWidget(self.sb_line_width, *(6, 2)) + groupbox2_layout.addWidget(QLabel("Inner Radius:"), *(7, 1)) + groupbox2_layout.addWidget(self.sb_inner_radius, *(7, 2)) + groupbox2_layout.addWidget(QLabel("Rev/s:"), *(8, 1)) + groupbox2_layout.addWidget(self.sb_rev_s, *(8, 2)) + + groupbox2.setLayout(groupbox2_layout) + + button_hbox.addWidget(self.btn_start) + button_hbox.addWidget(self.btn_stop) + button_hbox.addWidget(self.btn_pick_color) + button_hbox.addWidget(self.btn_randomize) + button_hbox.addWidget(self.btn_show_init) + + grid.addWidget(groupbox1, *(1, 1)) + grid.addWidget(groupbox2, *(1, 2)) + grid.addLayout(button_hbox, *(2, 1)) + + self.spinner.start() + self.show() + + @Slot(name="randomize") + def _randomize(self) -> None: + self.sb_roundness.setValue(random() * 1000) + self.sb_opacity.setValue(random() * 50) + self.sb_fadeperc.setValue(random() * 100) + self.sb_lines.setValue(math.floor(random() * 150)) + self.sb_line_length.setValue(math.floor(10 + random() * 20)) + self.sb_line_width.setValue(math.floor(random() * 30)) + self.sb_inner_radius.setValue(math.floor(random() * 30)) + self.sb_rev_s.setValue(random()) + + @Slot(name="show_color_picker") + def show_color_picker(self) -> None: + """Set the color for the spinner.""" + assert self.spinner + self.spinner.color = QColorDialog.getColor() + + @Slot(name="show_init_args") + def show_init_args(self) -> None: + """Display used arguments.""" + assert self.spinner + text = ( + f"WaitingSpinner(\n parent,\n " + f"roundness={self.spinner.roundness},\n " + f"opacity={self.spinner.minimum_trail_opacity},\n " + f"fade={self.spinner.trail_fade_percentage},\n " + f"radius={self.spinner.inner_radius},\n " + f"lines={self.spinner.number_of_lines},\n " + f"line_length={self.spinner.line_length},\n " + f"line_width={self.spinner.line_width},\n " + f"speed={self.spinner.revolutions_per_second},\n " + f"color={self.spinner.color.getRgb()[:3]}\n)\n" + ) + msg_box = QMessageBox() + msg_box.setText(text) + msg_box.setWindowTitle("Text was copied to clipboard") + clipboard = QApplication.clipboard() + clipboard.clear(mode=clipboard.Clipboard) + clipboard.setText(text, mode=clipboard.Clipboard) + print(text) + msg_box.exec_() + + +def main(): + app = QApplication(sys.argv) + configurator = SpinnerConfigurator() # noqa + sys.exit(app.exec()) diff --git a/moduletester/gui/external/pyqtspinner/spinner.py b/moduletester/gui/external/pyqtspinner/spinner.py new file mode 100644 index 0000000..c67f4b8 --- /dev/null +++ b/moduletester/gui/external/pyqtspinner/spinner.py @@ -0,0 +1,312 @@ +""" +The MIT License (MIT) + +Copyright (c) 2012-2014 Alexander Turkin +Copyright (c) 2014 William Hallatt +Copyright (c) 2015 Jacob Dawid +Copyright (c) 2016 Luca Weiss +Copyright (c) 2017 fbjorn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import math + +from qtpy.QtCore import QRect, Qt, QTimer +from qtpy.QtGui import QColor, QPainter, QPaintEvent +from qtpy.QtWidgets import QWidget + + +# pylint: disable=too-many-instance-attributes,too-many-arguments +class WaitingSpinner(QWidget): + """WaitingSpinner is a highly configurable, custom spinner widget.""" + + def __init__( + self, + parent: QWidget, + center_on_parent: bool = True, + disable_parent_when_spinning: bool = False, + modality: Qt.WindowModality = Qt.NonModal, + roundness: float = 100.0, + fade: float = 80.0, + lines: int = 20, + line_length: int = 10, + line_width: int = 2, + radius: int = 10, + speed: float = math.pi / 2, + color: QColor = QColor(0, 0, 0), + ) -> None: + super().__init__(parent) + + self._center_on_parent: bool = center_on_parent + self._disable_parent_when_spinning: bool = disable_parent_when_spinning + + self._color: QColor = color + self._roundness: float = roundness + self._minimum_trail_opacity: float = math.pi + self._trail_fade_percentage: float = fade + self._revolutions_per_second: float = speed + self._number_of_lines: int = lines + self._line_length: int = line_length + self._line_width: int = line_width + self._inner_radius: int = radius + self._current_counter: int = 0 + self._is_spinning: bool = False + + self._timer: QTimer = QTimer(self) + self._timer.timeout.connect(self._rotate) + self._update_size() + self._update_timer() + self.hide() + + self.setWindowModality(modality) + self.setAttribute(Qt.WA_TranslucentBackground) + + def paintEvent(self, _: QPaintEvent) -> None: # pylint: disable=invalid-name + """Paint the WaitingSpinner.""" + self._update_position() + painter = QPainter(self) + painter.fillRect(self.rect(), Qt.transparent) + painter.setRenderHint(QPainter.Antialiasing, True) + + if self._current_counter >= self._number_of_lines: + self._current_counter = 0 + + painter.setPen(Qt.NoPen) + for i in range(self._number_of_lines): + painter.save() + painter.translate( + self._inner_radius + self._line_length, + self._inner_radius + self._line_length, + ) + rotate_angle = 360 * i / self._number_of_lines + painter.rotate(rotate_angle) + painter.translate(self._inner_radius, 0) + distance = self._line_count_distance_from_primary( + i, self._current_counter, self._number_of_lines + ) + color = self._current_line_color( + distance, + self._number_of_lines, + self._trail_fade_percentage, + self._minimum_trail_opacity, + self._color, + ) + painter.setBrush(color) + painter.drawRoundedRect( + QRect( + 0, + -self._line_width // 2, + self._line_length, + self._line_width, + ), + self._roundness, + self._roundness, + Qt.RelativeSize, + ) + painter.restore() + + def start(self) -> None: + """Show and start spinning the WaitingSpinner.""" + self._update_position() + self._is_spinning = True + self.show() + + if self.parentWidget and self._disable_parent_when_spinning: + self.parentWidget().setEnabled(False) + + if not self._timer.isActive(): + self._timer.start() + self._current_counter = 0 + + def stop(self) -> None: + """Hide and stop spinning the WaitingSpinner.""" + self._is_spinning = False + self.hide() + + if self.parentWidget() and self._disable_parent_when_spinning: + self.parentWidget().setEnabled(True) + + if self._timer.isActive(): + self._timer.stop() + self._current_counter = 0 + + @property + def color(self) -> QColor: + """Return color of WaitingSpinner.""" + return self._color + + @color.setter + def color(self, color: Qt.GlobalColor = Qt.black) -> None: + """Set color of WaitingSpinner.""" + self._color = QColor(color) + + @property + def roundness(self) -> float: + """Return roundness of WaitingSpinner.""" + return self._roundness + + @roundness.setter + def roundness(self, roundness: float) -> None: + """Set color of WaitingSpinner.""" + self._roundness = max(0.0, min(100.0, roundness)) + + @property + def minimum_trail_opacity(self) -> float: + """Return minimum trail opacity of WaitingSpinner.""" + return self._minimum_trail_opacity + + @minimum_trail_opacity.setter + def minimum_trail_opacity(self, minimum_trail_opacity: float) -> None: + """Set minimum trail opacity of WaitingSpinner.""" + self._minimum_trail_opacity = minimum_trail_opacity + + @property + def trail_fade_percentage(self) -> float: + """Return trail fade percentage of WaitingSpinner.""" + return self._trail_fade_percentage + + @trail_fade_percentage.setter + def trail_fade_percentage(self, trail: float) -> None: + """Set trail fade percentage of WaitingSpinner.""" + self._trail_fade_percentage = trail + + @property + def revolutions_per_second(self) -> float: + """Return revolutions per second of WaitingSpinner.""" + return self._revolutions_per_second + + @revolutions_per_second.setter + def revolutions_per_second(self, revolutions_per_second: float) -> None: + """Set revolutions per second of WaitingSpinner.""" + self._revolutions_per_second = revolutions_per_second + self._update_timer() + + @property + def number_of_lines(self) -> int: + """Return number of lines of WaitingSpinner.""" + return self._number_of_lines + + @number_of_lines.setter + def number_of_lines(self, lines: int) -> None: + """Set number of lines of WaitingSpinner.""" + self._number_of_lines = lines + self._current_counter = 0 + self._update_timer() + + @property + def line_length(self) -> int: + """Return line length of WaitingSpinner.""" + return self._line_length + + @line_length.setter + def line_length(self, length: int) -> None: + """Set line length of WaitingSpinner.""" + self._line_length = length + self._update_size() + + @property + def line_width(self) -> int: + """Return line width of WaitingSpinner.""" + return self._line_width + + @line_width.setter + def line_width(self, width: int) -> None: + """Set line width of WaitingSpinner.""" + self._line_width = width + self._update_size() + + @property + def inner_radius(self) -> int: + """Return inner radius size of WaitingSpinner.""" + return self._inner_radius + + @inner_radius.setter + def inner_radius(self, radius: int) -> None: + """Set inner radius size of WaitingSpinner.""" + self._inner_radius = radius + self._update_size() + + @property + def is_spinning(self) -> bool: + """Return actual spinning status of WaitingSpinner.""" + return self._is_spinning + + def _rotate(self) -> None: + """Rotate the WaitingSpinner.""" + self._current_counter += 1 + if self._current_counter >= self._number_of_lines: + self._current_counter = 0 + self.update() + + def _update_size(self) -> None: + """Update the size of the WaitingSpinner.""" + size = (self._inner_radius + self._line_length) * 2 + self.setFixedSize(size, size) + + def _update_timer(self) -> None: + """Update the spinning speed of the WaitingSpinner.""" + self._timer.setInterval( + int(1000 / (self._number_of_lines * self._revolutions_per_second)) + ) + + def _update_position(self) -> None: + """Center WaitingSpinner on parent widget.""" + if self.parentWidget() and self._center_on_parent: + self.move( + (self.parentWidget().width() - self.width()) // 2, + (self.parentWidget().height() - self.height()) // 2, + ) + + @staticmethod + def _line_count_distance_from_primary( + current: int, primary: int, total_nr_of_lines: int + ) -> int: + """Return the amount of lines from _current_counter.""" + distance = primary - current + if distance < 0: + distance += total_nr_of_lines + return distance + + @staticmethod + def _current_line_color( + count_distance: int, + total_nr_of_lines: int, + trail_fade_perc: float, + min_opacity: float, + color_input: QColor, + ) -> QColor: + """Returns the current color for the WaitingSpinner.""" + color = QColor(color_input) + if count_distance == 0: + return color + min_alpha_f = min_opacity / 100.0 + distance_threshold = int( + math.ceil((total_nr_of_lines - 1) * trail_fade_perc / 100.0) + ) + if count_distance > distance_threshold: + color.setAlphaF(min_alpha_f) + else: + alpha_diff = color.alphaF() - min_alpha_f + gradient = alpha_diff / float(distance_threshold + 1) + result_alpha = color.alphaF() - gradient * count_distance + # If alpha is out of bounds, clip it. + result_alpha = min(1.0, max(0.0, result_alpha)) + color.setAlphaF(result_alpha) + return color diff --git a/moduletester/gui/main.py b/moduletester/gui/main.py index dd83130..fefb14a 100644 --- a/moduletester/gui/main.py +++ b/moduletester/gui/main.py @@ -1,10 +1,12 @@ # pylint: disable=missing-module-docstring, missing-class-docstring # pylint: disable=missing-function-docstring +import argparse import sys from importlib import import_module from typing import Optional +from guidata.qthelpers import qt_app_context from qtpy import QtWidgets as QW from moduletester.gui.states.signals import TMSignals @@ -160,20 +162,21 @@ def run(package: Optional[str] = None, path: Optional[str] = None) -> TestManage return main +def run_gui(): + """Run the gui with arguments from command line.""" + parser = argparse.ArgumentParser("Moduletester gui launcher") + parser.add_argument( + "-p", "--package", type=str, help="Package to load", default=None + ) + parser.add_argument( + "-f", "--file", type=str, help="Moduletester file to load", default=None + ) + args = parser.parse_args() + + with qt_app_context(True): + main = run(args.package, args.file) + main.window.show() + + if __name__ == "__main__": - # import faulthandler - # faulthandler.enable() - # faulthandler.dump_traceback_later(60, False, exit=True) - - PATH = r"C:\_projets\moduletester\DataLab\run.moduletester" - PACKAGE = "cdl" - app = QW.QApplication.instance() - if not app: - app = QW.QApplication(sys.argv) - - # run(package=PACKAGE) - # run(path=PATH) - moduletester = run() - moduletester.window.show() - - app.exec_() + run_gui() diff --git a/moduletester/gui/widgets/abstract_widget.py b/moduletester/gui/widgets/abstract_widget.py new file mode 100644 index 0000000..07fb6c9 --- /dev/null +++ b/moduletester/gui/widgets/abstract_widget.py @@ -0,0 +1,11 @@ +from abc import ABC, ABCMeta + +import qtpy.QtWidgets as QW + + +class MetaAbstractQWidget(ABCMeta, type(QW.QWidget)): + """Metaclass that combines ABCMeta and QWidget metaclasses.""" + + +class AbstractQWidget(ABC, QW.QWidget, metaclass=MetaAbstractQWidget): + """Abstract QWidget.""" diff --git a/moduletester/gui/widgets/cli_widget.py b/moduletester/gui/widgets/cli_widget.py index c0baab9..22a2d95 100644 --- a/moduletester/gui/widgets/cli_widget.py +++ b/moduletester/gui/widgets/cli_widget.py @@ -4,26 +4,35 @@ from typing import Optional from click import Context +from guidata.config import CONF +from guidata.configtools import get_font from qtpy import QtCore as QC from qtpy import QtWidgets as QW +from qtpy.QtWidgets import QAction -from moduletester.manager import cli, run +from moduletester.gui.widgets.dockable_widget import DockableQWidget from moduletester.model import Test -class CLIWidget(QW.QGroupBox): - def __init__(self, parent: Optional[QW.QWidget] = None): - super().__init__(parent) - self.setTitle("Command line") +class CLIWidget(DockableQWidget): + def __init__( + self, parent: Optional[QW.QWidget] = None, title: str = "Command Line" + ) -> None: + super().__init__(parent=parent, title=title) + self.menu = CLIContextMenu() self.command_label = QW.QLabel() - self.command_label.setTextInteractionFlags(QC.Qt.TextSelectableByMouse) + self.command_label.setTextInteractionFlags( + QC.Qt.TextInteractionFlag.TextSelectableByMouse + ) self.command_label.setWordWrap(True) + font = get_font(CONF, "codeeditor") + self.command_label.setFont(font) self.vlayout = QW.QVBoxLayout(self) + self.vlayout.addWidget(self.title_label) self.vlayout.addWidget(self.command_label) - self.get_help() self.menu.copy_cli_action.triggered.connect( # type: ignore self.copy_command_line @@ -36,27 +45,40 @@ def command(self): return command_txt def set_item(self, test: Test): + """Set the current test to display its command line. + + Args: + test: Test from which to display the command line. + """ if test.command != "": self.command_label.setText(test.command) else: self.command_label.setText("No command line available") - self.command_label.setContextMenuPolicy(QC.Qt.CustomContextMenu) + self.command_label.setContextMenuPolicy( + QC.Qt.ContextMenuPolicy.CustomContextMenu + ) self.command_label.customContextMenuRequested.connect( # type: ignore self.run_menu ) def run_menu(self, point: QC.QPoint): - self.menu.exec_(self.command_label.mapToGlobal(point)) + """Run the context menu. - def get_help(self): - ctx = Context(cli) - run_help = run.get_help(ctx) - options = run_help.split("Options:\n")[-1] - options_no_help = options.split("\n --help")[0] - return options_no_help + Args: + point: Point where the context menu was requested. + """ + self.menu.exec_(self.command_label.mapToGlobal(point)) def get_run_options(self, test: Test): + """Get the run options for the current test. + + Args: + test: Test for which to get the run options. + + Returns: + str: The run options for the current test. + """ ctx = Context(cli) run_params = run.get_params(ctx) run_options = "" @@ -68,15 +90,19 @@ def get_run_options(self, test: Test): return run_options def copy_command_line(self): + """Copy the command line to the clipboard.""" app = QW.QApplication.instance() clipboard = app.clipboard() clipboard.setText(self.command) class CLIContextMenu(QW.QMenu): + """Context menu for the command line widget.""" + def __init__(self, parent: Optional[QW.QWidget] = None) -> None: super().__init__(parent) # Actions - self.copy_cli_action = QW.QAction("Copy Command Line") + self.copy_cli_action = QAction("Copy Command Line") self.addAction(self.copy_cli_action) + self.addAction(self.copy_cli_action) diff --git a/moduletester/gui/widgets/config_editor.py b/moduletester/gui/widgets/config_editor.py new file mode 100644 index 0000000..58cdd77 --- /dev/null +++ b/moduletester/gui/widgets/config_editor.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import os + +from PyQt5.QtWidgets import QWidget +from qtpy import QtWidgets as QW + +import moduletester.config as cfg +from moduletester.gui.states.signals import TMSignals +from moduletester.gui.widgets.editor_widget import Editor + + +class ConfigEditor(Editor): + def __init__( + self, + parent: QWidget | None, + signals: TMSignals, + title: str = "Configuration Editor", + ) -> None: + """Editor for the configuration file. + + Args: + parent: Parent widget. Defaults to None. + signals: Signals object that contains shared global ModuleTester signals. + title: Widget title. Defaults to "Configuration Editor". + """ + self.signals = signals + super().__init__(parent, title, language="yaml") + + def setup(self): + """Setup the widget.""" + super().setup() + self.read_config() + self.sig_save_text.connect(self.save_config) + self.signals.SIG_PROJECT_LOADED.connect(self.read_config) + self.config_path = os.path.join( + cfg.MODULETESTER_CONFIG_DIR, cfg.MODULETESTER_CONFIG_NAME + ) + + def save_config(self) -> None: + """Tries to save the configuration file. This methods can handle errors and + conflicts in the configuration file. by prompting with dialog boxes.""" + do_save = True + if os.path.exists(self.config_path): + do_save = ( + QW.QMessageBox.question( + self, + "Overwrite configuration file?", + "Do you want to overwrite the existing file?\n" + f"{self.config_path}", + QW.QMessageBox.Yes | QW.QMessageBox.No, + ) + == QW.QMessageBox.Yes + ) + if do_save: + config_content = self.get_text() + try: + cfg.load_conf_from_string(config_content) + except cfg.ConfigConflictError as e: + result = ( + QW.QMessageBox.critical( + self, + "Error in configuration file", + f"Error in configuration file: {self.config_path}\n{str(e)}" + "\nDo you want to fix the error and save the file?", + QW.QMessageBox.Apply | QW.QMessageBox.Cancel, + ) + == QW.QMessageBox.Apply + ) + if not result: + return + cfg.load_conf_from_string(config_content, resolve=True) + self.set_text(cfg.conf_obj_to_str(cfg.PACKAGE_CONF)) + + except cfg.InvalidPathError as e: + QW.QMessageBox.critical( + self, + "Configuration file contains invalid values", + f"Configuration file {self.config_path} contains " + f"invalid value:\n {e.key} = {e.value}", + QW.QMessageBox.Cancel, + ) + + except Exception as e: + QW.QMessageBox.critical( + self, + "Configuration file is invalid", + f"While Parsing the new configuration file, an error " + f"occurred:\n{str(e)}\n\n" + "The file will not be saved.", + QW.QMessageBox.Cancel, + ) + cfg.save_config(cfg.PACKAGE_CONF, self.config_path) + self.saved() + + def read_config(self) -> None: + """Read the configuration file and display it in the editor. Can save the file + if it does not exist.""" + cfg_exists = False + config_content = cfg.conf_obj_to_str(cfg.PACKAGE_CONF) + self.config_path = os.path.join( + cfg.MODULETESTER_CONFIG_DIR, cfg.MODULETESTER_CONFIG_NAME + ) + cfg_exists = os.path.exists(self.config_path) + save_new_cfg = False + if not cfg_exists: + save_new_cfg = ( + QW.QMessageBox.question( + self, + "File not found", + f"Config file not found at {self.config_path}.\n" + "Do you want to create a new config file?", + QW.QMessageBox.Yes | QW.QMessageBox.No, + ) + == QW.QMessageBox.Yes + ) + + if save_new_cfg and not cfg_exists: + self.save_config() + + self.set_text(config_content) + self.change_saved = True + self.editor_save_btn.setEnabled(False) diff --git a/moduletester/gui/widgets/dock_wrapper.py b/moduletester/gui/widgets/dock_wrapper.py new file mode 100644 index 0000000..2cd169a --- /dev/null +++ b/moduletester/gui/widgets/dock_wrapper.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from typing import Generic, Optional, TypeVar + +import qtpy.QtCore as QC +import qtpy.QtWidgets as QW + +from moduletester.gui.widgets.dockable_widget import DockableQWidget + +STR_TO_DOCK_AREA: dict[str, QC.Qt.DockWidgetArea] = { + "left": QC.Qt.DockWidgetArea.LeftDockWidgetArea, + "right": QC.Qt.DockWidgetArea.RightDockWidgetArea, + "top": QC.Qt.DockWidgetArea.TopDockWidgetArea, + "bottom": QC.Qt.DockWidgetArea.BottomDockWidgetArea, +} + +AnyDockableWidget = TypeVar("AnyDockableWidget", bound=DockableQWidget) + + +class QDockWrapper(QW.QDockWidget, Generic[AnyDockableWidget]): + def __init__( + self, + parent: Optional[QW.QWidget], + widget: AnyDockableWidget, + title: Optional[str] = None, + ) -> None: + """Wrapper for DockableQWidget to transform it into a usable QDockWidget. + + Args: + parent: Parent widget. Defaults to None. + widget: DockableQWidget to wrap into a QDockWidget. + title: Dock widget title. If None, will default to the given widget title. + Defaults to None. + """ + widget_title_label: QW.QLabel | None = getattr(widget, "title_label", None) + if widget_title_label is not None: + widget_title_label.hide() + title = title or widget_title_label.text() if widget_title_label else "" + + super().__init__(title, parent) + self.setWidget(widget) + self.setFeatures(QW.QDockWidget.DockWidgetFeature.AllDockWidgetFeatures) + # self.setFloating(False) + # self.setContextMenuPolicy(QC.Qt.ContextMenuPolicy.CustomContextMenu) + + @staticmethod + def get_area_from_str(area: str) -> QC.Qt.DockWidgetArea: + """Get the QDockWidgetArea from a string. + + Args: + area: String representation of the dock area. + + Returns: + The QDockWidgetArea corresponding to the given string. + """ + return STR_TO_DOCK_AREA.get(area, QC.Qt.DockWidgetArea.RightDockWidgetArea) diff --git a/moduletester/gui/widgets/dockable_widget.py b/moduletester/gui/widgets/dockable_widget.py new file mode 100644 index 0000000..b21f542 --- /dev/null +++ b/moduletester/gui/widgets/dockable_widget.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +from typing import Optional + +import qtpy.QtCore as QC +import qtpy.QtWidgets as QW +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QWidget + +from moduletester.gui.widgets.abstract_widget import AbstractQWidget + + +class DockableQWidget(AbstractQWidget): + def __init__(self, parent: Optional[QW.QWidget], title: str = "") -> None: + """Normal QWidget with a title and a label. This class is meant to be used as a + base class for optionnally dockable widgets by being wrapped into a + QDockWrapper object. + + Args: + parent: Parent widget. Defaults to None. + title: Widget title. Defaults to "". + """ + super().__init__(parent) + self.title = title + self.title_label = QW.QLabel(self.title) diff --git a/moduletester/gui/widgets/editor_widget.py b/moduletester/gui/widgets/editor_widget.py new file mode 100644 index 0000000..02c7e0e --- /dev/null +++ b/moduletester/gui/widgets/editor_widget.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from abc import ABC +from typing import Optional + +from guidata.qthelpers import get_icon +from guidata.widgets import codeeditor +from PyQt5.QtGui import QCloseEvent +from PyQt5.QtWidgets import QWidget +from qtpy import QtCore as QC +from qtpy import QtGui as QG +from qtpy import QtWidgets as QW + +import moduletester.config as cfg +from moduletester.gui.states.signals import TMSignals +from moduletester.gui.widgets.dockable_widget import DockableQWidget + + +class DialogEditor(codeeditor.CodeEditor): + sig_update_content = QC.Signal() # type: ignore + sig_save_key = QC.Signal() # type: ignore + + def __init__( + self, + parent: Optional[QW.QWidget] = None, + language=None, + font=None, + columns=None, + rows=None, + ): + super().__init__(parent, language, font, columns, rows) # type: ignore + self.content_is_synched = True + + def closeEvent(self, event: QCloseEvent) -> None: # type: ignore # noqa: N802 + if not self.content_is_synched: + self.sig_update_content.emit() + super().closeEvent(event) + + def keyPressEvent(self, event: QG.QKeyEvent): # type: ignore # noqa: N802 + super().keyPressEvent(event) + if ( + event.modifiers() == QC.Qt.ControlModifier + and event.key() == QC.Qt.Key.Key_S + ): + self.sig_save_key.emit() + self.content_is_synched = True + else: + self.content_is_synched = False + + +class Editor(DockableQWidget, ABC): + """Editor widget with a read-only code editor and a button to open a popup editor. + The editors are guidata.widgets.codeeditor.CodeEditor objects. + + Args: + parent: Parent widget. Defaults to None. + title: Widget title. Defaults to "Editor". + language: Language to use for the code editor (see guidata documentation). + Defaults to None. + additional_btns: Additional buttons to add to the widget. Defaults to None. + """ + + sig_save_text = QC.Signal(str) # type: ignore + + def __init__( + self, + parent: QWidget | None, + title: str = "Editor", + language: str | None = None, + additional_btns: Optional[list[QW.QPushButton]] = None, + ) -> None: + super().__init__(parent, title) + + self._vlayout = QW.QVBoxLayout() + self._hlayout = QW.QHBoxLayout() + + self.editor_edit_btn = QW.QPushButton( + get_icon("libre-gui-action-edit.svg"), "Edit" + ) + self.editor_save_btn = QW.QPushButton(get_icon("libre-gui-save.svg"), "Save") + self.readonly_editor = codeeditor.CodeEditor(self, language=language, rows=True) + self.readonly_editor.setReadOnly(True) + self.popup_editor = DialogEditor( + self, + columns=100, + rows=45, + language=language, + ) + self.popup_editor.sig_save_key.connect(self.update_content) + self.popup_editor.sig_save_key.connect(self.save_text) + self.additional_btns = additional_btns or [] + + self.change_saved = True + + self.setup() + + def set_text(self, text: str) -> None: + """Set the text of the readonly editor. + + Args: + text: Text to set. + """ + self.readonly_editor.setPlainText(text) + + def get_text(self) -> str: + """Get the text of the readonly editor.""" + return self.readonly_editor.toPlainText() + + def update_content(self) -> None: + """Update the content of the readonly editor with the content of the popup + editor.""" + self.readonly_editor.setPlainText(self.popup_editor.toPlainText()) + self.change_saved = False + self.editor_save_btn.setEnabled(True) + + def open_popup_editor(self): + """Open the popup editor with the content of the readonly editor.""" + self.popup_editor.setPlainText(self.readonly_editor.toPlainText()) + self.popup_editor.show() + + def save_text(self) -> None: + """Emit the sig_save_text signal with the content of the readonly editor.""" + self.sig_save_text.emit(self.readonly_editor.toPlainText()) + + def saved(self) -> None: + """Set the change_saved attribute to True and disable the save button.""" + self.change_saved = True + self.editor_save_btn.setEnabled(False) + + def setup(self): + """Setup the widget.""" + self.editor_save_btn.clicked.connect(self.save_text) + self.editor_save_btn.setEnabled(False) + self.editor_edit_btn.clicked.connect(self.open_popup_editor) + + self._vlayout.addWidget(self.readonly_editor) + self._hlayout.addWidget(self.editor_edit_btn) + self._hlayout.addWidget(self.editor_save_btn) + self._vlayout.addLayout(self._hlayout) + + self.popup_editor.setWindowTitle("Edit...") + self.popup_editor.setWindowIcon(get_icon("libre-gui-action-edit.svg")) + self.popup_editor.setWindowFlags(QC.Qt.WindowType.Window) + self.popup_editor.sig_update_content.connect(self.update_content) + + if len(self.additional_btns) > 0: + splitter = QW.QSplitter() + splitter.setStyleSheet("QSplitter::border {border: 1px solid #d3d3d3;}") + self._vlayout.addWidget(splitter) + + for btn in self.additional_btns: + self._vlayout.addWidget(btn) + + self.setLayout(self._vlayout) diff --git a/moduletester/gui/widgets/result_comment.py b/moduletester/gui/widgets/result_comment.py index e4269d7..a929865 100644 --- a/moduletester/gui/widgets/result_comment.py +++ b/moduletester/gui/widgets/result_comment.py @@ -14,43 +14,149 @@ from moduletester.model import Test +class _CommentTextEdit(QW.QTextEdit): + """Custom QTextEdit can emit a specific signal when the user presses Ctrl+Z and all + available undo operations have been exhausted. + + Args: + parent: Parent widget. Defaults to None. + """ + + sig_reset_comment = QC.Signal() # type: ignore + + def __init__(self, parent: Optional[QW.QWidget] = None): + super().__init__(parent) + self.allow_reset_content = False + self.undoAvailable.connect(self.set_allow_reset_content) + + def set_allow_reset_content(self, avail: bool): + """Set whether the content can be reset.""" + self.allow_reset_content = not avail + + def keyPressEvent(self, e: QG.QKeyEvent) -> None: # noqa: N802 + """Handle key press events. + + Args: + e: Key event. + """ + super().keyPressEvent(e) + if ( + self.allow_reset_content + and e.key() == QC.Qt.Key.Key_Z + and e.modifiers() == QC.Qt.ControlModifier + ): + self.sig_reset_comment.emit() + self.allow_reset_content = False + + class TestCommentWidget(QW.QWidget): + """Widget to display and edit the comment of a test result. + + Args: + signals: Signals object that contains shared global ModuleTester signals. + parent: Parent widget. Defaults to None. + """ + + SIG_EDIT_STOPPED = QC.Signal() # type: ignore + def __init__(self, signals: TMSignals, parent: Optional[QW.QWidget] = None): super().__init__(parent) + + self.cached_comments: dict[str, str] = {} + self.signals = signals # Widgets self.lbl_icon = QW.QLabel() self.lbl_icon.setFixedWidth(32) - self.comment_label = QW.QTextEdit() + self.comment_label = _CommentTextEdit() self.comment_label.setWordWrapMode(QG.QTextOption.WordWrap) self.comment_label.setFrameStyle(0) for label in (self.comment_label, self.lbl_icon): - label.setAlignment(QC.Qt.AlignTop) + label.setAlignment(QC.Qt.AlignmentFlag.AlignTop) # Event Handlers self.comment_label.textChanged.connect(self.text_changed) # type: ignore + self.comment_label.sig_reset_comment.connect(self.reset_comment) + + self.timer = QC.QTimer() + self.timer.setSingleShot(True) + self.timer.setInterval(1000) + + self.timer.timeout.connect(self.SIG_EDIT_STOPPED) + self.comment_label.textChanged.connect(self.text_changed) + self.SIG_EDIT_STOPPED.connect(self.update_cached_comment) # Layouts self.hlayout = QW.QHBoxLayout(self) self.hlayout.addWidget(self.lbl_icon) self.hlayout.addWidget(self.comment_label) + self.test: Optional[Test] = None + + def reset_comment(self): + """Reset the comment to the last saved version.""" + text = "No result yet" + if self.test is not None and self.test.result is not None: + text = self.test.result.comment + self.cached_comments.pop(self.test.package.full_name, None) + self.comment_label.setText(text) + + def readonly(self, readonly: bool): + """Set the comment label to readonly or not. + + Args: + readonly: Whether the comment label should be readonly. + """ + if readonly: + self.comment_label.setTextInteractionFlags( + QC.Qt.TextInteractionFlag.TextSelectableByMouse + ) + else: + self.comment_label.setTextInteractionFlags( + QC.Qt.TextInteractionFlag.TextEditorInteraction + ) + def set_item(self, test: Test): + """Set the test item to display the comment of.""" + # save previous changes if save timer was still running + if self.timer.isActive(): + self.timer.stop() + self.update_cached_comment() + + self.test = test + cached_comment = self.cached_comments.get(test.package.full_name, None) if test.result is not None: - text = test.result.comment - self.comment_label.setTextInteractionFlags(QC.Qt.TextEditorInteraction) + text = test.result.comment if cached_comment is None else cached_comment + self.readonly(False) + elif test.is_running(): + text = "No result yet" + self.readonly(False) else: text = "No result yet" - self.comment_label.setTextInteractionFlags(QC.Qt.TextSelectableByMouse) + self.readonly(True) self.lbl_icon.setPixmap(get_std_icon("MessageBoxInformation").pixmap(24, 24)) - self.comment_label.blockSignals(True) self.comment_label.setText(text) self.comment_label.blockSignals(False) + self.comment_label.undoAvailable.emit(False) - def text_changed(self): + def text_changed(self) -> None: + """Emit SIG_EDIT_STOPPED after a delay so some actions are not trigger at + every key stroke.""" + """Text has changed: restart the timer to emit SIG_EDIT_STOPPED after a delay""" self.signals.SIG_PROJECT_MODIFIED.emit() + if self.timer.isActive(): + self.timer.stop() + self.timer.start() + + def update_cached_comment(self): + """Update the cached comment with the current comment.""" + if self.test is None: + return + self.cached_comments[self.test.package.full_name] = ( + self.comment_label.toPlainText() + ) diff --git a/moduletester/gui/widgets/result_error_widget.py b/moduletester/gui/widgets/result_error_widget.py index 8b47f26..54f2a77 100644 --- a/moduletester/gui/widgets/result_error_widget.py +++ b/moduletester/gui/widgets/result_error_widget.py @@ -12,6 +12,12 @@ class ResultError(QW.QWidget): + """Widget to display the error message of a test result (stderr). + + Args: + parent: Parent widget. Defaults to None. + """ + def __init__(self, parent: Optional[QW.QWidget] = None): super().__init__(parent) @@ -26,13 +32,16 @@ def __init__(self, parent: Optional[QW.QWidget] = None): # Config self.label.setWordWrapMode(QG.QTextOption.WordWrap) - self.label.setTextInteractionFlags(QC.Qt.TextSelectableByMouse) - self.label.setAlignment(QC.Qt.AlignTop) + self.label.setTextInteractionFlags( + QC.Qt.TextInteractionFlag.TextSelectableByMouse + ) + self.label.setAlignment(QC.Qt.AlignmentFlag.AlignTop) self.label.setFrameStyle(0) self.icon.setFixedWidth(32) - self.icon.setAlignment(QC.Qt.AlignTop) + self.icon.setAlignment(QC.Qt.AlignmentFlag.AlignTop) def set_item(self, test: Test): + """Set the test to display the error message of.""" if test.result is None: text_level = "Information" text = "No result yet" diff --git a/moduletester/gui/widgets/result_props_widget.py b/moduletester/gui/widgets/result_props_widget.py index 54c5e2c..2f7b990 100644 --- a/moduletester/gui/widgets/result_props_widget.py +++ b/moduletester/gui/widgets/result_props_widget.py @@ -1,77 +1,108 @@ # pylint: disable=missing-module-docstring, missing-class-docstring # pylint: disable=missing-function-docstring +from __future__ import annotations from typing import Any, Dict, Optional +from guidata.configtools import get_icon +from guidata.dataset import DataSet +from guidata.dataset.dataitems import StringItem +from guidata.dataset.qtwidgets import DataSetEditGroupBox from qtpy import QtWidgets as QW +from moduletester.config import _ +from moduletester.gui.widgets.dockable_widget import DockableQWidget from moduletester.model import ResultEnum, Test -class ResultProps(QW.QGroupBox): +class _PropertiesDataSet(DataSet): + return_code = StringItem("Return code").set_prop("display", active=False) + execution_duration = StringItem("Execution duration").set_prop( + "display", active=False + ) + last_run = StringItem("Last run").set_prop("display", active=False) + status = StringItem("Status").set_prop("display", active=False) + + +class ResultProps(DockableQWidget): + """Widget to display the properties of a test result. + + Args: + parent: Parent widget. Defaults to None. + """ + def __init__(self, parent: Optional[QW.QWidget] = None): - super().__init__(parent) + super().__init__(parent, title="Test Execution") self.props: Dict[str, Any] = {} # Widgets self.result_enum = QW.QComboBox() - self.table = QW.QTreeWidget() - + # self.table = QW.QTreeWidget() + self.dataset_gbox = DataSetEditGroupBox( + "", _PropertiesDataSet, show_button=False + ) + self.dataset_gbox.updateGeometry() + self.dataset_gbox.get() # Layouts self.vlayout = QW.QVBoxLayout(self) + self.vlayout.addWidget(self.title_label) self.vlayout.addWidget(self.result_enum) - self.vlayout.addWidget(self.table) + self.vlayout.addWidget(self.dataset_gbox) + self.dataset_gbox.update() # Config - result_value = [result.value for result in ResultEnum] - self.result_enum.addItems(result_value) - - self.table.setHeaderLabels(["Property", "Value"]) - self.table.setAlternatingRowColors(True) - self.table.setIndentation(False) - self.table.setColumnWidth(0, 100) - self.table.setColumnWidth(1, 200) + for i, result in enumerate(ResultEnum): + self.result_enum.addItem(result.format(), result) # noqa: F821 + self.result_enum.setItemIcon(i, get_icon(result.icon_path)) def set_item(self, test: Test): + """Set the test to display the properties of. + + Args: + test: Test to display the properties of. + """ self.set_props(test) - result_value = "no result" + result = ResultEnum.NO_RESULT if test.result is not None: - result_value = test.result.result.value + result = test.result.result - if test.result is None: + if test.result is None or test.is_running(): self.result_enum.setEnabled(False) else: self.result_enum.setEnabled(True) self.result_enum.blockSignals(True) - self.result_enum.setCurrentText(result_value) + self.result_enum.setCurrentText(result.format()) self.result_enum.blockSignals(False) def set_props(self, test: Test): + """Set the properties displayed in the widget using the given test. + + Args: + test: Test to display the properties of. + """ if test.result is not None: - self.props = { - "return code": test.result.error_code, - "execution duration": test.result.execution_duration, - "last run": test.result.last_run, - "status": test.result.status.value, - } + self.props.update( + { + "return code": test.result.error_code, + "execution duration": test.result.execution_duration, + "last run": test.result.last_run, + "status": test.result.status.value, + } + ) if self.props["execution duration"] is not None: self.props["execution duration"] = round( self.props["execution duration"], 3 ) else: - self.props = {} - - for _ in range(self.table.topLevelItemCount()): - self.table.takeTopLevelItem(0) + self.props.clear() - for key, value in self.props.items(): - item = QW.QTreeWidgetItem((str(key), str(value))) - tooltip = f"{key}: {value}" - self.set_tool_tips(item, tooltip) - self.table.addTopLevelItem(item) + dataset = self.dataset_gbox.dataset + dataset.return_code = self.props.get("return code", "") + dataset.execution_duration = self.props.get("execution duration", "") + dataset.last_run = self.props.get("last run", "") + dataset.status = self.props.get("status", "") - def set_tool_tips(self, item: QW.QTreeWidgetItem, tooltip: str): - for col_index in range(item.columnCount()): - item.setToolTip(col_index, tooltip) + self.dataset_gbox.updateGeometry() + self.dataset_gbox.get() diff --git a/moduletester/gui/widgets/test_description_widget.py b/moduletester/gui/widgets/test_description_widget.py index 2601fa2..d6f7f9d 100644 --- a/moduletester/gui/widgets/test_description_widget.py +++ b/moduletester/gui/widgets/test_description_widget.py @@ -6,46 +6,51 @@ from typing import Optional from guidata.qthelpers import get_std_icon # type: ignore -from qtpy import QtCore as QC -from qtpy import QtGui as QG from qtpy import QtWidgets as QW +from qtpy.QtWebEngineWidgets import QWebEnginePage # type: ignore from moduletester.gui.states.signals import TMSignals +from moduletester.gui.widgets.web_engine import SimpleWebViewer from moduletester.model import Test class TestDescriptionWidget(QW.QWidget): - def __init__(self, signals: TMSignals, parent: Optional[QW.QWidget] = None): + """Widget to display the description of a test. Includes a SimpleWebViewer to + display the HTML description of the test. + + Args: + parent: Parent widget. Defaults to None. + """ + + def __init__(self, parent: Optional[QW.QWidget] = None): super().__init__(parent) self.test: Optional[Test] = None - self.signals = signals # Widgets - self.lbl_icon = QW.QLabel() - self.lbl_icon.setFixedWidth(32) - - self.desc_label = QW.QTextEdit() - self.desc_label.setWordWrapMode(QG.QTextOption.WordWrap) - self.desc_label.setFrameStyle(0) - - for label in (self.desc_label, self.lbl_icon): - label.setAlignment(QC.Qt.AlignTop) + self.web_view = SimpleWebViewer(web_actions=[QWebEnginePage.WebAction.Reload]) # type: ignore + self.web_view.pageAction(QWebEnginePage.WebAction.Reload).triggered.connect( + self.force_reload + ) # Layouts self.hlayout = QW.QHBoxLayout(self) - self.hlayout.addWidget(self.lbl_icon) - self.hlayout.addWidget(self.desc_label) - - self.desc_label.textChanged.connect(self.text_changed) # type: ignore - - def text_changed(self): - self.signals.SIG_PROJECT_MODIFIED.emit() - - def set_item(self, test: Test): + self.hlayout.addWidget(self.web_view) + + def force_reload(self): + """Force the web view to reload the content.""" + if self.test is not None: + self.set_item(self.test, use_cached=False) + + def set_item(self, test: Test, use_cached: bool = True): + """Set the test to display the description of. + Args: + test: Test to display the description of. + use_cached: Whether to use the cached HTML description or not. + Defaults to True. + """ self.test = test - text_level = "Information" if test.is_valid else "Critical" - self.lbl_icon.setPixmap(get_std_icon(f"MessageBox{text_level}").pixmap(24, 24)) - - self.desc_label.blockSignals(True) - self.desc_label.setText(test.description) - self.desc_label.blockSignals(False) + self.web_view.setHtml( + test.get_html_description( + standalone=True, embeded=True, apply_style=True, use_cached=use_cached + ) + ) diff --git a/moduletester/gui/widgets/test_list_widget.py b/moduletester/gui/widgets/test_list_widget.py index cf076de..dd077ce 100644 --- a/moduletester/gui/widgets/test_list_widget.py +++ b/moduletester/gui/widgets/test_list_widget.py @@ -2,61 +2,365 @@ # pylint: disable=missing-module-docstring # guitest: skip +from __future__ import annotations from datetime import datetime -from typing import List, Optional +from typing import Any, Iterable, List, Optional +from guidata.configtools import get_icon from qtpy import QtCore as QC from qtpy import QtGui as QG from qtpy import QtWidgets as QW -from moduletester.model import Test +from moduletester.config import _ +from moduletester.gui.external.pyqtspinner import WaitingSpinner +from moduletester.model import ( + ModuleErrorType, + ModuleInternalErrorType, + ModuleNotFoundType, + Test, +) GREY = "#F5F5F5" +# BLUE = "#0060C6" +BLUE = "#007BFF" +# TREE_STYLESHEET = """ +# QTreeWidget::item:selected { +# background-color: #007BFF; +# } +# """ +TREE_STYLESHEET = "" class TestListWidget(QW.QTreeWidget): - def __init__(self, tests: List[Test], parent: Optional[QW.QWidget] = None): + """Widget to display the list of tests and some result/status information. + + Args: + tests: List of tests to display. Defaults to None. + parent: Parent widget. Defaults to None. + """ + + def __init__( + self, tests: Optional[List[Test]], parent: Optional[QW.QWidget] = None + ) -> None: super().__init__(parent) # Fields - self.tests = tests + tests = tests or [] + self.tests: dict[str, Test] = {} + self._setup_tests(tests) + self.test_items: dict[str, QW.QTreeWidgetItem] = {} self.menu = TestContextMenu(self) # Config self.setHeaderLabels(["Name", "Status", "Last run"]) self.setSelectionMode(QW.QAbstractItemView.SingleSelection) - self.setup_list(None) - self.setCurrentItem(self.topLevelItem(0)) + self.bold_font = QG.QFont() + self.bold_font.setBold(True) + self.bold_font.setUnderline(True) + # self.bold_font.setCapitalization(QG.QFont.Capitalization.Capitalize) + # self.setCurrentItem(self.topLevelItem(0)) self.installEventFilter(self) self.setAlternatingRowColors(True) - self.setIndentation(False) + self.setIndentation(15) + self.setColumnWidth(0, 250) + self.setStyleSheet(TREE_STYLESHEET) + self.currentItemChanged.connect(self._select_item) + + self.build_tree() + + def _setup_tests(self, tests: List[Test]) -> None: + """Set the tests list. + + Args: + tests: List of tests. + """ + self.tests.clear() + self.tests.update( + {t.package.name_from_source.rsplit(".", 1)[-1]: t for t in tests} + ) + + def reset_widget(self, tests: List[Test]) -> None: + """Reset the widget with the given tests. Avoids creating a new widget. + Args: + tests: List of tests to display. + """ + self._setup_tests(tests) + self.build_tree() + + def start_test_spinner(self, test_item: QW.QTreeWidgetItem) -> None: + """Start a spinner for the given test item. + + Args: + test_item: Test item to start the spinner for. + """ + spinner = WaitingSpinner( + self, + False, + radius=5, + roundness=0, + lines=50, + line_length=5, + line_width=1, + fade=100, + speed=3.1415 / 4, + color=QG.QColor("#0671D5"), + ) + + spinner.start() + test_item.setText(1, "") + test_item.setIcon(1, QG.QIcon()) + self.setItemWidget(test_item, 1, spinner) + + def stop_test_spinner(self, test_item: QW.QTreeWidgetItem) -> None: + """Stop the spinner for the given test item. + + Args: + test_item: Test item to stop the spinner for. + """ + spinner = self.itemWidget(test_item, 1) + self.removeItemWidget(test_item, 1) + if isinstance(spinner, WaitingSpinner): + spinner.stop() + del spinner @property - def current_item(self): - return self.selectedItems()[0] + def current_item(self) -> QW.QTreeWidgetItem | None: + return self.currentItem() + + def _select_item( + self, + current_item: QW.QTreeWidgetItem, + previous_item: Optional[QW.QTreeWidgetItem], + ) -> None: + """Select the given item and reset the icon of the previous item. Implements + some logic to avoid selecting non-selectable items (items can correspond to + tests or directory in the package). + + Args: + current_item: Current item. + previous_item: Previous item. + """ + if QC.Qt.ItemFlag.ItemIsSelectable & current_item.flags(): # type: ignore + current_item.setSelected(True) + self._reset_item_icon(current_item, None) + if previous_item is not None: + self._reset_item_icon(previous_item, None) + + elif ( + previous_item is not None + and QC.Qt.ItemFlag.ItemIsSelectable & previous_item.flags() # type: ignore + ): + self.setCurrentItem(previous_item) + + def _reset_item_icon( + self, item: Optional[QW.QTreeWidgetItem], test: Optional[Test] + ) -> None: + """Reset the icon of the given item. If no item is given, the current item is + used. One or both of the arguments must be set. + + Args: + item: Item to reset the icon of. + test: Test to reset the icon of. + """ + if isinstance(item, QW.QTreeWidgetItem) and isinstance(test, Test): + pass + elif item is None and test is None: + item, test = self.current_item, self.get_selected_test() + elif item is None and isinstance(test, Test): + item = self.test_items.get(test.package.last_name, None) + elif test is None and isinstance(item, QW.QTreeWidgetItem): + test = self.tests.get(item.text(0), None) + + if item is None or test is None or item.text(0) != test.package.last_name: + return + + if isinstance(test.package.module, ModuleErrorType) or ( + test.is_error_new() or test.is_message_new() + ): + new_icon = get_icon("file-notify.svg") + elif item is self.current_item and item.childCount() == 0: + new_icon = get_icon("file-selected.svg") + elif item.childCount() == 0: + new_icon = get_icon("file.svg") + else: + new_icon = get_icon("libre-gui-folder-open.svg") + + item.setIcon(0, new_icon) + + def set_test_icon(self, test: Test, icon: str | QG.QIcon) -> None: + """Set the icon of the given test. + + Args: + test: Test to set the icon of. + icon: icon to set. + """ + item = self.test_items.get(test.package.last_name, None) + if item is None: + return + icon = icon if isinstance(icon, QG.QIcon) else get_icon(icon) + item.setIcon(0, icon) + + def select_first_test_item_from( + self, root: Optional[QW.QTreeWidgetItem] = None + ) -> Optional[QW.QTreeWidgetItem]: + """Select the first test item from the given root. If no root is given, the + invisible root item is used. + + Args: + root: Root QTreeWidgetItem. Defaults to None. + + Returns: + The first test item from the given root. + """ + item = self.invisibleRootItem() if root is None else root + if item is None: + return None + + while item.childCount() > 0: # type: ignore + item = item.child(0) # type: ignore + + if item is not None: + self.setCurrentItem(item) + + return item + + def _set_result_icon(self, item: QW.QTreeWidgetItem, test: Test) -> None: + """Set the result icon of the given item depending on the test result. + + Args: + item: Item to set the result icon of. + test: Test to get the result from. + """ + if test.result is None: + item.setIcon(1, get_icon("unknown.svg")) + else: + item.setIcon(1, get_icon(test.result.result.icon_path)) + + def update_result(self, test: Optional[Test]) -> None: + """Update the result of the given test. If no test is given, the selected test + is used. + + Args: + test: Test to update the result of. Defaults to None. + """ + test = test or self.get_selected_test() + if test is None: + return + + item = self.test_items.get(test.package.last_name, None) - def setup_list(self, current_item: Optional[QW.QTreeWidgetItem]): - current_row = self.get_current_row(current_item) - self.blockSignals(True) + if item is None: + return + + test_columns = self.get_cols(test) + + if not test.is_running(): + self.stop_test_spinner(item) + item.setText(1, test_columns[1]) + self._set_result_icon(item, test) + self._reset_item_icon(item, test) + item.setText(2, test_columns[2]) + + def build_tree(self) -> None: + """Build the tree widget with the tests. The tree is built using the tests + list. + """ self.clear_widget() + tree_dict = self._build_submodules_tree_dict(self.tests.values()) + self._build_tree_widget(self, tree_dict) + + for test in self.tests.values(): + self.update_result(test) - for test in self.tests: - item = QW.QTreeWidgetItem(self.get_cols(test)) - for col in range(item.columnCount()): - item.setSizeHint(col, QC.QSize(1, 25)) - self.addTopLevelItem(item) - item_ind = self.topLevelItemCount() + self.select_first_test_item_from() - test = self.tests[item_ind - 1] + def _build_submodules_tree_dict(self, tests: Iterable[Test]) -> dict[str, Any]: + """Build a dictionary with the tests and their submodules. The dictionary is + used to build the tree widget. This is done by calling + self._build_submodules_tree_dict_step recursively. - if not test.is_valid: - item.setForeground(0, QG.QColor("#FF3333")) + Args: + tests: Tests to build the dictionary from. - self.setCurrentItem(self.topLevelItem(current_row)) + Returns: + Dictionary with the tests and their submodules. + """ + rows = [self.get_cols(test) for test in tests] + # T = Union[str, dict[str, "T"]] + grouped_elements: dict[str, Any] = {} + for row in rows: + self._build_submodules_tree_dict_step(grouped_elements, row) + return grouped_elements - self.blockSignals(False) + def _build_submodules_tree_dict_step( + self, current_dict: dict[str, Any], submodule_row: list[str] + ) -> None: + """Helper function to build the dictionary with the tests and their submodules. + + Args: + current_dict: Current dictionary. + submodule_row: Submodule row. + """ + submodules = submodule_row[0].split(".", 1) + + if len(submodules) == 1: + submodule, status, last_run = submodule_row + current_dict[submodule] = submodule, status, last_run + return + + submodule, submodule_row[0] = submodules + next_level: dict[str, Any] = current_dict.setdefault(submodule, {}) + self._build_submodules_tree_dict_step(next_level, submodule_row) + return + + def _build_tree_widget( + self, + parent: QW.QTreeWidgetItem | QW.QTreeWidget, + level: dict[str, dict] | dict[str, tuple[str, str, str]] | tuple[str, str, str], + ) -> Optional[QW.QTreeWidgetItem]: + """Build the tree widget using the given level information. + + Args: + parent: Parent QTreeWidgetItem or QTreeWidget. + level: Level to build the tree widget with. + """ + if isinstance(level, dict): + for key, next_level in level.items(): + if isinstance(next_level, tuple): + self._build_tree_widget(parent, next_level) + continue + new_parent_item = QW.QTreeWidgetItem(parent, (key, "", "")) + new_parent_item.setFont(0, self.bold_font) + new_parent_item.setExpanded(True) + new_parent_item.setFlags( + QC.Qt.ItemFlag( + new_parent_item.flags() & ~QC.Qt.ItemFlag.ItemIsSelectable + ) + ) + new_parent_item.setIcon(0, get_icon("libre-gui-folder-open.svg")) + + self._build_tree_widget(new_parent_item, next_level) + elif isinstance(level, tuple): + new_leaf_item = QW.QTreeWidgetItem(parent, level) + if isinstance( + self.tests[level[0]].package.module, + (ModuleNotFoundType, ModuleInternalErrorType), + ): + new_leaf_item.setForeground(0, QG.QColor("red")) + new_leaf_item.setIcon(0, get_icon("file-notify.svg")) + else: + new_leaf_item.setIcon(0, get_icon("file.svg")) + self.test_items[level[0]] = new_leaf_item def get_cols(self, test: Test) -> List[str]: + """Get the columns for the given test. + + Args: + test: Test to get the columns for. + + Returns: + Columns for the given test. + """ cols = [test.package.name_from_source] if test.result is None: cols.extend(["NOT EXECUTED", ""]) @@ -70,40 +374,94 @@ def get_cols(self, test: Test) -> List[str]: cols.extend([test.result.result_name, last_run]) return cols - def clear_widget(self): - for _ in range(self.topLevelItemCount()): + def clear_widget(self) -> None: + """Clear the widget.""" + for __ in range(self.topLevelItemCount()): self.takeTopLevelItem(0) - def set_row_background(self, item: QW.QTreeWidgetItem): - for col in range(item.columnCount()): - item.setBackground(col, QG.QColor(GREY)) + def get_selected_test(self) -> Test | None: + """Return the currently selected test. - def get_selected_test(self) -> Test: - item = self.selectedItems()[0] - test_index = self.get_current_row(item) - return self.tests[test_index] + Returns: + Currently selected test. + """ + item = self.current_item + assert isinstance(item, QW.QTreeWidgetItem) + if item.childCount() > 0: + return None + test_name = item.text(0) + return self.tests[test_name] def get_current_row(self, current_item: Optional[QW.QTreeWidgetItem]) -> int: + """Return the row of the given item. If no item is given, the current item is + used. + + Args: + current_item: Current item. Defaults to None. + """ + if current_item is None: return 0 - test_name = current_item.text(0) - for ind, test in enumerate(self.tests): - if test.package.name_from_source == test_name: - return ind - return 0 + return tuple(self.tests.keys()).index(test_name, 0) + + def filter_items(self, search_str: str) -> None: + """Filter the items in the tree widget using the given search string. + + Args: + search_str: Search string. + """ + # Hide items that don't contain the text + search_str = search_str.lower() + for i in range(self.topLevelItemCount()): + self._filter(self.topLevelItem(i), search_str) # type: ignore + + def _filter(self, item: QW.QTreeWidgetItem, search_str: str) -> bool: + """Recursively filter the items in the tree widget using the given search. + + Args: + item: Current item to check. + search_str: Search string. - def eventFilter( # pylint: disable=invalid-name + Returns: + Whether the item is enabled or not. + """ + is_enabled = search_str in item.text(0).lower() + for i in range(item.childCount()): + child_item = item.child(i) + is_enabled = child_item is not None and ( + is_enabled | self._filter(child_item, search_str) + ) + + item.setHidden(not is_enabled) + return is_enabled + + def eventFilter( # pylint: disable=invalid-name # noqa: N802 #type: ignore self, source: QC.QObject, event: QC.QEvent ) -> bool: - if event.type() == QC.QEvent.ContextMenu and source is self: + """Custom event filter to handle the context menu event. + + Args: + source: Source of the event. + event: Event to handle. + + Returns: + Whether the event was handled or not. + """ + if event.type() == QC.QEvent.Type.ContextMenu and source is self: self.menu.run(event) return True - return super(TestListWidget, self).eventFilter(source, event) + return super().eventFilter(source, event) class TestContextMenu(QW.QMenu): + """Context menu for the test list widget. + + Args: + parent: Parent widget. Defaults to None. + """ + def __init__(self, parent: Optional[QW.QWidget] = None) -> None: super().__init__(parent) # Actions @@ -113,5 +471,10 @@ def __init__(self, parent: Optional[QW.QWidget] = None) -> None: self.addAction(self.run_script) self.addAction(self.code_snippet) - def run(self, event: QC.QEvent): + def run(self, event: QC.QEvent) -> None: + """Run the context menu. + + Args: + event: Event used to get context menu position. + """ super().exec_(event.globalPos()) diff --git a/moduletester/gui/widgets/test_prop_widget.py b/moduletester/gui/widgets/test_prop_widget.py index 8ba8a12..35cd230 100644 --- a/moduletester/gui/widgets/test_prop_widget.py +++ b/moduletester/gui/widgets/test_prop_widget.py @@ -3,64 +3,69 @@ from typing import Any, Dict, Optional -from qtpy import QtCore as QC +from guidata.dataset import DataSet +from guidata.dataset.dataitems import IntItem, StringItem +from guidata.dataset.qtwidgets import DataSetEditGroupBox from qtpy import QtWidgets as QW +from moduletester.gui.widgets.dockable_widget import DockableQWidget from moduletester.model import Test -class TestProps(QW.QGroupBox): - def __init__(self, parent: Optional[QW.QWidget] = None): - super().__init__(parent) +class _PropertiesDataSet(DataSet): + name = StringItem("Name").set_prop("display", active=False) + source = StringItem("Source").set_prop("display", active=False) + path = StringItem("Path").set_prop("display", active=False) + args = StringItem("Args").set_prop("display", placeholder="No args") + timeout = IntItem("Timeout", default=0) + + +class TestProps(DockableQWidget): + """Widget to display the properties of a test. + + Args: + title: Title of the widget. Defaults to "Test Properties". + parent: Parent widget. Defaults to None. + """ + + def __init__(self, title="Test Properties", parent: Optional[QW.QWidget] = None): + super().__init__(parent, title) self.props: Dict[str, Any] = {} # Widgets - self.table = QW.QTreeWidget() - + self.dataset_gbox = DataSetEditGroupBox(None, _PropertiesDataSet) # Layout self.vlayout = QW.QVBoxLayout(self) - self.vlayout.addWidget(self.table) - - def setup(self): - self.setTitle("Properties") + self.vlayout.addWidget(self.title_label) + self.vlayout.addWidget(self.dataset_gbox) - self.table.setHeaderLabels(["Property", "Value"]) - self.table.setIndentation(False) - self.table.setColumnWidth(0, 100) - self.table.setColumnWidth(1, 200) - self.table.setAlternatingRowColors(True) + self.props = {} - self.table.setEditTriggers(QW.QAbstractItemView.NoEditTriggers) - - self.table.itemDoubleClicked.connect(self.on_item_double_clicked) + def setup(self): + """Setup the widget. Empty.""" + pass def set_props(self, test: Test): - self.props = { - "name": test.package.last_name, - "source": test.package.full_name.split(".")[0], - "path": test.package.root_path, - "args": test.command_args if test.command_args != "" else "No args", - "timeout": test.command_timeout if test.command_timeout != 86400 else 0, - } - - for _ in range(self.table.topLevelItemCount()): - self.table.takeTopLevelItem(0) - - for key, value in self.props.items(): - item = QW.QTreeWidgetItem((str(key), str(value))) - if key not in ("name", "source", "path"): - item.setFlags( - QC.Qt.ItemIsEditable | QC.Qt.ItemIsSelectable | QC.Qt.ItemIsEnabled - ) - - tooltip = f"{key}: {value}" - self.set_tool_tips(item, tooltip) - self.table.addTopLevelItem(item) - - def set_tool_tips(self, item: QW.QTreeWidgetItem, tooltip: str): - for col_index in range(item.columnCount()): - item.setToolTip(col_index, tooltip) - - def on_item_double_clicked(self, item: QW.QTreeWidgetItem, column: int): - if column == 1 and item.flags() & QC.Qt.ItemIsEditable: - self.table.editItem(item, column) + """Set the widget properties from a test. + + Args: + test: Test to set the properties from. + """ + self.props.update( + { + "name": test.package.last_name, + "source": test.package.full_name.split(".")[0], + "path": test.package.root_path, + "args": test.command_args, + "timeout": test.command_timeout, + } + ) + dataset = self.dataset_gbox.dataset + dataset.name = test.package.last_name + dataset.source = test.package.full_name.split(".")[0] + dataset.path = test.package.root_path + dataset.args = test.command_args + dataset.timeout = test.command_timeout + + self.dataset_gbox.get() + self.dataset_gbox.set_apply_button_state(False) diff --git a/moduletester/gui/widgets/toolbox_widget.py b/moduletester/gui/widgets/toolbox_widget.py new file mode 100644 index 0000000..f033b44 --- /dev/null +++ b/moduletester/gui/widgets/toolbox_widget.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from PyQt5.QtWidgets import QWidget +from qtpy import QtWidgets as QW + +from moduletester.gui.states.signals import TMSignals +from moduletester.gui.widgets.config_editor import ConfigEditor +from moduletester.gui.widgets.dockable_widget import DockableQWidget + + +class Toolbox(DockableQWidget): + """A toolbox widget to hold various tools. + + Args: + parent: Parent widget. Defaults to None. + signals: Signals object that contains shared global ModuleTester signals. + title: Title of the widget. Defaults to "Toolbox". + """ + + def __init__( + self, + parent: QWidget | None, + signals: TMSignals, + title: str = "Toolbox", + ) -> None: + super().__init__(parent, title) + self.signals = signals + + self.vlayout = QW.QVBoxLayout(self) + + self.tbx = QW.QToolBox() + + self.config_editor = ConfigEditor( + self, + self.signals, + "Config Editor", + ) + self.tbx.addItem(self.config_editor, "Config editor") + + self.vlayout.addWidget(self.tbx) + + self.setup() + self.setLayout(self.vlayout) + + def setup(self) -> None: + """Setup the widget. Empty.""" + pass diff --git a/moduletester/gui/widgets/web_engine.py b/moduletester/gui/widgets/web_engine.py new file mode 100644 index 0000000..66a9bd5 --- /dev/null +++ b/moduletester/gui/widgets/web_engine.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from typing import Optional + +import qtpy.QtWebEngineCore as QWEBC +import qtpy.QtWebEngineWidgets as QWEB +from qtpy.QtWidgets import QAction, QMenu + + +class UrlBloquer(QWEBC.QWebEngineUrlRequestInterceptor): + """Simple Url blocker for QWebEngineView.""" + + def interceptRequest(self, info: QWEBC.QWebEngineUrlRequestInfo) -> None: + """Intercept the request and block it if it is a url. + + Args: + info: Request info. + """ + scheme = info.requestUrl().scheme() + navigation_type = info.navigationType() + ressource_type = info.resourceType() + + block = not ( + (scheme == "data") + or ( + navigation_type + == QWEBC.QWebEngineUrlRequestInfo.NavigationType.NavigationTypeLink + and ressource_type + == QWEBC.QWebEngineUrlRequestInfo.ResourceType.ResourceTypeImage + ) + ) + + return info.block(block) + + +class SimpleWebViewer(QWEB.QWebEngineView): # type: ignore + """Simplified QWebEngineView web viewer. + + Args: + web_actions: List of QAction or QWebEnginePage.WebAction to add to the context + menu. + *arhs: Arguments to pass to the parent QWebEngineView class. + """ + + def __init__( + self, *args, web_actions: Optional[list[QAction | QWEB.QWebEnginePage.WebAction]] = None # type: ignore + ): + super().__init__(*args) + self.protect_settings() + self.protect_profile() + + self.menu = self.setup_menu(web_actions or []) + + def protect_settings(self): + """Creates new settings for the QWebEngineView. These settings are meant to + protect the user from malicious content.""" + WebAttribute = QWEB.QWebEngineSettings.WebAttribute # type: ignore + new_settings = { + WebAttribute.AutoLoadImages: True, + WebAttribute.JavascriptEnabled: False, + WebAttribute.JavascriptCanOpenWindows: False, + WebAttribute.JavascriptCanAccessClipboard: False, + WebAttribute.LinksIncludedInFocusChain: False, + WebAttribute.LocalStorageEnabled: False, + WebAttribute.LocalContentCanAccessRemoteUrls: False, + WebAttribute.XSSAuditingEnabled: True, + WebAttribute.SpatialNavigationEnabled: False, + WebAttribute.LocalContentCanAccessFileUrls: False, + WebAttribute.HyperlinkAuditingEnabled: False, + WebAttribute.ScrollAnimatorEnabled: False, + WebAttribute.ErrorPageEnabled: False, + WebAttribute.PluginsEnabled: False, + WebAttribute.FullScreenSupportEnabled: False, + WebAttribute.ScreenCaptureEnabled: False, + WebAttribute.WebGLEnabled: True, + WebAttribute.Accelerated2dCanvasEnabled: True, + WebAttribute.AutoLoadIconsForPage: False, + WebAttribute.TouchIconsEnabled: False, + WebAttribute.FocusOnNavigationEnabled: False, + WebAttribute.PrintElementBackgrounds: True, + WebAttribute.AllowRunningInsecureContent: False, + WebAttribute.AllowGeolocationOnInsecureOrigins: False, + WebAttribute.AllowWindowActivationFromJavaScript: False, + WebAttribute.ShowScrollBars: True, + WebAttribute.PlaybackRequiresUserGesture: True, + WebAttribute.WebRTCPublicInterfacesOnly: True, + WebAttribute.JavascriptCanPaste: False, + WebAttribute.DnsPrefetchEnabled: False, + WebAttribute.PdfViewerEnabled: True, + } + + settings = self.page().settings() + for setting, value in new_settings.items(): + settings.setAttribute(setting, value) + + def protect_profile(self): + """Protect the profile of the QWebEngineView. This is done by setting a + UrlRequestInterceptor that will block any request that is not a data url or a + link to an image. + """ + self.page().profile().setUrlRequestInterceptor(UrlBloquer(self)) + + def setup_menu( + self, web_actions: list[QWEB.QWebEnginePage.WebAction] # type: ignore + ) -> QMenu: + """Setup the context menu. + + Args: + web_actions: List of QAction or QWebEnginePage.WebAction to add to the + context menu. + + Returns: + The context menu. + """ + menu = QMenu() + for action in web_actions: + if isinstance(action, QWEB.QWebEnginePage.WebAction): # type: ignore + action = self.pageAction(action) + menu.addAction(action) + return menu + + def contextMenuEvent(self, event): + """Show the context menu. + + Args: + event: Context menu event to get the position from. + """ + self.menu.exec_(event.globalPos()) diff --git a/moduletester/gui/window.py b/moduletester/gui/window.py index 200f024..003802e 100644 --- a/moduletester/gui/window.py +++ b/moduletester/gui/window.py @@ -2,10 +2,12 @@ # pylint: disable=missing-module-docstring # guitest: skip +from __future__ import annotations + import os from importlib import import_module from pathlib import Path -from typing import Optional +from typing import TYPE_CHECKING, Iterable, Optional from guidata.config import CONF # type: ignore from guidata.configtools import get_font, get_icon, get_image_file_path # type: ignore @@ -13,18 +15,25 @@ from qtpy import QtGui as QG from qtpy import QtWidgets as QW +from moduletester import config +from moduletester.config import _ +from moduletester.test_exporter import TestListDocument, TestResultsDocument + from ..config import APP_NAME from ..manager import TestManager -from ..model import Module, Test -from ..python_helpers import rst2odt +from ..model import Module, Test, TestSuite from .components.body_component import TMWidget from .components.status_bar_component import TMStatusBar from .components.tool_bar_component import TestManagerToolbar -from .states.signals import TMSignals -from .states.state_machine import TMStateMachine + +if TYPE_CHECKING: + from .states.signals import TMSignals + from .states.state_machine import TMStateMachine class TMWindow(QW.QMainWindow): + export_finished = QC.Signal(str) # type: ignore + def __init__( self, signals: TMSignals, @@ -41,20 +50,31 @@ def __init__( font = get_font(CONF, "codeeditor") ffamily, fsize = font.family(), font.pointSize() bgurl = Path(get_image_file_path("ModuleTester-watermark.png")).as_posix() - self.ss_nobg = f"QWidget {{ font-family: '{ffamily}'; font-size: {fsize}pt;}}" + # self.ss_nobg = f"QWidget {{ font-family: '{ffamily}'; font-size: {fsize}pt;}}" + self.ss_nobg = f"QWidget {{ font-size: {fsize}pt;}}" self.ss_withbg = f"QMainWindow {{ background: url({bgurl}) no-repeat center;}}" self.setStyleSheet(self.ss_withbg + " " + self.ss_nobg) + # self.setStyleSheet(self.ss_withbg) self.signals = signals + self.last_file_dir: str + self.last_export_dir: str + self.manager: Optional[TestManager] = None if package is not None and moduletester_path is None: - self.manager = TestManager(package, _category="visible") + self.manager = self.new_test_manager( + package, _category=config.PACKAGE_CONF["general"].category + ) + self.last_export_dir = self.last_file_dir = package.root_path or os.getcwd() elif package is None and moduletester_path is not None: - self.manager = TestManager( - moduletester_path=moduletester_path, _category="visible" + self.manager = self.new_test_manager( + moduletester_path=moduletester_path, + _category=config.PACKAGE_CONF["general"].category, ) + self.last_export_dir = self.last_file_dir = moduletester_path else: self.manager = None + self.last_export_dir = self.last_file_dir = os.getcwd() self.toolbar = TestManagerToolbar(self) self.statusbar = TMStatusBar(self) @@ -67,17 +87,40 @@ def __init__( self.statusbar.set_state_label("Not loaded") self.statusbar.set_path_label("") + self.export_finished.connect(self.doc_exported) + if self.manager is not None: - self.central_widget = TMWidget( - self.signals, self.manager.test_suite, moduletester_path, self - ) + self.set_central_widget(self.manager.test_suite, moduletester_path) self.setup() + def set_central_widget( + self, test_suite: TestSuite, path: Optional[str] = None + ) -> None: + """Create or update the central widget with the given test_suite and path. This + methods avoids to create a new widget if the central widget already exists. + + Args: + test_suite: The test_suite to display in the central widget. + path: The path of the moduletester file. Defaults + to None. + """ + if hasattr(self, "central_widget"): + self.central_widget.update_widget(test_suite, path) + else: + self.central_widget = TMWidget(self.signals, test_suite, path, self) + @property def current_test(self) -> Test: return self.central_widget.test_list.get_selected_test() def closeEvent(self, a0: QG.QCloseEvent) -> None: # pylint: disable=C0103 + """Close the main window and stop the thread if it is running. If the file has + been modified, a message box is displayed to ask the user if he wants to save + the file. + + Args: + a0: The close event. + """ if self.state_machine.running_state.active(): self.central_widget.stop_thread() @@ -87,6 +130,7 @@ def closeEvent(self, a0: QG.QCloseEvent) -> None: # pylint: disable=C0103 return super().closeEvent(a0) def save_alert(self): + """Display a message box to ask the user if he wants to save the current file.""" save_mb = QW.QMessageBox( QW.QMessageBox.Warning, APP_NAME, @@ -101,6 +145,7 @@ def save_alert(self): save_mb.exec() def save_alert_accepted(self): + """Save the current file if the user wants to save the modifications.""" self.save() QW.QMessageBox( @@ -111,12 +156,14 @@ def save_alert_accepted(self): ).exec_() def setup(self): + """Setup the main window with the current test_suite.""" self.setWindowTitle(f"{APP_NAME} - Module {self.manager.module.full_name}") self.setMinimumSize(0, 0) self.setStyleSheet(self.ss_nobg) self.setCentralWidget(self.central_widget) self.signals.SIG_PROJECT_LOADED.emit() self.connect_test_actions() + self.toolbar.setup_view(self.central_widget.view_menu) def show(self): super().show() @@ -126,17 +173,32 @@ def show(self): parent=self, ) + def refresh_package(self): + """Refresh the package and the central widget. This method should keep the + current results.""" + if self.manager is None: + return + self.manager.refresh_package(category=config.PACKAGE_CONF["general"].category) + self.signals.SIG_PROJECT_LOADED.emit() + def connect_file_actions(self): + """Connect the toolbar file actions to instance methods.""" self.toolbar.new_file_action.triggered.connect(self.create_new_file) self.toolbar.open_action.triggered.connect(self.open) + self.toolbar.update_action.triggered.connect(self.refresh_package) self.toolbar.save_action.triggered.connect(self.save) self.toolbar.save_as_action.triggered.connect(self.save_as) - self.toolbar.export_dtv_action.triggered.connect(lambda: self.export_dtv(None)) - self.toolbar.export_rtv_action.triggered.connect(lambda: self.export_rtv(None)) + self.toolbar.export_test_list_action.triggered.connect( + lambda: self.export_test_list(None) + ) + self.toolbar.export_test_results_action.triggered.connect( + lambda: self.export_test_results(None) + ) self.toolbar.export_action.triggered.connect(self.export) def connect_test_actions(self): + """Connect the toolbar test actions to the central widget methods.""" self.toolbar.run_action.triggered.connect(self.central_widget.run_test) self.toolbar.stop_action.triggered.connect(self.central_widget.stop_thread) self.toolbar.restart_action.triggered.connect( @@ -144,45 +206,71 @@ def connect_test_actions(self): ) def apply_changes(self, test: Test): - description = self.central_widget.test_information.description - comment = self.central_widget.result_information.comment + """Save the comment of the test in the result object.""" + comment = ( + self.central_widget.result_information.comment_widget.cached_comments.get( + test.package.full_name, None + ) + ) - test.description = description - if test.result is not None: + if test.result is not None and comment is not None: test.result.comment = comment - def get_open_file_name(self): - path = os.getcwd() - if self.manager is not None: - path = self.manager.moduletester_path + def apply_all_changes(self): + """Parse all the tests and apply the changes to the result object.""" + if self.manager is None: + return + + # Updates the cached comment of the current test if the timer is still running + comment_widget = self.central_widget.result_information.comment_widget + if comment_widget.timer.isActive(): + comment_widget.timer.stop() + comment_widget.update_cached_comment() + for test in self.manager.test_suite.tests: + self.apply_changes(test) + + def get_open_file_name(self) -> str: + """Open a file dialog to select a .moduletester file to open. + Returns: + str: The path of the selected file. + """ open_file_name = QW.QFileDialog.getOpenFileName( - self, "Open .moduletester file", path, "*.moduletester" + self, "Open .moduletester file", self.last_file_dir, "*.moduletester" ) file_path = open_file_name[0] + self.last_file_dir = os.path.dirname(file_path) return file_path - def get_save_file_name(self): - path = os.getcwd() - if self.manager is not None: - path = self.manager.moduletester_path + def get_save_file_name(self) -> str: + """Open a file dialog to select a file to save the .moduletester file. + Returns: + str: The path of the selected file. + """ save_file_name = QW.QFileDialog.getSaveFileName( - self, "Save .moduletester file", path, "*.moduletester *.txt" + self, "Save .moduletester file", self.last_file_dir, "*.moduletester *.txt" ) file_path = save_file_name[0] + self.last_file_dir = os.path.dirname(file_path) return file_path - def get_existing_dir(self): + def get_existing_dir(self) -> str: + """Open a file dialog to select an existing directory. + + Returns: + str: The path of the selected directory. + """ dir_name = QW.QFileDialog.getExistingDirectory( self, "Export Directory", - self.manager.module.root_path, + self.last_export_dir, QW.QFileDialog.ShowDirsOnly, ) return dir_name def open(self): + """Open a .moduletester file and load the tests.""" if ( self.state_machine.modified_state.active() and self.state_machine.has_file_state.active() @@ -193,13 +281,161 @@ def open(self): if not os.path.exists(file_path): return - self.manager = TestManager(moduletester_path=file_path, _category="visible") - self.central_widget = TMWidget( - self.signals, self.manager.test_suite, file_path, self + if os.path.exists( + bkp_file := (file_path + ".bkp") + ) and self.backup_file_exists_warning(bkp_file): + file_path = bkp_file + + self.manager = self.new_test_manager( + moduletester_path=file_path, + _category=config.PACKAGE_CONF["general"].category, ) + if self.manager is None: + return + + if len(missing_modules := self.manager.get_missing_modules()) > 0: + self._handle_missing_module(missing_modules) + + if len(errored_modules := self.manager.get_errored_modules()) > 0: + self._notifiy_errored_module(errored_modules) + + self.set_central_widget(self.manager.test_suite, file_path) self.setup() self.signals.SIG_FILE_LOADED.emit(file_path) + def new_test_manager( + self, + module: Module | None = None, + moduletester_path: str | None = None, + _category: str = config.PACKAGE_CONF["general"].category, + _template_path: str = "", + ) -> TestManager | None: + """Create a new TestManager object and handle the configuration errors. + + Args: + module: Module object that contains the tests. Defaults to None. + moduletester_path: Path to the moduletester file. Defaults to None. + _category: Test discovery category. Defaults to the value stored in + config.PACKAGE_CONF["general"].category. + _template_path: .moduletester template file. Defaults to "". + + Returns: + TestManager: A new TestManager object or None if the configuration file + contains errors. + """ + manager = TestManager(module, moduletester_path, _category, _template_path) + if (conf_err := manager.get_conf_conflict_err()) is not None: + ok = self._resolve_moduletester_config_error(conf_err) + if ok: + return self.new_test_manager( + module, moduletester_path, _category, _template_path + ) + else: + return manager + + if (conf_err := manager.get_conf_path_val_err()) is not None: + print("Error was not None") + # TODO: open a conf editor to allow the user to modify the config file + conf_file = os.path.join( + config.MODULETESTER_CONFIG_DIR, config.MODULETESTER_CONFIG_NAME + ) + QW.QMessageBox.critical( + self, + "Configuration file contains invalid values", + f"Configuration file {conf_file} contains " + f"invalid value:\n {conf_err.key} = {conf_err.value}", + QW.QMessageBox.Cancel, + ) + return None + return manager + + def _resolve_moduletester_config_error(self, e: config.ConfigConflictError) -> bool: + """Handle the configuration file error by opening a dialog to allow the user to + fix the error. + + Args: + e: The configuration error. + + Returns: + True if the user wants to fix the error and save the file, False otherwise. + """ + config_path = os.path.join( + config.MODULETESTER_CONFIG_DIR, config.MODULETESTER_CONFIG_NAME + ) + response = ( + QW.QMessageBox.critical( + self, + "Error in configuration file", + f"Error in configuration file: {config_path}\n{str(e)}" + "\nDo you want to fix the error and save the file?", + QW.QMessageBox.Apply | QW.QMessageBox.Cancel, + ) + == QW.QMessageBox.Apply + ) + if response: + + config.load_package_conf(config_path, resolve=True) + config.save_config(config.PACKAGE_CONF, config_path) + print(config_path) + return True + return False + + def _handle_missing_module(self, missing_modules: list[Module]) -> None: + """Handle the missing modules by asking the user if he wants to reimport all the + tests and override the current file. + + Args: + missing_modules: List of missing modules. + """ + if self.manager is None: + return + missing_modules_names = ", ".join(map(lambda m: m.full_name, missing_modules)) + response = ( + QW.QMessageBox.warning( + self, + _("Error during moduletester file loading."), + ( + _("Error while parsing the .moduletester file: %s") + % f"{self.manager.moduletester_path}\n" + + _("Missing modules:") + + f"\n\n{missing_modules_names}\n\n" + + _( + "Do you want to reimport all tests and override the current file?" + ) + ), + buttons=QW.QMessageBox.Ok | QW.QMessageBox.Cancel, + ) + == QW.QMessageBox.Ok + ) + if response: + self.manager.reload() + self.manager.save() + self.signals.SIG_FILE_LOADED.emit(self.manager.moduletester_path) + + def _notifiy_errored_module(self, errored_modules: list[Module]) -> None: + """Notify the user that some modules could not be imported do to error in the + imported file. + + Args: + errored_modules: list of errored modules. + """ + if self.manager is None: + return + errored_modules_names = ", ".join(map(lambda m: m.full_name, errored_modules)) + _response = QW.QMessageBox.warning( + self, + _("Error during module loading."), + ( + _("Error while importing modules:") + + f"\n\n{errored_modules_names}\n\n" + + _( + "The modules are still visible in moduletester but should be fixed " + "or removed." + ) + ), + buttons=QW.QMessageBox.Ok, + ) + def create_new_file(self): if ( self.state_machine.modified_state.active() @@ -219,52 +455,138 @@ def create_new_file(self): edit.setFixedSize(220, 25) btn.setFixedWidth(80) - vlayout.addWidget(edit, alignment=QC.Qt.AlignRight) - vlayout.addWidget(btn, alignment=QC.Qt.AlignRight) + vlayout.addWidget(edit, alignment=QC.Qt.AlignmentFlag.AlignRight) + vlayout.addWidget(btn, alignment=QC.Qt.AlignmentFlag.AlignRight) btn.clicked.connect(lambda: self.create_template(edit.text(), dialog)) dialog.exec() + def clear_dock_widgets(self): + """Remove all the dock widgets from the main window.""" + for dock in self.findChildren(QW.QDockWidget): + self.removeDockWidget(dock) + self.toolbar.removeAction(dock.toggleViewAction()) + def create_template(self, module_name: str, dialog: QW.QDialog): + """Create a new template file with the given module name. + + Args: + module_name: The name of the module. + dialog: The dialog that contains the module name input. + """ try: module = Module(import_module(module_name)) dialog.close() - self.manager = TestManager(module, _category="visible") - self.central_widget = TMWidget( - self.signals, self.manager.test_suite, parent=self + self.manager = self.new_test_manager( + module, _category=config.PACKAGE_CONF["general"].category ) + if self.manager is None: + return + if len(errored_modules := self.manager.get_errored_modules()) > 0: + self._notifiy_errored_module(errored_modules) + self.set_central_widget(self.manager.test_suite) self.setup() self.signals.SIG_PROJECT_LOADED.emit() self.signals.SIG_TEMPLATE_CREATED.emit() - except ModuleNotFoundError: + except (ModuleNotFoundError, ValueError): QW.QMessageBox( QW.QMessageBox.Icon.Critical, "Module not found", f"No module named {module_name}", ).exec() + def file_exists_warning(self, file_path: str) -> bool: + """Display a warning message to the user if the file already exists. + + Args: + file_path: The path of the file. + + Returns: + True if the user wants to overwrite the file, False otherwise. + """ + res = QW.QMessageBox.warning( + self, + "File already exists", + f"File {file_path} already exists, do you want to overwite it?", + buttons=QW.QMessageBox.Yes | QW.QMessageBox.No, + defaultButton=QW.QMessageBox.No, + ) + return res == QW.QMessageBox.Yes + + def backup_file_exists_warning(self, bkp_file_path: str) -> bool: + """Display a warning message to the user if a backup file was found. + + Args: + bkp_file_path: The path of the backup file. + + Returns: + True if the user wants to import the backup file, False otherwise. + """ + res = QW.QMessageBox.warning( + self, + "Backup file found", + f"Backup file {bkp_file_path} was found which means the application may " + "have crashed during saving. \nDo you want to import it instead?", + buttons=QW.QMessageBox.Yes | QW.QMessageBox.No, + defaultButton=QW.QMessageBox.No, + ) + return res == QW.QMessageBox.Yes + + def alert_test_running(self): + """Alert the user that a test is currently running.""" + QW.QMessageBox.warning( + self, + _("Test running"), + _( + "A test is currently running, please wait for it " + "to finish before saving." + ), + ) + def save(self): - if self.manager.moduletester_path is None: + """Save the current test_suite to the current file. If the file does not exist, + the user is asked to select a file to save the test_suite. + """ + if (manager := self.manager) is None: + return + + if manager.test_suite.running_test is not None: + self.alert_test_running() + return + + if manager.moduletester_path is None: self.save_as() else: - test = self.current_test - self.apply_changes(test) - self.manager.save() - self.signals.SIG_FILE_LOADED.emit(self.manager.moduletester_path) - self.signals.SIG_PROJECT_SAVED.emit(self.manager.moduletester_path) + + self.apply_all_changes() + manager.save() + self.signals.SIG_FILE_LOADED.emit(manager.moduletester_path) + self.signals.SIG_PROJECT_SAVED.emit(manager.moduletester_path) def save_as(self): + """Save the current test_suite to a new file. If the file already exists, the + user is asked if he wants to overwrite it. + """ file_path = self.get_save_file_name() + # is_save_ok = True if file_path == "": return + # elif os.path.exists(file_path): + # is_save_ok = self.file_exists_warning(file_path) + # if is_save_ok: + # open(file_path, "w", encoding="utf-8").close() elif not os.path.exists(file_path): open(file_path, "w", encoding="utf-8").close() - test = self.current_test + if self.manager is None: + return - self.apply_changes(test) + if self.state_machine.running_state.active(): + self.alert_test_running() + return + self.apply_all_changes() self.manager.save_as(file_path) self.central_widget.moduletester_path = self.manager.moduletester_path self.central_widget.set_item() @@ -272,63 +594,152 @@ def save_as(self): self.signals.SIG_FILE_LOADED.emit(file_path) self.signals.SIG_PROJECT_SAVED.emit(file_path) + def _multi_export_callback(self, basename: str, fmts: Iterable[str]): + """Callback function for the multi_export_async method of the TestListDocument + and TestResultsDocument classes. + + Args: + basename: The basename of the file that are exported. + fmts: The formats of the files that are exported (their extensions). + """ + + for fmt in fmts: + self.export_finished.emit(f"{basename}.{fmt}") + + def _check_exports(self, abs_out_basename: str, fmts: Iterable[str]) -> bool: + """Check if the files already exist and ask the user if he wants to overwrite + them. + + Args: + abs_out_basename: The absolute path of the file to export. + fmts: The formats of the files to export (their extensions). + """ + files_already_exist: list[str] = [ + filename + for fmt in fmts + if os.path.isfile(filename := f"{abs_out_basename}.{fmt}") + ] + files_str = ", ".join(files_already_exist) + if len(files_already_exist) > 0: + res = QW.QMessageBox.warning( + self, + "File already exists.", + ( + f"The following files aleady exist: {files_str}. \n" + f"File {abs_out_basename} already exists, do you want to overwite it?" + ), + buttons=QW.QMessageBox.Yes | QW.QMessageBox.No, + defaultButton=QW.QMessageBox.No, + ) + return res == QW.QMessageBox.Yes + + return True + def export(self): + """Export both TestListDocument and TestResultsDocument files to the same + directory. The user is asked to select the directory. + """ dir_name = self.get_existing_dir() if dir_name == "": return - test = self.current_test - self.apply_changes(test) + self.export_test_list(dir_name) + self.export_test_results(dir_name) + + def export_test_list(self, dir_name: Optional[str] = None): + """Exports a TestListDocument file to the given directory. If the directory is + None, the user is asked to select a directory. - self.export_dtv(dir_name) - self.export_rtv(dir_name) + Args: + dir_name: Path to the export directory. Defaults to None. + """ - def export_dtv(self, dir_name: Optional[str] = None): if dir_name is None: dir_name = self.get_existing_dir() if dir_name == "": return - test = self.current_test - self.apply_changes(test) - target_dir = os.path.join(dir_name, "dtv") + self.apply_all_changes() - self.manager.export(dir_name, "dtv") + model = "test_list" + fmts = [] + abs_out_basename = os.path.join(dir_name, model) + if self.manager is not None and self.manager.module is not None: + abs_out_basename = os.path.join( + dir_name, f"{model}_{self.manager.module.last_name}" + ) + fmts.extend(config.PACKAGE_CONF["export"].export_fmts) - source = os.path.join(target_dir, "dtv.rst") - dest = os.path.join(target_dir, "dtv.odt") - rst2odt(source, dest) + else: + raise ValueError("No manager or module loaded") - self.odt_created(dest) + if not self._check_exports(abs_out_basename, fmts): + return + + self.statusbar.set_export_label(f"Exporting {abs_out_basename} to {fmts}") + # rst2odt(source, dest) + doc = TestListDocument( + test_suite=self.manager.test_suite, + reload_template=config.PACKAGE_CONF["export"].reload_templates_on_export, + template_name=config.PACKAGE_CONF["export"].test_list_template_name, + ) + doc.multi_exports_async( + fmts, + abs_out_basename, + lambda: self._multi_export_callback(abs_out_basename, fmts), + ) + + def export_test_results(self, dir_name: Optional[str] = None): + """Exports a TestResultsDocument file to the given directory. If the directory + is None, the user is asked to select a directory. - def export_rtv(self, dir_name: Optional[str] = None): + Args: + dir_name: Path to the export directory. Defaults to None. + """ if dir_name is None: dir_name = self.get_existing_dir() if dir_name == "": return - test = self.current_test - self.apply_changes(test) - target_dir = os.path.join(dir_name, "rtv") + self.apply_all_changes() + model = "test_results" - self.manager.export(dir_name, "rtv") + fmts = [] + abs_out_basename = os.path.join(dir_name, model) + if self.manager is not None and self.manager.module is not None: + abs_out_basename = os.path.join( + dir_name, f"{model}_{self.manager.module.last_name}" + ) + fmts.extend(config.PACKAGE_CONF["export"].export_fmts) - source = os.path.join(target_dir, "rtv.rst") - dest = os.path.join(target_dir, "rtv.odt") - rst2odt(source, dest) + else: + raise ValueError("No manager or module loaded") - self.odt_created(dest) + if not self._check_exports(abs_out_basename, fmts): + return + self.statusbar.set_export_label(f"Exporting {abs_out_basename} to {fmts}") + doc = TestResultsDocument( + test_suite=self.manager.test_suite, + reload_template=config.PACKAGE_CONF["export"].reload_templates_on_export, + template_name=config.PACKAGE_CONF["export"].test_results_template_name, + ) + doc.multi_exports_async( + fmts, + abs_out_basename, + lambda: self._multi_export_callback(abs_out_basename, fmts), + ) - def odt_created(self, file: str): + def doc_exported(self, file: str): + self.statusbar.set_export_label("") odt_mb = QW.QMessageBox( QW.QMessageBox.NoIcon, "TestManager", - f"Odt file generated in: \n{file}", + f"File generated: \n{file}", QW.QMessageBox.StandardButtons(QW.QMessageBox.Open | QW.QMessageBox.Close), ) - odt_mb.accepted.connect(lambda: self.open_odt_files(file)) # type: ignore + odt_mb.accepted.connect(lambda: self.open_file(file)) # type: ignore odt_mb.exec_() - def open_odt_files(self, fname: str): + def open_file(self, fname: str): QG.QDesktopServices.openUrl(QC.QUrl.fromLocalFile(fname)) diff --git a/moduletester/locale/fr/LC_MESSAGES/moduletester.po b/moduletester/locale/fr/LC_MESSAGES/moduletester.po new file mode 100644 index 0000000..56e0932 --- /dev/null +++ b/moduletester/locale/fr/LC_MESSAGES/moduletester.po @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ORGANIZATION +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"POT-Creation-Date: 2024-03-13 16:12+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: pygettext.py 1.5\n" + +#: moduletester\gui\window.py:396 +msgid "Error during moduletester file loading." +msgstr "Erreur lors du chargement du fichier moduletester." + +#: moduletester\gui\window.py:398 +msgid "Error while parsing the .moduletester file: %s" +msgstr "Erreur lors du chargement du fichier .moduletester : %s" + +#: moduletester\gui\window.py:400 +msgid "Missing modules:" +msgstr "Modules manquants :" + +#: moduletester\gui\window.py:402 +msgid "Do you want to reimport all tests and override the current file?" +msgstr "Vous voulez réimporter tous les tests et écraser le fichier actuel ?" + +#: moduletester\gui\window.py:427 +msgid "Error during module loading." +msgstr "Erreur lors du chargement du module." + +#: moduletester\gui\window.py:429 +msgid "Error while importing modules:" +msgstr "Erreur lors de l'importation des modules :" + +#: moduletester\gui\window.py:431 +msgid "" +"The modules are still visible in moduletester but should be fixed or removed." +msgstr "" +"Les modules sont toujours visibles dans moduletester mais devraient être " +"corrigés ou supprimés." + +#: moduletester\gui\window.py:540 +msgid "Test running" +msgstr "Test en cours" + +#: moduletester\gui\window.py:541 +msgid "" +"A test is currently running, please wait for it to finish before saving." +msgstr "" +"Un test est en cours, veuillez attendre qu'il se termine avant de " +"sauvegarder." + +#: moduletester\model.py:89 +msgid "" +"This package encountered the following error during import:\n" +"%s" +msgstr "" +"Ce paquet a rencontré l'erreur suivante lors de l'import :\n" +"%s" + +msgid "Test List Document" +msgstr "Document de la liste des tests" + +msgid "Test Results Document" +msgstr "Document de résultat de tests" + +#~ msgid "Last execution date:" +#~ msgstr "Dernière date d'exécution :" + +#~ msgid "%s test results summary" +#~ msgstr "%s résumé des résultats de test" + +#~ msgid "No result" +#~ msgstr "Pas de résultat" + +#~ msgid "Accepted" +#~ msgstr "Validé" + +#~ msgid "Accepted with reserve" +#~ msgstr "Validé sous réserve" + +#~ msgid "Skipped" +#~ msgstr "Passé" + +#~ msgid "Rejected" +#~ msgstr "Rejeté" + +#~ msgid "Count by result cateogry" +#~ msgstr "Compte par catégorie de résultat" + +#~ msgid "ACCEPTED" +#~ msgstr "ACCEPTÉ" + +#~ msgid "ACCEPTED WITH RESERVES" +#~ msgstr "ACCEPTÉ SOUS RÉSERVE" + +#~ msgid "SKIPPED" +#~ msgstr "PASSÉ" + +#~ msgid "REJECTED" +#~ msgstr "REJETÉ" + +#~ msgid "NO RESULT" +#~ msgstr "PAS DE RÉSULTAT" diff --git a/moduletester/manager.py b/moduletester/manager.py index bf7f555..2d8ddc2 100644 --- a/moduletester/manager.py +++ b/moduletester/manager.py @@ -2,16 +2,18 @@ # pylint: disable=missing-function-docstring, missing-module-docstring # guitest: skip +from __future__ import annotations import os +import shutil from dataclasses import dataclass, field from importlib import import_module -from typing import Optional +from typing import Callable, Optional -import click from guidata.guitest import get_test_package # type: ignore -from .exporter import TestSuiteExporter +from moduletester import config as cfg + from .model import Module, TestSuite from .python_helpers import rst2odt from .serializer import dumper, loader @@ -31,30 +33,47 @@ class TestManager: test_suite: TestSuite = field(init=False) up_to_date: bool = field(init=False, default=True) + _config_conflict_err: Optional[cfg.ConfigConflictError] = field( + init=False, default=None + ) + _config_path_val_err: Optional[cfg.InvalidPathError] = field( + init=False, default=None + ) + _category: str = "all" _template_path: str = "" def __post_init__(self): - if (self.module is None and self.moduletester_path is None) or ( - self.module is not None and self.moduletester_path is not None + mod = self.module + if (mod is None and self.moduletester_path is None) or ( + mod is not None and self.moduletester_path is not None ): raise ValueError("One argument should be None") - elif self.module is not None: + elif mod is not None: print(self._category) - self.test_suite = TestSuite(self.module, _category=self._category) + test_suite = self._try_load_testsuite( + lambda: TestSuite(mod, _category=self._category) + ) + if test_suite is None: + return + + self.test_suite = test_suite if self._template_path == "": - self._template_path = os.path.join( - self.module.path, "template.moduletester" - ) + self._template_path = os.path.join(mod.path, "template.moduletester") dumper(self._template_path, self.test_suite) print(f"Template created in '{self._template_path}'") - elif self.moduletester_path is not None: - test_suite = loader(self.moduletester_path) + elif (mod_p := self.moduletester_path) is not None: + test_suite = self._try_load_testsuite(lambda: loader(mod_p)) + + if test_suite is None: + return + self.module = test_suite.package + test_package = get_test_package(self.module.module) for test in test_suite.tests: test.retrieve_category(test_package) @@ -62,197 +81,55 @@ def __post_init__(self): self.test_suite = test_suite self.up_to_date = True + def _try_load_testsuite( + self, test_suite_init: Callable[[], TestSuite | None] + ) -> TestSuite | None: + try: + return test_suite_init() # type: ignore + except cfg.ConfigConflictError as e: + self._config_conflict_err = e + return None + except cfg.InvalidPathError as e: + self._config_path_val_err = e + return None + + def get_missing_modules(self) -> list[Module]: + return self.test_suite.get_missing_modules() + + def get_errored_modules(self) -> list[Module]: + return self.test_suite.get_errored_modules() + def reload(self): """ """ self.test_suite.reset() - def save_as(self, moduletester_path): + def refresh_package(self, category: Optional[str] = None): + self.test_suite.refresh_package(category) + + def save_as(self, moduletester_path: str): """ """ + backup_file = moduletester_path + ".bkp" + shutil.copy(moduletester_path, backup_file) dumper(moduletester_path, self.test_suite) self.moduletester_path = moduletester_path + os.remove(backup_file) def save(self): """ """ - dumper(self.moduletester_path, self.test_suite) + self.save_as(self.moduletester_path or "") def open(self, moduletester_path: str): """ """ test_suite = loader(moduletester_path) self.test_suite = test_suite - def export(self, basedir: str, model: str): - if model.lower() in ["rtv", "dtv"]: - basedir = os.path.abspath(basedir) - path_to_temp = os.path.join(basedir, model, "tmp") - path_to_rst = os.path.join(basedir, model, f"{model}.rst") - - os.makedirs(path_to_temp, exist_ok=True) - - exporter = TestSuiteExporter(self.test_suite) - export_section = ( - exporter.export_section_dtv - if model == "dtv" - else exporter.export_section_rtv - ) - - exporter.export(path_to_rst, path_to_temp, export_section) - else: - print("model parameter must be in ['rtv', 'dtv']") - - -@click.group(context_settings=CONTEXT_SETTINGS) -def cli(): - pass - - -@cli.command() -@click.argument("package") -@click.option( - "--output", "-o", default="", help="output path for .moduletester template file" -) -def template(package: str, output: str = ""): - """Generate .moduletester template file""" - mod = import_module(package) - - if output != "" and os.path.isdir(output): - output = os.path.join(output, "template.moduletester") - elif ( - output != "" - and not os.path.exists(output) - and not os.path.basename(output).endswith(".moduletester") - ): - raise ValueError(f"'{output}' is incorrect for output") - - _ = TestManager(Module(mod), _template_path=output) - - -@cli.command(context_settings=CONTEXT_SETTINGS) -@click.pass_context -@click.argument("moduletester_path") -@click.option("--category", "-c", default="all", help="guitest category") -@click.option("save_path", "--save", "-s", default="", help="Path to save result") -@click.option("pattern", "--pattern", "-p", default="", help="test name pattern") -@click.option("--timeout", default=86400, type=int, help="test timeout") -def run( - ctx, - moduletester_path: str, - pattern: str = "", - category: str = "all", - save_path: str = "", - timeout: int = 86400, -): - """Run tests with --test-args""" - testmanager = TestManager(moduletester_path=moduletester_path) - - if save_path == "": - save_path = moduletester_path - elif os.path.exists(save_path): - raise ValueError(f'"{save_path}" already exists') - - args = "" - if len(ctx.args) != 0: - args = " ".join(ctx.args) - - testmanager.test_suite.run(category, pattern, timeout, args) - testmanager.save_as(save_path) - print(f"Run saved in {save_path}") - - -@cli.command() -@click.argument("moduletester_path") -@click.option("output_dir", "--output", "-o", default="", help="Output directory") -def dtv(moduletester_path: str, output_dir: str = ""): - """Generate dtv for given .moduletester file""" - testmanager = TestManager(moduletester_path=moduletester_path) - assert testmanager.module - - if output_dir == "": - output_dir = testmanager.module.path - - testmanager.export(output_dir, "dtv") - print(f"DTV exported in {output_dir}") - - -@cli.command() -@click.argument("moduletester_path") -@click.option("output_dir", "--output", "-o", default="", help="Output directory") -def rtv(moduletester_path: str, output_dir: str = ""): - """Generate rtv for given .moduletester file""" - testmanager = TestManager(moduletester_path=moduletester_path) - assert testmanager.module - - if output_dir == "": - output_dir = testmanager.module.path - - testmanager.export(output_dir, "rtv") - print(f"RTV exported in {output_dir}") - - -@cli.command() -@click.argument("moduletester_path") -@click.option("output_dir", "--output", "-o", default="", help="Output directory") -def doc(moduletester_path: str, output_dir: str = ""): - """Generate both rtv and dtv for given .testmanager file""" - testmanager = TestManager(moduletester_path=moduletester_path) - - if testmanager.module is not None and output_dir == "": - path = testmanager.module.path - elif output_dir != "": - path = output_dir - else: - return - - testmanager.export(path, "dtv") - testmanager.export(path, "rtv") - print(f"DTV/RTV exported in {path}") - - -@cli.command -@click.argument("rst_path") -@click.option("output_file", "--output", "-o", default="", help="Output file") -def odt(rst_path: str, output_file: str = ""): - """Convert rst file to odt file""" - if output_file == "": - output_file = os.path.abspath(rst_path.replace(".rst", ".odt")) - rst2odt(rst_path, output_file) - - -@cli.command -@click.argument("moduletester_path") -def ls(moduletester_path: str): # pylint: disable=invalid-name - """list test in file""" - testmanager = TestManager(moduletester_path=moduletester_path, _category="batch") - - if len(testmanager.test_suite.tests) == 0: - print(f"No test found in file {moduletester_path}") - return - - print(f"{len(testmanager.test_suite.tests)} tests found") - for test in testmanager.test_suite.tests: - print(test.package.name_from_source) - - -@cli.command -@click.argument("moduletester_path") -def tree(moduletester_path: str): - """list test in file grouped by directory""" - testmanager = TestManager(moduletester_path=moduletester_path, _category="batch") - grouped_tests = testmanager.test_suite.group_tests() - - len_test = len(testmanager.test_suite.tests) - if len_test == 0: - print(f"No tests found in file {moduletester_path}") - return - - print(f"{len_test} test found\n") - for key, tests in grouped_tests.items(): - print(f"{key}:") - - for test in tests: - print(f" | {test.package.last_name}") - - print("\n") + def pre_export(self, basedir: str, model: str): + basedir = os.path.abspath(basedir) + model_path = os.path.join(basedir, model) + os.makedirs(model_path, exist_ok=True) + def get_conf_conflict_err(self) -> Optional[cfg.ConfigConflictError]: + return self._config_conflict_err -if __name__ == "__main__": - cli() + def get_conf_path_val_err(self) -> Optional[cfg.InvalidPathError]: + return self._config_path_val_err diff --git a/moduletester/model.py b/moduletester/model.py index f172411..d230278 100644 --- a/moduletester/model.py +++ b/moduletester/model.py @@ -1,6 +1,9 @@ # pylint: disable=empty-docstring, missing-class-docstring, keyword-arg-before-vararg # pylint: disable=missing-function-docstring, missing-module-docstring # guitest: skip + +import contextlib +import importlib import os import shlex import signal @@ -11,11 +14,21 @@ from dataclasses import dataclass, field from datetime import datetime, timedelta from enum import Enum +from importlib import metadata from types import ModuleType -from typing import Dict, List, Optional, Union -from guidata.guitest import TestModule, get_tests # type: ignore +# cannot import __future__.annotations because it breaks the ModuleType import for some +# reason. The line: 'loader(self.moduletester_path)' return a string instead of a +# module +from typing import Dict, List, Optional, Tuple, Union + +import pypandoc +from guidata.configtools import get_image_file_path +from guidata.guitest import TestModule, get_test_package, get_tests # type: ignore +import moduletester.module_not_found as empty_module +from moduletester import config +from moduletester.config import _ from moduletester.python_helpers import get_image_path # type: ignore from moduletester.serializer import ( DataclassSerializer, @@ -30,11 +43,60 @@ # ============================================================================ +class ModuleErrorType(ModuleType): + """Base class for module errors.""" + + +class ModuleNotFoundType(ModuleErrorType): + """Module proxy to handle missing modules cleanly. + + Args: + name: The name of the missing module. + """ + + __file__ = empty_module.__file__ + __path__ = [os.path.dirname(empty_module.__file__)] + + def __init__(self, name: str): + super().__init__(name) + self.__doc__ = empty_module.__doc__ + + +class ModuleInternalErrorType(ModuleErrorType): + """Module proxy to handle module erros at import (error in the module). + + Args: + test_package: The test package from which the import was tried. + path: The path of the errored module. + error: The error message. + """ + + def __init__(self, test_package: ModuleType, path: str, error: str): + test_pkg_file = test_package.__file__ + if test_pkg_file is None: + raise ValueError( + "Attribute test_package.__file__ is None instead of a str path." + ) + test_package_path = os.path.dirname(os.path.realpath(test_pkg_file)) + name = os.path.relpath(path, test_package_path) + subpkgname = test_package.__name__ + if len(name.split(os.sep)) > 1: + subpkgname += "." + ".".join(name.split(os.sep)).rstrip(".py") + super().__init__(subpkgname) + self.__file__ = path + self.__path__ = [os.path.dirname(path)] + self.__doc__ = ( + _("This package encountered the following error during import:\n%s") % error + ) + + # @xxx.register class Module: """ """ def __init__(self, module: ModuleType): + if module.__name__ in sys.modules and not isinstance(module, ModuleErrorType): + module = importlib.reload(module) self.module = module def __copy__(self): @@ -71,28 +133,38 @@ def path(self) -> str: return self.module.__path__[0] @property - def doc(self) -> Optional[str]: - if self.module.__doc__ is None: - return None - return self.module.__doc__.strip() + def doc(self) -> str: + return self.module.__doc__ or "" @property - def root_path(self) -> Optional[str]: + def author(self) -> str: + try: + return metadata.metadata(self.module.__name__)["Author"] or "" + except metadata.PackageNotFoundError as e: + print(e) + return "" + + @property + def root_path(self) -> str: path = self.module.__file__ if path is not None: - if os.path.basename(path) == "__init__.py": - path = os.path.join(path, "..") - path = os.path.abspath(os.path.join(path, "..")) - - return path + # if os.path.basename(path) == "__init__.py": + # path = os.path.join(path, "..") + return os.path.abspath(os.path.join(path, "..")) + else: + return os.path.join(*self.module.__path__) @classmethod def __deserialize__(cls, obj: str) -> "Module": try: return cls(sys.modules[obj]) except KeyError: - __import__(obj) - return cls(sys.modules[obj]) + try: + __import__(obj) + return cls(sys.modules[obj]) + except ModuleNotFoundError as e: + print(e) + return cls(ModuleNotFoundType(obj)) class ModuleSerializer(ValueSerializerBase[Module, str]): @@ -123,11 +195,26 @@ class StatusEnum(Enum): class ResultEnum(Enum): """Results value for a test.""" - ACCEPTED = "accepted" - ACCEPTED_WITH_RESERVES = "accepted with reserves" - SKIPPED = "skipped" - REJECTED = "rejected" - NO_RESULT = "no result" + ACCEPTED = "accepted", "green-check-square.png" + ACCEPTED_WITH_RESERVES = "accepted with reserves", "yellow-check-square.png" + SKIPPED = "skipped", "skip.png" + REJECTED = "rejected", "rejected.png" + NO_RESULT = "no result", "unknown.png" + + icon_path: str + + def __new__(cls, label: str, icon_name: Optional[str] = None): + obj = object.__new__(cls) + obj._value_ = label + obj.icon_path = get_image_file_path(icon_name or "") + return obj + + def __init__(self, label: str, __ignored=None) -> None: ... + + """Fake init method used to get the correct linting/auto-completion.""" + + def format(self) -> str: + return self.name.replace("_", " ") # ============================================================================ @@ -151,6 +238,10 @@ class TestResult: error_code: Optional[int] = None error_msg: str = "" + def __post_init__(self): + if isinstance(self.last_run, str): + self.last_run = datetime.strptime(self.last_run, "%d/%m/%y %H:%M:%S.%f") + @property def result_name(self) -> str: return self.result.name.replace("_", " ") @@ -160,6 +251,9 @@ def status_name(self) -> str: return self.status.name.replace("_", " ") +FormatArgsType = Tuple[str, ...] + + @DataclassSerializer.register @dataclass class Test: @@ -169,9 +263,11 @@ class Test: description: str = "" result: Optional[TestResult] = None command_args: str = "" - command_timeout: int = 86400 + command_timeout: int = 0 run_opts: List[str] = field(default_factory=list) is_valid: bool = True + _is_new_message: bool = False + _is_new_error: bool = False _end_time: float = 0 _is_running: bool = False _forced: bool = False @@ -181,6 +277,8 @@ class Test: _tf: float = 0 _command: str = "" _is_stopped: bool = False + _cached_description: Dict[FormatArgsType, str] = field(default_factory=dict) + _max_cache_size = 10 def __post_init__(self): if self.description == "": @@ -196,6 +294,7 @@ def end_time(self, end_time): @property def command(self): + self._command = self.build_command() return self._command def is_visible(self): @@ -210,17 +309,31 @@ def is_skipped(self): def set_skipped(self, is_skipped): self._is_skipped = is_skipped + def is_message_new(self): + return self._is_new_message + + def is_error_new(self): + return self._is_new_error + + def set_error_state(self, is_new: bool): + self._is_new_error = is_new + + def set_message_state(self, is_new: bool): + self._is_new_message = is_new + def __enter__(self): """ """ def __exit__(self, _type, _value, _traceback): """ """ - forced = self._end_time > self._tf + forced = self._end_time is None or self._end_time > self._tf if not forced: self.stop(False) - assert self.result is not None + assert self.result is not None and self._proc is not None - self.result.execution_duration = time.time() - (self._tf - self.command_timeout) + self.result.execution_duration = round( + time.time() - (self._tf - self.command_timeout), 2 + ) self.result.error_code = self._proc.returncode self.result.last_run = datetime.now() @@ -242,42 +355,64 @@ def stop(self, forced: bool = False): if self._proc is not None and self._is_running: self._forced = forced if forced: - self._proc.send_signal(signal.CTRL_BREAK_EVENT) + if sys.platform == "win32": + self._proc.send_signal(signal.CTRL_BREAK_EVENT) + elif sys.platform == "linux": + os.killpg(self._proc.pid, signal.SIGKILL) self._is_stopped = True self._is_running = False else: self.wait_kill() self._is_running = False + def build_command(self) -> str: + """Builds the command to run the test. + + Returns: + Command line as a string + """ + command = [ + sys.executable, + "-u", + "-X", + "utf8", + f"{self.package.module.__file__}", + ] + + if self.command_args: + command.extend(shlex.split(self.command_args)) + self._tf = time.time() + self.command_timeout + + return shlex.join(command).replace("'", '"') + def run(self): """Runs test""" if self._proc is None: self._is_stopped = False - self._end_time = None + self._end_time = 0 os.environ["PYTHONPATH"] = os.pathsep.join(sys.path) - path = self.package.module.__file__ if self.result is None: self.result = TestResult(StatusEnum.NOT_EXECUTED) self.result.error_msg = "" self.result.output_msg = "" - command = [sys.executable, "-u", "-X", "utf8", f"{path}"] - - if self.command_args: - command.append(self.command_args) - self._tf = time.time() + self.command_timeout - - self._command = shlex.join(command).replace("'", '"') + self._command = self.build_command() self._proc = subprocess.Popen( - command, + self._command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - creationflags=subprocess.CREATE_NEW_PROCESS_GROUP, + creationflags=( + subprocess.CREATE_NEW_PROCESS_GROUP + if sys.platform == "win32" + else 0 + ), bufsize=1, universal_newlines=True, + encoding="utf-8", + preexec_fn=os.setpgrp if sys.platform == "linux" else None, ) self._is_running = True else: @@ -293,7 +428,7 @@ def is_running(self) -> bool: """ """ return ( self._proc is not None - and time.time() < self._tf + and (self.command_timeout <= 0 or time.time() < self._tf) and self._proc.returncode is None and not self._is_stopped ) @@ -301,17 +436,13 @@ def is_running(self) -> bool: def communicate(self, timeout: float = 1): """ """ if self._proc is not None and self.result is not None: - try: + with contextlib.suppress(subprocess.TimeoutExpired): last_outs, last_errs = self._proc.communicate(timeout=timeout) - # self.result.output_msg += last_outs.decode("utf-8") - # self.result.error_msg += last_errs.decode("utf-8") - - self.result.output_msg += last_outs - self.result.error_msg += last_errs - - except subprocess.TimeoutExpired: - pass + if last_outs is not None: + self.result.output_msg += last_outs + if last_errs is not None: + self.result.error_msg += last_errs else: raise subprocess.SubprocessError("No subprocess running.") @@ -321,25 +452,147 @@ def wait_kill(self): if self._proc is not None: self._proc.kill() while self._proc is not None and self._proc.returncode is None: - try: + with contextlib.suppress(subprocess.TimeoutExpired): self.communicate(timeout=0.5) - except subprocess.TimeoutExpired: - pass - def get_description(self) -> Optional[str]: - return self.package.doc + def get_description(self) -> str: + return self.package.doc or "" + + def get_fmt_description( + self, fmt: str, extra_args: Optional[List[str]] = None, use_cached: bool = True + ) -> str: + """Get the description of the test in the specified format. + + Args: + fmt: format to convert the docstring into. + extra_args: Extra Pandoc args. Defaults to None. + use_cached: Use a cached version of the formated description. + Defaults to True. + + Returns: + The description of the test in the specified format. + """ + if extra_args is None: + extra_args = [] + extra_args.append(f"--resource-path={self.package.root_path}") + fmt_args = (fmt, *extra_args) + doc = self._cached_description.get(fmt_args, None) if use_cached else None + if doc is None: + doc = pypandoc.convert_text( + self.get_description() or "", + fmt, + format=config.PACKAGE_CONF["general"].docstring_fmt, + extra_args=extra_args, + ) + self._cached_description[fmt_args] = doc + return doc + + def _get_pandoc_extra_args( + self, + standalone=False, + embeded=True, + shift_header=0, + apply_style: bool = False, + quiet=True, + ) -> List[str]: + """Simply computes a list of extra arguments for pandoc. + + Args: + standalone: Generate a standalone document. Defaults to False. + embeded: Embed all ressources in the document. Defaults to True. + shift_header: Shift header level by specified value. Defaults to 0. + apply_style: Apply the given styles from the css file given in config. + Defaults to False. + quiet: Flag for pandoc verbose level. Defaults to True. + + Returns: + List of extra arguments for pandoc. + """ + extra_args = [] + if standalone: + extra_args.append("--standalone") + if embeded: + extra_args.append("--embed-resources") + if shift_header != 0: + extra_args.append(f"--shift-heading-level-by={shift_header}") + if apply_style: + extra_args.append(f"--css={config.PACKAGE_CONF['export'].get_css_style()}") + if quiet: + extra_args.append("--quiet") + return extra_args + + def get_html_description( + self, + standalone=False, + embeded=True, + shift_header=0, + apply_style: bool = False, + use_cached: bool = True, + ) -> str: + """Get the description of the test in HTML format. + + Args: + standalone: Generate a standalone document. Defaults to False. + embeded: Embed all ressources in the document. Defaults to True. + shift_header: Shift header level by specified value. Defaults to 0. + apply_style: Apply the given styles from the css file given in config. + Defaults to False. + use_cached: Use a cached version of the formated description. + Defaults to True. + + Returns: + The description of the test in HTML format. + """ + extra_args = self._get_pandoc_extra_args( + standalone, embeded, shift_header, apply_style, True + ) + return self.get_fmt_description( + "html", extra_args=extra_args, use_cached=use_cached + ) + + def get_txt_description( + self, standalone=False, embeded=True, shift_header=0 + ) -> str: + """Get the description of the test in plain text format. + + Args: + standalone: Generate a standalone document. Defaults to False. + embeded: Embed all ressources in the document. Defaults to True. + shift_header: Shift header level by specified value. Defaults to 0. + + Returns: + The description of the test in plain text format. + """ + extra_args = self._get_pandoc_extra_args(standalone, embeded, shift_header) + + return self.get_fmt_description("plain", extra_args=extra_args) + + def get_md_description(self, standalone=False, embeded=True, shift_header=0) -> str: + """Get the description of the test in markdown format. + + Args: + standalone: Generate a standalone document. Defaults to False. + embeded: Embed all ressources in the document. Defaults to True. + shift_header: Shift header level by specified value. Defaults to 0. + + Returns: + The description of the test in markdown format. + """ + extra_args = self._get_pandoc_extra_args(standalone, embeded, shift_header) + + return self.get_fmt_description("md", extra_args=extra_args) def get_images(self, image_dirs: List[str]) -> List[str]: """ """ return get_image_path(self.package.last_name, image_dirs) - def retrieve_category(self, test_package: Module): + def retrieve_category(self, test_package: ModuleType): path = self.package.module.__file__ test_module = TestModule(test_package, path) self.set_visible(test_module.is_visible()) self.set_skipped(test_module.is_skipped()) - def get_code_snippet(self, test_package: Module): + def get_code_snippet(self, test_package: ModuleType): path = self.package.module.__file__ test_module = TestModule(test_package, path) @@ -355,6 +608,21 @@ def build_from_test_module(cls, test_module: TestModule) -> "Test": test._is_visible = test_module.is_visible() return test + def result_binary_label(self) -> Tuple[int, ...]: + """Computes the binary label of the result. + + Returns: + Tuple of int, 1 if the result is the same as the test result, 0 otherwise. + """ + return tuple( + map( + lambda res: ( + 1 if (self.result is not None and self.result.result is res) else 0 + ), + ResultEnum, + ) + ) + @DataclassSerializer.register @dataclass @@ -366,29 +634,92 @@ class TestSuite: description: str = "" last_run: Optional[datetime] = None - tests: Optional[List[Test]] = None - _category: str = "all" + tests: List[Test] = field(default_factory=list) + + _category: str = config.PACKAGE_CONF["general"].category _running_test: Optional[Test] = None + def __post_init__(self) -> None: + if len(self.tests) == 0: + self.reset() + self.author = self.package.author + self.description = self.package.doc + config.load_package_conf(self.package.root_path) + + def get_fmt_description(self, fmt="html", extra_args=None): + """Return the description of the test suite in the specified format. + + Args: + fmt: format to convert the docstring into. Defaults to "html". + extra_args: Extra Pandoc args. Defaults to None. + + Returns: + The description of the test suite in the specified format. + """ + if extra_args is None: + extra_args = [] + extra_args.append(f"--resource-path={self.package.root_path}") + return pypandoc.convert_text( + self.description or "", + fmt, + format=config.PACKAGE_CONF["general"].docstring_fmt, + extra_args=extra_args, + ) + @property - def package_name(self): + def package_name(self) -> str: return self.package.module.__name__ @property - def running_test(self): + def running_test(self) -> Optional[Test]: return self._running_test - def __post_init__(self): - if self.tests is None: - self.tests = [] - self.reset() - - def reset(self): + def reset(self) -> None: """category must be "all", "visible", or "batch".""" self.tests.clear() for test_module in get_tests(self.package.module, self._category): + if not test_module.is_valid(): + test_module.module = self.load_errored_test_module(test_module) + test = Test.build_from_test_module(test_module) + self.tests.append(test) + + def refresh_package(self, category: Optional[str] = None) -> None: + """Refresh the package and its tests. + + Args: + category: category must be "all", "visible", or "batch". Defaults to None. + """ + exising_tests: Dict[str, Test] = { + test.package.full_name: test for test in self.tests + } + self.tests.clear() + category = category or self._category + for test_module in get_tests(self.package.module, category): + if not test_module.is_valid(): + test_module.module = self.load_errored_test_module(test_module) test = Test.build_from_test_module(test_module) + if old_test := exising_tests.get(test.package.full_name, None): + test.result = old_test.result + test.command_args = old_test.command_args + test.command_timeout = old_test.command_timeout + test.run_opts = old_test.run_opts + self.tests.append(test) + self.__post_init__() + + def load_errored_test_module( + self, test_module: TestModule + ) -> ModuleInternalErrorType: + """Load the errored test module. + + Args: + test_module: The test module that errored during import. + + Returns: + The errored test module wrapped in a ModuleType proxy class. + """ + package = get_test_package(self.package.module) + return ModuleInternalErrorType(package, test_module.path, test_module.error_msg) # Run related methods def run( @@ -397,7 +728,7 @@ def run( pattern: str = "", timeout: Optional[int] = None, test_args: Optional[str] = None, - ): + ) -> None: """""" assert self.tests self.last_run = datetime.now() @@ -411,18 +742,20 @@ def run( self._running_test = test with test.start(): - while self.running_test.is_running(): - self.running_test.communicate(0.5) - self.running_test.end_time = time.time() + while test.is_running(): + test.communicate(0.5) + # Kills the test if still running, otherwise the process could keep + # running in the background + test.stop() + test.end_time = time.time() self._running_test = None - def terminate_run(self): + def terminate_run(self) -> None: pass def should_run(self, test: Test, category: str = "all", pattern: str = "") -> bool: package = test.package called = self.is_called(package, pattern) - is_valid = ( category == "all" or (category == "visible" and test.is_visible()) @@ -432,8 +765,9 @@ def should_run(self, test: Test, category: str = "all", pattern: str = "") -> bo return is_valid and called def is_called(self, package: Module, pattern: str = "") -> bool: - path = str(package.module.__file__) - if pattern in ("", "*") or pattern in path or pattern in package.full_name: + # path = str(package.module.__file__) + # if pattern in ("", "*") or pattern in path or pattern in package.full_name: + if pattern in ("", "*") or pattern == package.last_name: return True return False @@ -446,3 +780,47 @@ def group_tests(self) -> Dict[str, List[Test]]: diff_path[str(test.package.module.__package__)].append(test) return diff_path + + def get_missing_modules(self) -> List[Module]: + """Get the list of missing modules. + + Returns: + List of missing modules. + """ + missing_modules_list = [ + test.package + for test in self.tests + if isinstance(test.package.module, ModuleNotFoundType) + ] + return missing_modules_list + + def get_errored_modules(self) -> list[Module]: + """Get the list of errored modules (for which the import failed). + + Returns: + List of errored modules. + """ + error_modules_list = [ + test.package + for test in self.tests + if isinstance(test.package.module, ModuleInternalErrorType) + ] + return error_modules_list + + def results_binary_labels(self) -> List[Tuple[int, ...]]: + """Computes the binary labels of the results. + + Returns: + List of tuples of int, 1 if the result is the same as the test result, + 0 otherwise. + """ + return [test.result_binary_label() for test in self.tests] + + def results_count(self) -> Tuple[int, ...]: + """Computes the of test result by result kind. + + Returns: + Tuple of int, the count of test result by result kind. + """ + results = self.results_binary_labels() + return tuple(map(sum, zip(*results))) diff --git a/moduletester/module_not_found.py b/moduletester/module_not_found.py new file mode 100644 index 0000000..e2540f8 --- /dev/null +++ b/moduletester/module_not_found.py @@ -0,0 +1,14 @@ +"""This module is empty and is used as a placeholder for moduletester when importing +the .moduletester file and a test sub-module is not found. + +Pleaser, reload update the moduletester files to remove this sub-module or create a +new moduletester file. +""" + +# guitest: show + +if __name__ == "__main__": + raise ModuleNotFoundError( + "ModuleTester did not find this module in your package, " + "please update the .moduletester file." + ) diff --git a/moduletester/new_exporter.py b/moduletester/new_exporter.py new file mode 100644 index 0000000..3185e05 --- /dev/null +++ b/moduletester/new_exporter.py @@ -0,0 +1,305 @@ +from __future__ import annotations + +import os +import threading +from abc import ABC +from dataclasses import dataclass, field +from typing import Callable, Iterable, Optional + +import pypandoc +from jinja2 import Environment, FileSystemLoader, Template, select_autoescape + +from moduletester import config +from moduletester.config import _ + +_export_conf = config.PACKAGE_CONF["export"] + +DEFAULT_TEMPLATE_LOADER = FileSystemLoader(searchpath=_export_conf.template_dir) +JINJA_ENV = Environment( + loader=DEFAULT_TEMPLATE_LOADER, + autoescape=select_autoescape(["html", "xml"]), + trim_blocks=True, + lstrip_blocks=True, +) +JINJA_ENV.globals["_"] = _ + +FMT_TO_EXTENSION = { + "html": "html", + "docx": "docx", + "odt": "odt", + "rst": "rst", + "md": "md", + "markdown": "md", + "markdown_mmd": "md", + "markdown_github": "md", + "markdown_strict": "md", + "pdf": "pdf", + "latex": "tex", + "tex": "tex", +} + +DEFAULT_TEMPLATE_NAME = _export_conf.test_results_template_name +DEFAULT_DOCX_REFERENCE = _export_conf.get_docx_ref() + +DEFAULT_ODT_REFERENCE = _export_conf.get_odt_ref() + +DEFAULT_CSS_STYLE = _export_conf.get_css_style() + +DEFAULT_HEADER_SHIFT = _export_conf.docstrings_header_shift + +DEFAULT_TOC_DEPTH = _export_conf.toc_depth + + +@dataclass +class DocumentExporter(ABC): + """BaseClass for document exportation. To define specific documents, you should + inherit from this class and define the template_name attribute. + If the template_name is the default one, the class will search for a template + corresponding to [class name]_template.j2 in the template directory. + If the template is found, it will be loaded and used as""" + + template_name: str = DEFAULT_TEMPLATE_NAME + reload_template: bool = False + resource_path: Optional[str] = None + docstrings_header_shift = DEFAULT_HEADER_SHIFT + toc_depth = DEFAULT_TOC_DEPTH + _template: Template = field( + init=False, default=JINJA_ENV.get_template(template_name) + ) + _docx_reference: str = field(init=False, default=DEFAULT_DOCX_REFERENCE) + _odt_reference: str = field(init=False, default=DEFAULT_ODT_REFERENCE) + _css_style: str = field(init=False, default=DEFAULT_CSS_STYLE) + + def render_html(self, with_toc=False) -> str: + """Render the document as html with or without a table of content. + + Args: + with_toc: Insert a table of content. Defaults to False. + + Returns: + generated html string + """ + extra_args = [ + "--embed-resources", + "--standalone", + f"--css={self._css_style}", + ] + if with_toc: + extra_args.extend(["--toc", f"--toc-depth={self.toc_depth}"]) + + if self.reload_template: + self._template = JINJA_ENV.get_template(self.template_name) + + if self.resource_path is not None: + extra_args.append(f"--resource-path={self.resource_path}") + + return pypandoc.convert_text( + self._template.render(doc_obj=self), + to="html", + format="html", + extra_args=extra_args, + ) + + def export( + self, filename: str, fmt="html", extra_args: Optional[list[str]] = None + ) -> None: + """Export the document to a file in the specified format. + + Args: + filename: The name of the file to write to. + fmt: The format to export to. Defaults to "html". + extra_args: Additional arguments to pass to pandoc. Defaults to None. + """ + if extra_args is None: + extra_args = [] + if fmt == "html": + self.export_html(filename) + return + + pypandoc.convert_text( + self.render_html(), + fmt, + format="html", + outputfile=filename, + extra_args=extra_args, + ) + + def export_html(self, filename: str) -> None: + """Export the document to a file in html format. + + Args: + filename: The name of the file to write to. + """ + with open(filename, "w", encoding="utf-8") as f: + d = self.render_html(with_toc=True) + f.write(d) + + def export_docx(self, filename: str) -> None: + """Export the document to a file in docx format. + + Args: + filename: The name of the file to write to. + """ + extra_args = [ + f"--reference-doc={self._docx_reference}", + "--toc", + f"--toc-depth={self.toc_depth}", + ] + + self.export(filename, "docx", extra_args=extra_args) + + def export_odt(self, filename: str) -> None: + """Export the document to a file in odt format. + + Args: + filename: The name of the file to write to. + """ + extra_args = [ + f"--reference-doc={self._odt_reference}", + "--toc", + f"--toc-depth={self.toc_depth}", + ] + self.export(filename, "odt", extra_args=extra_args) + + def export_rst(self, filename: str) -> None: + """Export the document to a file in rst format. + + Args: + filename: The name of the file to write to. + """ + self.export(filename, "rst") + + def export_md(self, filename: str) -> None: + """Export the document to a file in markdown format. + + Args: + filename: The name of the file to write to. + """ + self.export( + filename, + "markdown-raw_html-native_divs-native_spans-fenced_divs-bracketed_spans-escaped_line_breaks", + ) + + def export_pdf(self, filename: str) -> None: + """Export the document to a file in pdf format. + + Args: + filename: The name of the file to write to. + """ + self.export( + filename, + "pdf", + extra_args=[ + "--pdf-engine=xelatex", + "--toc", + f"--toc-depth={self.toc_depth}", + f"--css={self._css_style}", + ], + ) + + def _export_with_callback( + self, + filename: str, + fmt: str, + extra_args: Optional[list[str]], + callback: Optional[Callable[[], None]] = None, + ) -> None: + """Export the document to a file in the specified format and call the callback + function when the export is complete. + + Args: + filename: The name of the file to write to. + fmt: The format to export to. + extra_args: Additional arguments to pass to pandoc. + callback: The function to call when the export is complete. + """ + if extra_args is None: + extra_args = [] + self.export(filename, fmt, extra_args=extra_args) + if callback is not None: + callback() + + def export_docx_async( + self, filename: str, callback: Optional[Callable[[], None]] = None + ) -> None: + """Export the document to a file in docx format in a separate thread. + + Args: + filename: The name of the file to write to. + callback: Callback to call once the export is finished. Defaults to None. + """ + extra_args = [ + f"--reference-doc={self._docx_reference}", + "--toc", + f"--toc-depth={self.toc_depth}", + ] + t = threading.Thread( + target=self._export_with_callback, + args=(filename, "docx", extra_args, callback), + ) + t.start() + + def export_html_async( + self, filename: str, callback: Optional[Callable[[], None]] = None + ) -> None: + """Export the document to a file in html format in a separate thread. + + Args: + filename: The name of the file to write to. + callback: Callback to call once the export is finished. Defaults to None. + """ + threading.Thread( + target=self._export_with_callback, args=(filename, "html", None, callback) + ).start() + + def multi_exports(self, fmts: Iterable[str], basename: str): + """Export the document to multiple formats successively. + + Args: + fmts: The formats to export to. + basename: The base name of the files to write to. + + Raises: + ValueError: If an unknown format is specified. + """ + for fmt in fmts: + if fmt not in FMT_TO_EXTENSION: + raise ValueError(f"Unknown format {fmt}") + export_method = getattr(self, f"export_{fmt}") + export_method(f"{basename}.{fmt}") + print(f"Export of {basename}.{fmt} complete") + + def multi_exports_async( + self, + fmts: Iterable[str], + basename: str, + callback: Optional[Callable[[], None]] = None, + ): + """Export the document to multiple formats successively in a separate thread. + + Args: + fmts: The formats to export to. + basename: The base name of the files to write to. + callback: Callback to call once the export is finished. Defaults to None. + """ + + def _multi_exports_async( + fmts: Iterable[str], basename: str, callback: Optional[Callable[[], None]] + ): + self.multi_exports(fmts, basename) + if callback is not None: + callback() + + threading.Thread( + target=_multi_exports_async, args=(fmts, basename, callback) + ).start() + + @classmethod + def __init_subclass__(cls) -> None: + + if os.path.exists(os.path.join(_export_conf.template_dir, cls.template_name)): + cls.load_template() + + @classmethod + def load_template(cls) -> None: + cls._template = JINJA_ENV.get_template(cls.template_name) diff --git a/moduletester/python_helpers.py b/moduletester/python_helpers.py index a1e6da5..14b58c7 100644 --- a/moduletester/python_helpers.py +++ b/moduletester/python_helpers.py @@ -6,7 +6,7 @@ import sys from dataclasses import fields, is_dataclass from itertools import zip_longest -from typing import Any, Dict, Generator, List, Protocol, Tuple +from typing import Any, Dict, Generator, List, Protocol, Tuple, overload from bs4 import BeautifulSoup @@ -27,8 +27,7 @@ def get_original_bases(cls): class SupportsWrite(Protocol): """ """ - def write(self, text: str) -> Any: - ... + def write(self, text: str, /) -> Any: ... # ============================================================================ @@ -155,7 +154,7 @@ def exec_rst( print(outs, errs) -def parse_html(html_path: str, dtv_path: str): +def parse_html(html_path: str, test_list_path: str): """Removes the navbar from the index.html file""" soup = None with open(html_path, "r", encoding="utf-8") as html_doc: @@ -169,7 +168,7 @@ def parse_html(html_path: str, dtv_path: str): if footer is not None: footer.replaceWith("") - with open(dtv_path, "w", encoding="utf-8") as html_doc: + with open(test_list_path, "w", encoding="utf-8") as html_doc: html_doc.write(soup.prettify()) @@ -212,7 +211,7 @@ def get_image_path(file_name, dirs): def rst2odt(source: str, dest: str): """ """ python = sys.executable - script = os.path.join(sys.base_prefix, "Scripts", "rst2odt.py") + script = os.path.join(sys.prefix, "Scripts", "rst2odt.py") proc = subprocess.Popen(" ".join([python, script, source, dest])) @@ -226,7 +225,7 @@ def rst2odt(source: str, dest: str): def rst2html(source: str, dest: str): """ """ python = sys.executable - script = os.path.join(sys.base_prefix, "Scripts", "rst2html.py") + script = os.path.join(sys.prefix, "Scripts", "rst2html.py") proc = subprocess.Popen(" ".join([python, script, source, dest])) diff --git a/moduletester/serializer.py b/moduletester/serializer.py index 49cfd2d..804de95 100644 --- a/moduletester/serializer.py +++ b/moduletester/serializer.py @@ -1,5 +1,7 @@ # pylint: disable=empty-docstring, missing-class-docstring, # pylint: disable=missing-function-docstring, missing-module-docstring +from __future__ import annotations + import json from abc import ABC, abstractmethod from dataclasses import asdict, fields, is_dataclass @@ -30,12 +32,10 @@ def register_data_type( cls.TYPES[data_type] = serializer or cls() @abstractmethod - def serialize(self, obj: ISerializerT) -> IJsonT: - ... + def serialize(self, obj: ISerializerT) -> IJsonT: ... @abstractmethod - def deserialize(self, obj: IJsonT) -> ISerializerT: - ... + def deserialize(self, obj: IJsonT) -> ISerializerT: ... class ObjectSerializerBase(IJSONSerializer[ISerializerT, Dict[str, Any]]): @@ -171,6 +171,6 @@ def dumper(path: str, obj: Any) -> None: ObjectSerializerBase.dump(obj, output) -def loader(path: str) -> Any: - with open(path, encoding="utf-8") as obj: +def loader(path: str) -> Any | None: + with open(path, "rb") as obj: return ObjectSerializerBase.load(obj) diff --git a/moduletester/test_exporter.py b/moduletester/test_exporter.py new file mode 100644 index 0000000..30d8e22 --- /dev/null +++ b/moduletester/test_exporter.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Optional + +from jinja2 import FileSystemLoader + +from moduletester import config +from moduletester.new_exporter import JINJA_ENV, DocumentExporter + +from .model import Test, TestSuite + + +@dataclass +class _TestExporter(DocumentExporter): + test_suite: Optional[TestSuite] = None + _image_dirs: list[str] = field(init=False, default_factory=list) + + def __post_init__(self): + global JINJA_ENV + conf = config.PACKAGE_CONF["export"] + # if self.test_suite is not None: + # if ( + # templ := getattr( + # conf, f"{self.__class__.__name__.lower()}_template_name", None + # ) + # ) is not None and self.template_name != templ: + # new_template_loader = FileSystemLoader(searchpath=conf.template_dir) + # JINJA_ENV.loader = new_template_loader + # self.template_name = templ + # self._template = JINJA_ENV.get_template(self.template_name) + # else: + # raise ValueError( + # "Configuration is missing key " + # f"{self.__class__.__name__.lower()}_template_name.\n" + # "Update file config.py and moduletester.ini to add the key." + # ) + + self._docx_reference = conf.get_docx_ref() + self._odt_reference = conf.get_odt_ref() + self._css_style = conf.get_css_style() + self.docstrings_header_shift = conf.docstrings_header_shift + self.toc_depth = conf.toc_depth + self.resource_path = self.test_suite.package.path + + # Updating the template directory in the jinja environment + new_template_loader = FileSystemLoader(searchpath=conf.template_dir) + JINJA_ENV.loader = new_template_loader + + def get_images_paths(self, test: Test) -> list[str]: + if self.test_suite is None: + return [] + return test.get_images(self._image_dirs) + + +@dataclass +class TestResultsDocument(_TestExporter): + template_name: str = "test_results_template.j2" + + +@dataclass +class TestListDocument(_TestExporter): + template_name: str = "test_list_template.j2" diff --git a/pyproject.toml b/pyproject.toml index cff57e6..5aa784b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,15 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] requires-python = ">=3.8, <4" -dependencies = ["guidata >= 3.1", "QtPy >= 1.9", "beautifulsoup4", "click"] +dependencies = [ + "guidata >= 3.3", + "QtPy >= 1.9", + "click", + "pyqtwebengine", + "pypandoc", + "jinja2", + "beautifulsoup4", +] dynamic = ["version"] [project.urls] @@ -37,7 +45,7 @@ Documentation = "https://moduletester.readthedocs.io/en/latest/" moduletester-cli = "moduletester.manager:cli" [project.gui-scripts] -moduletester = "moduletester.gui:run" +moduletester = "moduletester.gui.main:run_gui" [project.optional-dependencies] dev = ["black", "isort", "pylint", "Coverage"] @@ -47,7 +55,17 @@ doc = ["PyQt5", "sphinx>6", "pydata_sphinx_theme"] include = ["moduletester*"] [tool.setuptools.package-data] -"*" = ["*.svg", "*.mo", "*.txt", "*.json", "*.png"] +"*" = [ + "*.svg", + "*.mo", + "*.txt", + "*.json", + "*.png", + "*.j2", + "*.docx", + "*.odt", + "*.css", +] [tool.setuptools.dynamic] version = { attr = "moduletester.__version__" } diff --git a/requirements.txt b/requirements.txt index 462a1a7..54bfee5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,6 @@ pyinstaller sphinx pydata_sphinx_theme build -twine \ No newline at end of file +twine +pypandoc +jinja2 \ No newline at end of file From 4ad770856deb600cf8c2bc75e3c463a3550de6af Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Wed, 20 May 2026 16:08:02 +0200 Subject: [PATCH 2/9] add default templates and example --- .../default_templates/custom-reference.docx | Bin 0 -> 21744 bytes .../default_templates/custom-reference.odt | Bin 0 -> 9589 bytes .../default_templates/default_style.css | 408 ++ .../default_templates/default_template.j2 | 10 + .../default_templates/test_list_template.j2 | 39 + .../test_results_template.j2 | 116 + test_results_exemple.html | 3920 +++++++++++++++++ 7 files changed, 4493 insertions(+) create mode 100644 moduletester/default_templates/custom-reference.docx create mode 100644 moduletester/default_templates/custom-reference.odt create mode 100644 moduletester/default_templates/default_style.css create mode 100644 moduletester/default_templates/default_template.j2 create mode 100644 moduletester/default_templates/test_list_template.j2 create mode 100644 moduletester/default_templates/test_results_template.j2 create mode 100644 test_results_exemple.html diff --git a/moduletester/default_templates/custom-reference.docx b/moduletester/default_templates/custom-reference.docx new file mode 100644 index 0000000000000000000000000000000000000000..045165cc66d715de1b1584aacc9878d01ce1a9cb GIT binary patch literal 21744 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 literal 0 HcmV?d00001 diff --git a/moduletester/default_templates/custom-reference.odt b/moduletester/default_templates/custom-reference.odt new file mode 100644 index 0000000000000000000000000000000000000000..63ad93d59d9caddaf967061fc30893685f009423 GIT binary patch literal 9589 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 YRpj9y>L&n169o^@gETPVsQ(`QFAYw&ssI20 literal 0 HcmV?d00001 diff --git a/moduletester/default_templates/default_style.css b/moduletester/default_templates/default_style.css new file mode 100644 index 0000000..bb40de9 --- /dev/null +++ b/moduletester/default_templates/default_style.css @@ -0,0 +1,408 @@ +/* + * I add this to html files generated with pandoc. + */ + +html { + font-size: 100%; + overflow-y: scroll; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + color: #444; + font-family: Georgia, Palatino, 'Palatino Linotype', Times, 'Times New Roman', serif; + font-size: 12px; + line-height: 1.7; + padding: 2em; + margin: auto; + background: #fefefe; +} + +a { + color: #0645ad; + text-decoration: none; +} + +a:visited { + color: #0b0080; +} + +a:hover { + color: #06e; +} + +a:active { + color: #faa700; +} + +a:focus { + outline: thin dotted; +} + +*::-moz-selection { + background: rgba(255, 255, 0, 0.3); + color: #000; +} + +*::selection { + background: rgba(255, 255, 0, 0.3); + color: #000; +} + +a::-moz-selection { + background: rgba(255, 255, 0, 0.3); + color: #0645ad; +} + +a::selection { + background: rgba(255, 255, 0, 0.3); + color: #0645ad; +} + +p { + margin: 1em 0; +} + +img { + max-width: 100%; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: #111; + line-height: 125%; + margin-top: 2em; + font-weight: normal; +} + +h4, +h5, +h6 { + font-weight: bold; +} + +h1 { + font-size: 2.5em; +} + +h2 { + font-size: 2em; +} + +h3 { + font-size: 1.5em; +} + +h4 { + font-size: 1.2em; +} + +h5 { + font-size: 1em; +} + +h6 { + font-size: 0.9em; +} + +blockquote { + color: #666666; + margin: 0; + padding-left: 3em; + border-left: 0.5em #EEE solid; +} + +hr { + display: block; + height: 2px; + border: 0; + border-top: 1px solid #aaa; + border-bottom: 1px solid #eee; + margin: 1em 0; + padding: 0; +} + +pre, +code, +kbd, +samp { + color: #000; + font-family: monospace, monospace; + _font-family: 'courier new', monospace; + font-size: 0.98em; +} + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +b, +strong { + font-weight: bold; +} + +dfn { + font-style: italic; +} + +ins { + background: #ff9; + color: #000; + text-decoration: none; +} + +mark { + background: #ff0; + color: #000; + font-style: italic; + font-weight: bold; +} + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +ul, +ol { + margin: 1em 0; + padding: 0 0 0 2em; +} + +li p:last-child { + margin-bottom: 0; +} + +ul ul, +ol ol { + margin: .3em 0; +} + +dl { + margin-bottom: 1em; +} + +dt { + font-weight: bold; + margin-bottom: .8em; +} + +dd { + margin: 0 0 .8em 2em; +} + +dd:last-child { + margin-bottom: 0; +} + +img { + border: 0; + -ms-interpolation-mode: bicubic; + vertical-align: middle; +} + +figure { + display: block; + text-align: center; + margin: 1em 0; +} + +figure img { + border: none; + margin: 0 auto; +} + +figcaption { + font-size: 0.8em; + font-style: italic; + margin: 0 0 .8em; +} + +table { + margin-bottom: 2em; + border-bottom: 1px solid #ddd; + border-right: 1px solid #ddd; + border-spacing: 0; + border-collapse: collapse; + width: 100%; +} + +table th { + padding: .2em 1em; + background-color: #eee; + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; +} + +table td { + padding: .2em 1em; + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; + vertical-align: top; +} + +.author { + font-size: 1.2em; + text-align: center; +} + +@media only screen and (min-width: 480px) { + body { + font-size: 14px; + } +} + +@media only screen and (min-width: 768px) { + body { + font-size: 16px; + } +} + +@media print { + * { + background: transparent !important; + color: black !important; + filter: none !important; + -ms-filter: none !important; + } + + body { + font-size: 12pt; + max-width: 100%; + } + + a, + a:visited { + text-decoration: underline; + } + + hr { + height: 1px; + border: 0; + border-bottom: 1px solid black; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + padding-right: 1em; + page-break-inside: avoid; + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + @page :left { + margin: 15mm 20mm 15mm 10mm; + } + + @page :right { + margin: 15mm 10mm 15mm 20mm; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h2, + h3 { + page-break-after: avoid; + } +} + +/* ADDITIONAL CUSTOM STYLING */ + +.section-block { + border: 1px solid #ccc; + border-radius: 5px; + padding: 1em; + margin: 1em 0; + box-shadow: 5px 5px 10px lightgrey; +} + +.result-block { + display: flex; + align-items: center; +} + +.result-block img { + margin-right: 1em; +} + +.new-page { + page-break-before: always; +} + + +nav#TOC { + border: 1px solid #ccc; + border-radius: 5px; + padding: 1em; + margin: 1em 0; + box-shadow: 5px 5px 10px lightgrey; +} + +/* Common code properties */ +code { + background-color: WhiteSmoke; +} + +/* Code block */ +:not(p):not(td)>code { + box-shadow: 5px 5px 10px lightgrey; + border-radius: 10px; + display: block; + padding: 1em; +} + +/* Inline code */ +p code { + border-radius: 5px; + padding: 0.2em; +} + +td code { + border-radius: 5px; + padding: 0em; +} \ No newline at end of file diff --git a/moduletester/default_templates/default_template.j2 b/moduletester/default_templates/default_template.j2 new file mode 100644 index 0000000..5844163 --- /dev/null +++ b/moduletester/default_templates/default_template.j2 @@ -0,0 +1,10 @@ + + + + + This is an empty template + + + + + \ No newline at end of file diff --git a/moduletester/default_templates/test_list_template.j2 b/moduletester/default_templates/test_list_template.j2 new file mode 100644 index 0000000..dd1cad3 --- /dev/null +++ b/moduletester/default_templates/test_list_template.j2 @@ -0,0 +1,39 @@ + + + + + {{ _("Test List Document") }}: {{ doc_obj.test_suite.package.full_name.capitalize() }} + + + +

+

+

+

{{ _("Last execution date:") }} {{ doc_obj.test_suite.last_run }}

+

Description

+
+ {{ doc_obj.test_suite.get_fmt_description("html").strip() or "No package description found."}}
+ {% for section_name, tests in doc_obj.test_suite.group_tests().items() %} +
+ +

Test sub-module: {{ section_name.rsplit(".", 1)[-1] }}

+ + {% for test in tests %} +
+ {% set test_name_split = test.package.last_name.split("_") %} +

{{test_name_split[0].capitalize()}}: {{" ".join(test_name_split[1:])}}

+
+

Description

+ {{ test.get_html_description(standalone=False, embeded=True, + shift_header=doc_obj.docstrings_header_shift)|safe + }} + +
+
+
+ {% endfor %} +
+ {% endfor %} + + + \ No newline at end of file diff --git a/moduletester/default_templates/test_results_template.j2 b/moduletester/default_templates/test_results_template.j2 new file mode 100644 index 0000000..59241d0 --- /dev/null +++ b/moduletester/default_templates/test_results_template.j2 @@ -0,0 +1,116 @@ + + + + + {{ _("Test Results Document") }}: {{ doc_obj.test_suite.package.full_name.capitalize() }} + + + +

+

{{ doc_obj.test_suite.author or "Unknown author " }}
+

+

{{ _("Last execution date:") }} {{ doc_obj.test_suite.last_run }}

+

Description

+
+ {% set extra_desc_gen_args = ["--shift-heading-level-by={s}".format(s=doc_obj.docstrings_header_shift)] %} + {{ doc_obj.test_suite.get_fmt_description("html", extra_desc_gen_args + ).strip() or "No package description found."}} +
+ {% for section_name, tests in doc_obj.test_suite.group_tests().items() %} +
+ +

Test sub-module: {{ section_name.rsplit(".", 1)[-1] }}

+ + {% for test in tests %} +
+ {% set test_name_split = test.package.last_name.split("_") %} +

{{test_name_split[0].capitalize()}}: {{" ".join(test_name_split[1:])}}

+
+

Description

+ {{ test.get_html_description(standalone=False, embeded=True, + shift_header=doc_obj.docstrings_header_shift)|safe + }} + +
+ +
+

Command

+ {{ test.command or "-" }} +
+ +
+

Result

+ {% if test.result != None %} +
+ +

{{ test.result.result_name }}, executed on {{ + test.result.last_run or "-"}}

+
+ {% else %} +

No result found.

+ {% endif %} +

{{ test.comment|safe }}

+ +
+ {% for image_path in doc_obj.get_images_paths(test) %} + Test Image + {% endfor %} +
+
+
+
+ {% endfor %} +
+ {% endfor %} + +
+
+

Summary

+

{{_("%s test results summary") % doc_obj.test_suite.package.last_name.capitalize() }}

+ + + + + + + + + + + {% for test in doc_obj.test_suite.tests %} + + {% set result_bin = test.result_binary_label() %} + + + + + + + + + {% endfor %} +
Test{{ _("No result") }}{{ _("Accepted") }}{{ _("Accepted with reserve") }}{{ _("Skipped") }}{{ _("Rejected") }}Execution date
{{ test.package.full_name }}{{ "X" if result_bin[0] == 1 else "" }}{{ "X" if result_bin[1] == 1 else "" }}{{ "X" if result_bin[2] == 1 else "" }}{{ "X" if result_bin[3] == 1 else "" }}{{ "X" if result_bin[4] == 1 else "" }}{{ test.result.last_run }}
+

{{_("Count by result cateogry")}}

+ {% set result_count = doc_obj.test_suite.results_count() %} + + + + + + + + + + + + + + + +
{{ _("No result") }}{{ _("Accepted") }}{{ _("Accepted with reserve") }}{{ _("Skipped") }}{{ _("Rejected") }}
{{ result_count[0] }}{{ result_count[1] }}{{ result_count[2] }}{{ result_count[3] }}{{ result_count[4] }}
+
+
+ + + + \ No newline at end of file diff --git a/test_results_exemple.html b/test_results_exemple.html new file mode 100644 index 0000000..a849f2e --- /dev/null +++ b/test_results_exemple.html @@ -0,0 +1,3920 @@ + + + + + + + + + + + + + RTV: exemple_module__test + + + + + + + + + +
+ +

RTV: exemple_module__test

+ +
+ +
+ +
+ + Unknown author + +
+ +

Last execution date: 12/02/24 13:39:52.833739

+ +

Description

+ +
+ + No package description found. + +
+ +
+ +

Test sub-module: benchmarks

+ +
+ +

Test: bigimages

+ +
+ +

Description

+ +

Test showing 10 big images

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\benchmarks\test_bigimages.py" + +
+ +
+ +

Result

+ +
+ + + +

ACCEPTED, executed on 24/01/24 13:16:06.796710

+ +
+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: loadtest

+ +
+ +

Description

+ +

Load test: instantiating a large number of image widgets

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\benchmarks\test_loadtest.py" + +
+ +
+ +

Result

+ +
+ + + +

REJECTED, executed on 24/01/24 11:35:33.697528

+ +
+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +
+ +

Test sub-module: features

+ +
+ +

Data:

+ +
+ +

Description

+ +

pyplot test

+ +

Interactive plotting interface with MATLAB-like syntax

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\data.py" + +
+ +
+ +

Result

+ +
+ + + +

SKIPPED, executed on 24/01/24 14:05:07.607218

+ +
+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Data: 1

+ +
+ +

Description

+ +

pyplot test

+ +

Interactive plotting interface with MATLAB-like syntax

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\data_1.py" + +
+ +
+ +

Result

+ +
+ + + +

NO RESULT, executed on 12/02/24 13:40:03.146832

+ +
+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Data: 2

+ +
+ +

Description

+ +

Resize test: using the scaler C++ engine to resize images

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\data_2.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: autoscale shapes

+ +
+ +

Description

+ +

This example shows autoscaling of plot with various shapes.

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_autoscale_shapes.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: auto curve image

+ +
+ +

Description

+ +

Testing 'auto' plot type

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_auto_curve_image.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: builder

+ +
+ +

Description

+ +

Builder tests

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_builder.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: computations

+ +
+ +

Description

+ +

Plot computations test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_computations.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: contrast

+ +
+ +

Description

+ +

Contrast tool test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_contrast.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: cursors

+ +
+ +

Description

+ +

Horizontal/vertical cursors test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_cursors.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: dicom image

+ +
+ +

Description

+ +

DICOM image test

+ +

Requires pydicom (>=0.9.3)

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_dicom_image.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: fit

+ +
+ +

Description

+ +

Curve fitting tools

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_fit.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: imagefilter

+ +
+ +

Description

+ +

Image filter demo

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_imagefilter.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: imagesuperp

+ +
+ +

Description

+ +

Image superposition test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_imagesuperp.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: loadsaveitems hdf5

+ +
+ +

Description

+ +

Load/save items from/to HDF5 file

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_loadsaveitems_hdf5.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: loadsaveitems json

+ +
+ +

Description

+ +

Unit test for plot items <--> JSON + + serialization/deserialization

+ +

How to save/restore items to/from a JSON string?

+ +
+ +

# Plot items --> JSON: writer = JSONWriter(None) + + save_items(writer, items) text = writer.get_json()

+ +

# JSON --> Plot items: items = load_items(JSONReader(text))

+ +
+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_loadsaveitems_json.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: loadsaveitems pickle

+ +
+ +

Description

+ +

Load/save items using Python's pickle protocol

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_loadsaveitems_pickle.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: manager

+ +
+ +

Description

+ +

PlotManager test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_manager.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: no auto tools

+ +
+ +

Description

+ +

Testing auto_tools plot option

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_no_auto_tools.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: plot log

+ +
+ +

Description

+ +

Logarithmic scale test for curve plotting

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_plot_log.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: plot types

+ +
+ +

Description

+ +

PlotTypes test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_plot_types.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: plot yreverse

+ +
+ +

Description

+ +

Reverse y-axis test for curve plotting

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_plot_yreverse.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: pyplot

+ +
+ +

Description

+ +

pyplot test

+ +

Interactive plotting interface with MATLAB-like syntax

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_pyplot.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: resize

+ +
+ +

Description

+ +

Resize test: using the scaler C++ engine to resize images

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\features\test_resize.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +
+ +

Test sub-module: items

+ +
+ +

Test: annotations

+ +
+ +

Description

+ +

Annotation test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_annotations.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: curves

+ +
+ +

Description

+ +

Curve plotting test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_curves.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: hist2d

+ +
+ +

Description

+ +

2-D Histogram test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_hist2d.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: histogram

+ +
+ +

Description

+ +

Histogram test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_histogram.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: image

+ +
+ +

Description

+ +

Test showing an image

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_image.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: image contour

+ +
+ +

Description

+ +

Contour test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_image_contour.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: image masked

+ +
+ +

Description

+ +

Masked Image test, creating the MaskedImageItem object via + + make.maskedimage

+ +

Masked image items are constructed using a masked array item. Masked + + data is ignored in computations, like the average cross sections.

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_image_masked.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: image masked xy

+ +
+ +

Description

+ +

Masked Image test, creating the MaskedXYImageItem object via + + make.maskedxyimage

+ +

Masked image XY items are constructed using a masked array item. + + Masked data is ignored in computations, like the average cross + + sections.

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_image_masked_xy.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: image rgb

+ +
+ +

Description

+ +

RGB Image test, creating the RGBImageItem object via + + make.rgbimage

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_image_rgb.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: image xy

+ +
+ +

Description

+ +

Image with custom X/Y axes linear scales

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_image_xy.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: mandelbrot

+ +
+ +

Description

+ +

Mandelbrot demo

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_mandelbrot.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: pcolor

+ +
+ +

Description

+ +

Test showing a pcolor plot

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_pcolor.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: polygons

+ +
+ +

Description

+ +

PolygonMapItem test

+ +

PolygonMapItem is intended to display maps ie items containing + + several hundreds of independent polygons.

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_polygons.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: svgshapes

+ +
+ +

Description

+ +

Test showing SVG shapes

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_svgshapes.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: transform

+ +
+ +

Description

+ +

Tests around image transforms: rotation, translation, ...

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\items\test_transform.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +
+ +

Test sub-module: tools

+ +
+ +

Test: actiontool

+ +
+ +

Description

+ +

ActionTool test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\tools\test_actiontool.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: cross section

+ +
+ +

Description

+ +

Renders a cross section chosen by a cross marker

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\tools\test_cross_section.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: cross section oblique

+ +
+ +

Description

+ +

Oblique averaged cross section test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\tools\test_cross_section_oblique.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: customize shape tool

+ +
+ +

Description

+ +

Shows how to customize a shape created with a tool like + + RectangleTool

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\tools\test_customize_shape_tool.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: get point

+ +
+ +

Description

+ +

SelectPointTool test

+ +

This exemple_module_ tool provide a MATLAB-like "ginput" feature.

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\tools\test_get_point.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: get rectangle

+ +
+ +

Description

+ +

Get rectangular selection from image

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\tools\test_get_rectangle.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: get rectangle with svg

+ +
+ +

Description

+ +

Get rectangular selection from image with SVG shape

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\tools\test_get_rectangle_with_svg.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: get segment

+ +
+ +

Description

+ +

Test get_segment feature: select a segment on an + + image.

+ +

This exemple_module_ tool provide a MATLAB-like "ginput" feature.

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\tools\test_get_segment.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: image plot tools

+ +
+ +

Description

+ +

All image and plot tools test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\tools\test_image_plot_tools.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +
+ +

Test sub-module: unit

+ +
+ +

Test: baseplot

+ +
+ +

Description

+ +

Testing BasePlot API

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\unit\test_baseplot.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: rst format

+ +
+ +

Description

+ +

Subtitle

+ +
+ +

Overview

+ +
+ +
Section 1
+ +

Text can be italicized or bolded as well as + + monospaced. You can /escape certain/ special + + characters. +

+ +
Subsection 1 (Level 2)
+ +

Some section 2 text

+ +

Sub-subsection 1 (level 3)

+ +

Some more text.

+ +
Examples
+ +
Comments
+ +
Images
+ +

Add an image with:

+ +

alternate text

+ +

You can inline an image or other directive with the (missing image text) command.

+ +
Lists
+ +
    + +
  • Bullet are made like this
  • + +
  • +
    + +
    Point levels must be consistent
    + +
    + +
      + +
    • +
      + +
      Sub-bullets
      + +
      + +
        + +
      • Sub-sub-bullets
      • + +
      + +
      + +
      +
    • + +
    + +
    + +
    +
  • + +
  • Lists
  • + +
+ +
+ +
Term
+ +
+ +

Definition for term

+ +
+ +
Term2
+ +
+ +

Definition for term 2

+ +
+ +
:List of Things:
+ +
+ +

item1 - these are 'field lists' not bulleted lists item2 item 3

+ +
+ +
+ +
+ +
Something
+ +
+ +

single item

+ +
+ +
Someitem
+ +
+ +

single item

+ +
+ +
+ +
Preformatted text
+ +

A code example prefix must always end with double colon like it's + + presenting something:

+ +
Anything indented is part of the preformatted block
+
+Until
+
+It gets back to
+
+Allll the way left
+ +

Now we're out of the preformatted block.

+ +
Code blocks
+ +

There are three equivalents: code, + + sourcecode, and code-block. +

+ +
+ +
+
import os
+
+print(help(os))
+
+ +
+ +
# Equivalent
+ +
# Equivalent
+ + + +

Web addresses by themselves will auto link, like this: https://www.devdungeon.com

+ +

You can also inline custom links: Google search engine

+ +

This is a simple link to Google + + with the link defined separately.

+ +

This is a link to the Python + + website.

+ +

This is a link back to Section 1. You can + + link based off of the heading name within a document.

+ +
Footnotes
+ +

Footnote Reference1

+ +

Or autonumbered [#]

+ +
Lines/Transitions
+ +

Any 4+ repeated characters with blank lines surrounding it becomes an + + hr line, like this.

+ +
+ +
Tables
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TimeNumberValue
12:00422
23:00234
+ +
Preserving line breaks
+ +

Normally you can break the line in the middle of a paragraph and it + + will ignore the newline. If you want to preserve the newlines, use the + + | prefix on the lines. For example: +

+ +
These lines will
+ + break exactly
+ + where we told them to.
+ +
+ +
+ +
    + +
  1. +
    + +

    This is footnote number one that would go at the bottom of the + + document.↩︎

    + +
    +
  2. + +
+ +
+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\unit\test_rst_format.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +
+ +

Test sub-module: widgets

+ +
+ +

Test: dotarraydemo

+ +
+ +

Description

+ +

Dot array example

+ +

Example showing how to create a custom item (drawing dots of variable + + size) and integrate the associated guidata dataset (GUI-based form) to edit its + + parameters (directly into the same window as the plot itself, + + and within the custom item parameters: right-click on the + + selectable item to open the associated dialog box). +

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\widgets\test_dotarraydemo.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: filtertest1

+ +
+ +

Description

+ +

Simple filter testing application based on PyQt and exemple_module_

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\widgets\test_filtertest1.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: filtertest2

+ +
+ +

Description

+ +

Simple filter testing application based on PyQt and exemple_module_ + + filtertest1.py + plot manager

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\widgets\test_filtertest2.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: fliprotate

+ +
+ +

Description

+ +

Flip/rotate test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\widgets\test_fliprotate.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: plot timecurve

+ +
+ +

Description

+ +

Dynamic curve widget test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\widgets\test_plot_timecurve.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: qtdesigner

+ +
+ +

Description

+ +

Testing exemple_module_ QtDesigner plugins

+ +

These plugins provide PlotWidget objects embedding in GUI layouts + + directly from QtDesigner.

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\widgets\test_qtdesigner.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: resize dialog

+ +
+ +

Description

+ +

ResizeDialog test

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\widgets\test_resize_dialog.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: rotatecrop

+ +
+ +

Description

+ +

Rotate/crop test: using the scaler C++ engine to rotate/crop + + images

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\widgets\test_rotatecrop.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: simple dialog

+ +
+ +

Description

+ +

Simple dialog box based on exemple_module_

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\widgets\test_simple_dialog.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ +

Test: simple window

+ +
+ +

Description

+ +

Simple application based on exemple_module_

+ +
+ +
+ +

Command

+ + "-path-to-python-env-\ModuleTester\.venv\Scripts\python.exe" -u -X utf8 "-path-to-python-env-\ModuleTester\.venv\lib\site-packages\exemple_module__test\tests\widgets\test_simple_window.py" + +
+ +
+ +

Result

+ +

No result found.

+ +
+ + + +
+ +
+ +
+ +
+ + + +
+ + + + \ No newline at end of file From 334a39d8cd6e72ca466589031511d87190c3280c Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Wed, 20 May 2026 16:49:42 +0200 Subject: [PATCH 3/9] update installation setup and vscode task --- .gitignore | 1 + .vscode/settings.json | 7 + .vscode/tasks.json | 294 +++++++++++------- babel.cfg | 4 + .../locale/fr/LC_MESSAGES/moduletester.po | 88 +----- pyproject.toml | 7 +- scripts/run_with_env.py | 210 +++++++++++++ 7 files changed, 424 insertions(+), 187 deletions(-) create mode 100644 babel.cfg create mode 100644 scripts/run_with_env.py diff --git a/.gitignore b/.gitignore index b8eafe1..52d5fe8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.env winpython.env .spyderproject doc.zip diff --git a/.vscode/settings.json b/.vscode/settings.json index 824675c..18d7f52 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -42,4 +42,11 @@ "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "svg.preview.background": "editor", + "python-envs.pythonProjects": [ + { + "path": ".", + "envManager": "ms-python.python:venv", + "packageManager": "ms-python.python:pip" + } + ], } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 252220f..1b8cfb0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,137 +4,175 @@ "version": "2.0.0", "tasks": [ { - "label": "gettext - Scan", + "label": "🔎 gettext - Scan", "type": "shell", - "command": "cmd", + "command": "${command:python.interpreterPath}", "args": [ - "/c", - "gettext_scan.bat" + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "guidata.utils.translations", + "scan", + "--name", + "moduletester", + "--directory", + ".", + "--languages", + "fr", ], "options": { - "cwd": "scripts", - "env": { - "UNATTENDED": "1", - "PYTHON": "${env:PPSTACK_PYTHONEXE}" - } + "cwd": "${workspaceFolder}", }, "group": { "kind": "build", - "isDefault": true + "isDefault": false, }, "presentation": { + "clear": true, "echo": true, - "reveal": "always", "focus": false, - "panel": "shared", + "panel": "dedicated", + "reveal": "always", "showReuseMessage": true, - "clear": false - } + }, }, { - "label": "gettext - Compile", + "label": "📚 gettext - Compile", "type": "shell", - "command": "cmd", + "command": "${command:python.interpreterPath}", "args": [ - "/c", - "gettext.bat", - "compile" + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "guidata.utils.translations", + "compile", + "--name", + "moduletester", + "--directory", + ".", ], "options": { - "cwd": "scripts", - "env": { - "UNATTENDED": "1", - "PYTHON": "${env:PPSTACK_PYTHONEXE}" - } + "cwd": "${workspaceFolder}", }, "group": { "kind": "build", - "isDefault": true + "isDefault": false, }, "presentation": { + "clear": true, "echo": true, - "reveal": "always", "focus": false, - "panel": "shared", + "panel": "dedicated", + "reveal": "always", "showReuseMessage": true, - "clear": false - } + }, }, { - "label": "Run Pylint", + "label": "🔦 Pylint", "type": "shell", - "command": "cmd", + "command": "${command:python.interpreterPath}", "args": [ - "/c", - "run_pylint.bat", + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "pylint", + "moduletester", + "--disable=duplicate-code", "--disable=fixme", + "--disable=too-many-arguments", + "--disable=too-many-branches", + "--disable=too-many-instance-attributes", ], "options": { - "cwd": "scripts", - "env": { - "UNATTENDED": "1", - "PYTHON": "${env:PPSTACK_PYTHONEXE}" - } + "cwd": "${workspaceFolder}", }, "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, "presentation": { + "clear": true, "echo": true, - "reveal": "always", "focus": false, "panel": "dedicated", + "reveal": "always", "showReuseMessage": true, - "clear": true - } + }, }, { - "label": "Run Coverage", + "label": "🧪 Coverage tests", "type": "shell", - "command": "cmd", + "command": "${command:python.interpreterPath}", "args": [ - "/c", - "run_coverage.bat" + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "coverage", + "run", + "-m", + "pytest", + "moduletester", ], "options": { - "cwd": "scripts", + "cwd": "${workspaceFolder}", "env": { - "UNATTENDED": "1", - "PYTHON": "${env:PPSTACK_PYTHONEXE}" - } + "COVERAGE_PROCESS_START": "${workspaceFolder}/.coveragerc", + }, + "statusbar": { + "hide": true, + }, }, "group": { - "kind": "build", - "isDefault": true + "kind": "test", + "isDefault": true, }, "presentation": { - "echo": true, - "reveal": "always", - "focus": false, "panel": "dedicated", - "showReuseMessage": true, - "clear": true - } + }, + "problemMatcher": [], + }, + { + "label": "📊 Coverage full", + "type": "shell", + "command": "${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage combine; if ($?) { ${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage html; if ($?) { start htmlcov\\index.html } }", + "options": { + "cwd": "${workspaceFolder}", + "env": { + "COVERAGE_PROCESS_START": "${workspaceFolder}/.coveragerc", + }, + }, + "presentation": { + "panel": "dedicated", + }, + "problemMatcher": [], + "dependsOrder": "sequence", + "dependsOn": [ + "🧪 Coverage tests", + ], }, { - "label": "Upgrade environment", + "label": "Upgrade guidata", "type": "shell", - "command": "cmd", + "command": "${command:python.interpreterPath}", "args": [ - "/c", - "upgrade_env.bat" + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "pip", + "install", + "--upgrade", + "pip", + "guidata", ], "options": { - "cwd": "scripts", - "env": { - "UNATTENDED": "1", - "PYTHON": "${env:PPSTACK_PYTHONEXE}" - } + "cwd": "${workspaceFolder}", + "statusbar": { + "hide": true, + }, }, "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, "presentation": { "echo": true, @@ -142,23 +180,25 @@ "focus": false, "panel": "shared", "showReuseMessage": true, - "clear": false - } + "clear": false, + }, }, { - "label": "Clean Up", + "label": "🧹 Clean Up", "type": "shell", - "command": "cmd", + "command": "${command:python.interpreterPath}", "args": [ - "/c", - "clean_up.bat" + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "guidata.utils.cleanup", ], "options": { - "cwd": "scripts" + "cwd": "${workspaceFolder}", }, "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, "presentation": { "echo": true, @@ -166,68 +206,106 @@ "focus": false, "panel": "shared", "showReuseMessage": true, - "clear": false - } + "clear": false, + }, }, { - "label": "Build documentation", + "label": "📚 Build doc", "type": "shell", - "command": "cmd", + "command": "${command:python.interpreterPath}", + "args": [ + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "sphinx", + "build", + "doc", + "${workspaceFolder}/build/doc", + "-b", + "singlehtml", + ], "options": { - "cwd": "scripts", + "cwd": "${workspaceFolder}", "env": { - "PYTHON": "${env:PPSTACK_PYTHONEXE}", "QT_COLOR_MODE": "light", - "UNATTENDED": "1" - } + }, }, - "args": [ - "/c", - "build_doc.bat" - ], - "problemMatcher": [], "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, "presentation": { + "clear": true, "echo": true, - "reveal": "always", "focus": false, - "panel": "shared", + "panel": "dedicated", + "reveal": "always", "showReuseMessage": true, - "clear": true - } + }, }, { - "label": "Build Python packages", + "label": "🌐 Open HTML doc", "type": "shell", - "command": "cmd", + "windows": { + "command": "start build/doc/index.html", + }, + "linux": { + "command": "xdg-open build/doc/index.html", + }, + "osx": { + "command": "open build/doc/index.html", + }, "options": { - "cwd": "scripts", - "env": { - "PYTHON": "${env:PPSTACK_PYTHONEXE}", - "UNATTENDED": "1" - } + "cwd": "${workspaceFolder}", }, + "problemMatcher": [], + }, + { + "label": "📦 Build package", + "type": "shell", + "command": "${command:python.interpreterPath}", "args": [ - "/c", - "build_dist.bat" + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "build", ], + "options": { + "cwd": "${workspaceFolder}", + }, + "group": { + "kind": "build", + "isDefault": false, + }, + "presentation": { + "clear": true, + "panel": "dedicated", + }, "problemMatcher": [], + "dependsOrder": "sequence", + "dependsOn": [ + "🧹 Clean Up", + ], + }, + { + "label": "❔ Untracked files", + "type": "shell", + "command": "git ls-files --others | Where-Object { $_ -notmatch '^\\.' -and $_ -notmatch '^(build|dist|releases)/' -and $_ -notmatch '.(pyc|mo)$'}", + "options": { + "cwd": "${workspaceFolder}", + }, "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, "presentation": { "echo": true, "reveal": "always", "focus": false, - "panel": "shared", + "panel": "dedicated", "showReuseMessage": true, - "clear": true + "clear": true, }, - "dependsOrder": "sequence", }, - ] + ], } \ No newline at end of file diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..b46a78c --- /dev/null +++ b/babel.cfg @@ -0,0 +1,4 @@ +# This file is used to configure Babel for the project. + +[python: **.py] +encoding = utf-8 diff --git a/moduletester/locale/fr/LC_MESSAGES/moduletester.po b/moduletester/locale/fr/LC_MESSAGES/moduletester.po index 56e0932..05a80b2 100644 --- a/moduletester/locale/fr/LC_MESSAGES/moduletester.po +++ b/moduletester/locale/fr/LC_MESSAGES/moduletester.po @@ -1,64 +1,44 @@ -# -*- coding: utf-8 -*- -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR ORGANIZATION -# FIRST AUTHOR , YEAR. +# French translations for moduletester. +# Copyright (C) 2026 ORGANIZATION +# This file is distributed under the same license as the moduletester project. # msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"POT-Creation-Date: 2024-03-13 16:12+0100\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" +"Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: pygettext.py 1.5\n" -#: moduletester\gui\window.py:396 msgid "Error during moduletester file loading." msgstr "Erreur lors du chargement du fichier moduletester." -#: moduletester\gui\window.py:398 +#, python-format msgid "Error while parsing the .moduletester file: %s" msgstr "Erreur lors du chargement du fichier .moduletester : %s" -#: moduletester\gui\window.py:400 msgid "Missing modules:" msgstr "Modules manquants :" -#: moduletester\gui\window.py:402 msgid "Do you want to reimport all tests and override the current file?" msgstr "Vous voulez réimporter tous les tests et écraser le fichier actuel ?" -#: moduletester\gui\window.py:427 msgid "Error during module loading." msgstr "Erreur lors du chargement du module." -#: moduletester\gui\window.py:429 msgid "Error while importing modules:" msgstr "Erreur lors de l'importation des modules :" -#: moduletester\gui\window.py:431 -msgid "" -"The modules are still visible in moduletester but should be fixed or removed." -msgstr "" -"Les modules sont toujours visibles dans moduletester mais devraient être " -"corrigés ou supprimés." +msgid "The modules are still visible in moduletester but should be fixed or removed." +msgstr "Les modules sont toujours visibles dans moduletester mais devraient être corrigés ou supprimés." -#: moduletester\gui\window.py:540 msgid "Test running" msgstr "Test en cours" -#: moduletester\gui\window.py:541 -msgid "" -"A test is currently running, please wait for it to finish before saving." -msgstr "" -"Un test est en cours, veuillez attendre qu'il se termine avant de " -"sauvegarder." +msgid "A test is currently running, please wait for it to finish before saving." +msgstr "Un test est en cours, veuillez attendre qu'il se termine avant de sauvegarder." -#: moduletester\model.py:89 +#, python-format msgid "" "This package encountered the following error during import:\n" "%s" @@ -66,47 +46,3 @@ msgstr "" "Ce paquet a rencontré l'erreur suivante lors de l'import :\n" "%s" -msgid "Test List Document" -msgstr "Document de la liste des tests" - -msgid "Test Results Document" -msgstr "Document de résultat de tests" - -#~ msgid "Last execution date:" -#~ msgstr "Dernière date d'exécution :" - -#~ msgid "%s test results summary" -#~ msgstr "%s résumé des résultats de test" - -#~ msgid "No result" -#~ msgstr "Pas de résultat" - -#~ msgid "Accepted" -#~ msgstr "Validé" - -#~ msgid "Accepted with reserve" -#~ msgstr "Validé sous réserve" - -#~ msgid "Skipped" -#~ msgstr "Passé" - -#~ msgid "Rejected" -#~ msgstr "Rejeté" - -#~ msgid "Count by result cateogry" -#~ msgstr "Compte par catégorie de résultat" - -#~ msgid "ACCEPTED" -#~ msgstr "ACCEPTÉ" - -#~ msgid "ACCEPTED WITH RESERVES" -#~ msgstr "ACCEPTÉ SOUS RÉSERVE" - -#~ msgid "SKIPPED" -#~ msgstr "PASSÉ" - -#~ msgid "REJECTED" -#~ msgstr "REJETÉ" - -#~ msgid "NO RESULT" -#~ msgstr "PAS DE RÉSULTAT" diff --git a/pyproject.toml b/pyproject.toml index 5aa784b..3bc6dc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,10 +24,12 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.11", ] requires-python = ">=3.8, <4" dependencies = [ - "guidata >= 3.3", + "guidata >= 3.14", "QtPy >= 1.9", "click", "pyqtwebengine", @@ -36,7 +38,6 @@ dependencies = [ "beautifulsoup4", ] dynamic = ["version"] - [project.urls] Homepage = "https://github.com/Codra-Ingenierie-Informatique/ModuleTester/" Documentation = "https://moduletester.readthedocs.io/en/latest/" @@ -48,7 +49,7 @@ moduletester-cli = "moduletester.manager:cli" moduletester = "moduletester.gui.main:run_gui" [project.optional-dependencies] -dev = ["black", "isort", "pylint", "Coverage"] +dev = ["black", "isort", "pylint", "pytest", "Coverage", "build"] doc = ["PyQt5", "sphinx>6", "pydata_sphinx_theme"] [tool.setuptools.packages.find] diff --git a/scripts/run_with_env.py b/scripts/run_with_env.py new file mode 100644 index 0000000..6688cb6 --- /dev/null +++ b/scripts/run_with_env.py @@ -0,0 +1,210 @@ +# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file. + +"""Run a command with environment variables loaded from a .env file. + +This script automatically detects the best Python interpreter to use: + +1. ``PYTHON`` variable in ``.env`` file (e.g. for WinPython distributions) +2. ``WINPYDIRBASE`` variable (legacy WinPython base directory) +3. ``VENV_DIR`` variable (explicit virtual environment directory) +4. A local virtual environment (``.venv*`` directory in the project root) +5. Falls back to ``sys.executable`` (the Python that launched this script) + +This ensures that VS Code tasks always use the correct Python environment +regardless of which interpreter is configured globally or in VS Code. +""" + +from __future__ import annotations + +import glob +import os +import subprocess +import sys +from pathlib import Path + + +def _find_venv_python(project_root: Path) -> str | None: + """Find a Python executable in a ``.venv*`` directory. + + Searches for directories matching ``.venv*`` in the project root and + returns the first valid Python executable found. + + Args: + project_root: The root directory of the project. + + Returns: + Absolute path to the venv Python executable, or None if not found. + """ + # Sort to prefer ".venv" over ".venv-xyz" etc. + venv_dirs = sorted(glob.glob(str(project_root / ".venv*"))) + for venv_dir in venv_dirs: + venv_path = Path(venv_dir) + if not venv_path.is_dir(): + continue + result = _get_venv_python(venv_path) + if result: + return result + return None + + +def _get_venv_python(venv_dir: Path) -> str | None: + """Get the Python executable from a specific venv directory. + + Args: + venv_dir: Path to the virtual environment directory. + + Returns: + Absolute path to the Python executable, or None if not found. + """ + if not venv_dir.is_dir(): + return None + # Windows: Scripts/python.exe — Unix: bin/python + candidates = [ + venv_dir / "Scripts" / "python.exe", + venv_dir / "bin" / "python", + ] + for candidate in candidates: + if candidate.is_file(): + # Keep the venv-local executable path without resolving symlinks: + # on Linux/WSL, ``bin/python`` is often a symlink to a global + # interpreter (e.g. /usr/bin/python3.x). Resolving it would lose + # venv context and site-packages selection. + return str(candidate.absolute()) + return None + + +def resolve_python(project_root: Path) -> str: + """Resolve the best Python interpreter for the project. + + Priority order: + + 1. ``PYTHON`` environment variable (set in ``.env`` or externally) + 2. ``WINPYDIRBASE`` environment variable (legacy WinPython base directory) + 3. ``VENV_DIR`` environment variable (explicit venv directory) + 4. ``.venv*`` directory in *project_root* (auto-discovery) + 5. ``sys.executable`` (the interpreter running this script) + + Args: + project_root: The root directory of the project. + + Returns: + Absolute path to the Python executable to use. + """ + # 1. Explicit PYTHON variable (e.g. WinPython distribution) + python_env = os.environ.get("PYTHON") + if python_env: + python_path = Path(python_env) + if python_path.is_file(): + # Do not resolve symlinks for the same reason as in + # ``_get_venv_python``. + resolved = str(python_path.absolute()) + print(f" 🐍 Using PYTHON from .env: {resolved}") + return resolved + print(f" ⚠️ PYTHON variable set but not found: {python_env}") + + # 2. Legacy WINPYDIRBASE variable (WinPython distribution) + winpy_base = os.environ.get("WINPYDIRBASE") + if winpy_base and Path(winpy_base).is_dir(): + # Search for python.exe in the WinPython directory structure + # Patterns: python-3.11.5.amd64/python.exe (old) or python/python.exe (new) + for pattern in ("python-*/python.exe", "python/python.exe"): + for candidate in sorted(Path(winpy_base).glob(pattern)): + if candidate.is_file(): + resolved = str(candidate.absolute()) + print(f" 🐍 Using WINPYDIRBASE (legacy): {resolved}") + return resolved + # Also try direct python.exe in the base directory + direct = Path(winpy_base) / "python.exe" + if direct.is_file(): + resolved = str(direct.absolute()) + print(f" 🐍 Using WINPYDIRBASE (legacy): {resolved}") + return resolved + print(f" ⚠️ WINPYDIRBASE set but no Python found in: {winpy_base}") + + # 3. Explicit VENV_DIR variable (e.g. for multiple local venvs) + venv_dir_env = os.environ.get("VENV_DIR") + if venv_dir_env: + venv_dir = Path(venv_dir_env) + if not venv_dir.is_absolute(): + venv_dir = project_root / venv_dir + venv_python = _get_venv_python(venv_dir) + if venv_python: + print(f" 🐍 Using VENV_DIR from .env: {venv_python}") + return venv_python + print(f" ⚠️ VENV_DIR set but no Python found in: {venv_dir}") + + # 4. Auto-discover local venv + venv_python = _find_venv_python(project_root) + if venv_python: + print(f" 🐍 Using venv Python: {venv_python}") + return venv_python + + # 5. Fallback + print(f" 🐍 Using caller Python: {sys.executable}") + return sys.executable + + +def load_env_file(env_path: str | None = None) -> None: + """Load environment variables from a .env file.""" + if env_path is None: + env_path = Path.cwd() / ".env" + if not Path(env_path).is_file(): + raise FileNotFoundError(f"Environment file not found: {env_path}") + print(f"Loading environment variables from: {env_path}") + with open(env_path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + value = value.strip().strip('"').strip("'") + os.environ[key.strip()] = value + print(f" Loaded variable: {key.strip()}={value}") + + +def execute_command(command: list[str], python_exe: str) -> int: + """Execute a command, replacing ``python`` placeholders. + + Any argument that is the bare word ``python`` or that points to a Python + executable (checked via filename) is replaced by *python_exe* so that the + subprocess uses the resolved interpreter rather than the global one. + + Args: + command: The command and its arguments. + python_exe: The resolved Python interpreter path. + + Returns: + The subprocess exit code. + """ + resolved: list[str] = [] + for arg in command: + if arg.lower() == "python" or ( + Path(arg).name.lower().startswith("python") + and Path(arg).is_file() + and arg.lower() != python_exe.lower() + ): + resolved.append(python_exe) + else: + resolved.append(arg) + print("Executing command:") + print(" ".join(resolved)) + print("") + result = subprocess.call(resolved) + print(f"Process exited with code {result}") + return result + + +def main() -> None: + """Main function to load environment variables and execute a command.""" + if len(sys.argv) < 2: + print("Usage: python run_with_env.py [args ...]") + sys.exit(1) + print("🏃 Running with environment variables") + project_root = Path.cwd() + load_env_file() + python_exe = resolve_python(project_root) + return execute_command(sys.argv[1:], python_exe) + + +if __name__ == "__main__": + main() From b150b6191bebe3509e0468945488cd5ecda16973 Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Wed, 20 May 2026 17:08:01 +0200 Subject: [PATCH 4/9] update python minimum version and changelog --- CHANGELOG.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- pyproject.toml | 6 +++--- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d26039..b62cfe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,51 @@ # ModuleTester Releases # +## Version 1.0.0 ## + +First stable release of ModuleTester. + +### New features + +- Reworked GUI with dockable panels, collapsible CLI widget, and resizable + result/info panels +- Tree view for test navigation with body/tree synchronization +- Notification system for test execution output +- Configuration editor widget with error handling and conflict resolution +- Web engine view for test descriptions with forced reload and restricted + context menu +- New Jinja2-based document exporter supporting HTML, DOCX, ODT, PDF, MD + and RST output formats +- Test list export as a standalone document +- Customizable CSS styling for exported documents (code blocks, inline + code spans) +- Missing module handling with placeholder display +- Bundled `pyqtspinner` widget (removed external dependency) + +### Bug fixes + +- Fixed processes not being able to be opened/closed on Linux +- Fixed package-defined templates not applied on generated files +- Fixed module not reloading correctly after code changes +- Fixed command timeout handling in test execution +- Fixed command line arguments parsing +- Fixed encoding errors on test outputs (non-UTF8) +- Fixed double-click run synchronization with the run button +- Fixed partial name match causing unintended tests to run +- Fixed `end_time` remaining `None` after test completion +- Fixed `communicate()` returning `None` in edge cases +- Fixed invalid tests returned by `get_tests()` +- Fixed toolbar layout and typing issues +- Fixed dark theme rendering on Windows 10 +- Fixed result combo box state while tests are running +- Fixed RST conversion using `sys.prefix` for executable lookup + +### Improvements + +- Improved configuration system with new tools and better error handling +- Consistent result enum casing between widgets +- Removed dead code and debug prints +- Pylint pass and type checking improvements + ## Version 0.1.0 ## -This is an experimental release of ModuleTester. It is not yet ready for -production use. +Experimental release. Not ready for production use. diff --git a/pyproject.toml b/pyproject.toml index 3bc6dc8..946d5bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ description = "ModuleTester is a test management software for Python packages" readme = "README.md" license = { file = "LICENSE" } classifiers = [ + "Development Status :: 5 - Production/Stable", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Utilities", "Topic :: Software Development :: User Interfaces", @@ -19,15 +20,14 @@ classifiers = [ "Operating System :: OS Independent", "Operating System :: POSIX", "Operating System :: Unix", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", - "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.14", ] -requires-python = ">=3.8, <4" +requires-python = ">=3.9, <4" dependencies = [ "guidata >= 3.14", "QtPy >= 1.9", From dcbc62fa89da10b73b8735389d3bd8572e87399b Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Wed, 20 May 2026 17:18:20 +0200 Subject: [PATCH 5/9] add test unit suite and minimal test unit (serialisation and model) --- moduletester/tests/__init__.py | 0 moduletester/tests/conftest.py | 2 + moduletester/tests/test_model.py | 79 +++++++++++++++++++++++++++ moduletester/tests/test_serializer.py | 36 ++++++++++++ pyproject.toml | 3 + scripts/run_coverage.bat | 2 +- scripts/run_test_launcher.bat | 2 +- scripts/run_unittests.bat | 2 +- 8 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 moduletester/tests/__init__.py create mode 100644 moduletester/tests/conftest.py create mode 100644 moduletester/tests/test_model.py create mode 100644 moduletester/tests/test_serializer.py diff --git a/moduletester/tests/__init__.py b/moduletester/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/moduletester/tests/conftest.py b/moduletester/tests/conftest.py new file mode 100644 index 0000000..662ba24 --- /dev/null +++ b/moduletester/tests/conftest.py @@ -0,0 +1,2 @@ +# pylint: disable=missing-module-docstring +from __future__ import annotations diff --git a/moduletester/tests/test_model.py b/moduletester/tests/test_model.py new file mode 100644 index 0000000..9c6a062 --- /dev/null +++ b/moduletester/tests/test_model.py @@ -0,0 +1,79 @@ +"""Tests for the model module (enums, dataclasses).""" +from __future__ import annotations + +from datetime import datetime, timedelta +from io import StringIO + +from moduletester.model import ( + ModuleNotFoundType, + ResultEnum, + StatusEnum, + TestResult, +) +from moduletester.serializer import DataclassSerializer, EnumSerializer + + +class TestStatusEnum: + def test_values(self): + assert StatusEnum.EXECUTED.value == "executed" + assert StatusEnum.NOT_EXECUTED.value == "not executed" + assert StatusEnum.ABORTED.value == "aborted" + + def test_serializer_roundtrip(self): + s = EnumSerializer(StatusEnum) + for member in StatusEnum: + assert s.deserialize(s.serialize(member)) == member + + +class TestResultEnum: + def test_values(self): + assert ResultEnum.ACCEPTED.value == "accepted" + assert ResultEnum.NO_RESULT.value == "no result" + + def test_format(self): + assert ResultEnum.ACCEPTED_WITH_RESERVES.format() == "ACCEPTED WITH RESERVES" + + def test_serializer_roundtrip(self): + s = EnumSerializer(ResultEnum) + for member in ResultEnum: + assert s.deserialize(s.serialize(member)) == member + + +class TestTestResult: + def test_creation_defaults(self): + tr = TestResult(status=StatusEnum.NOT_EXECUTED) + assert tr.result == ResultEnum.NO_RESULT + assert tr.comment == "" + assert tr.output_msg == "" + assert tr.error_msg == "" + assert tr.error_code is None + + def test_serialization_roundtrip(self): + tr = TestResult( + status=StatusEnum.EXECUTED, + result=ResultEnum.ACCEPTED, + execution_duration=timedelta(seconds=1.5), + last_run=datetime(2026, 5, 20, 10, 0, 0, 0), + comment="OK", + ) + s = DataclassSerializer() + data = s.serialize(tr) + restored = s.deserialize(data) + assert isinstance(restored, TestResult) + assert restored.status == StatusEnum.EXECUTED + assert restored.result == ResultEnum.ACCEPTED + assert restored.comment == "OK" + + def test_properties(self): + tr = TestResult( + status=StatusEnum.EXECUTED, result=ResultEnum.ACCEPTED_WITH_RESERVES + ) + assert tr.result_name == "ACCEPTED WITH RESERVES" + assert tr.status_name == "EXECUTED" + + +class TestModuleNotFoundType: + def test_creation(self): + m = ModuleNotFoundType("some.missing.module") + assert m.__name__ == "some.missing.module" + assert m.__doc__ is not None diff --git a/moduletester/tests/test_serializer.py b/moduletester/tests/test_serializer.py new file mode 100644 index 0000000..7d6e1ab --- /dev/null +++ b/moduletester/tests/test_serializer.py @@ -0,0 +1,36 @@ +"""Tests for the serializer module.""" +from __future__ import annotations + +import json +from datetime import datetime, timedelta + +from moduletester.serializer import ( + DataclassSerializer, + DateTimeSerializer, + ObjectSerializerBase, + TimedeltaSerializer, +) + + +class TestDateTimeSerializer: + def test_roundtrip(self): + dt = datetime(2026, 5, 20, 14, 30, 45, 123456) + s = DateTimeSerializer() + assert s.deserialize(s.serialize(dt)) == dt + + def test_format(self): + dt = datetime(2026, 1, 2, 3, 4, 5, 678900) + s = DateTimeSerializer() + assert s.serialize(dt) == "02/01/26 03:04:05.678900" + + +class TestTimedeltaSerializer: + def test_roundtrip(self): + td = timedelta(hours=1, minutes=30, seconds=15) + s = TimedeltaSerializer() + assert s.deserialize(s.serialize(td)) == td + + def test_serialize_seconds(self): + td = timedelta(seconds=90.5) + s = TimedeltaSerializer() + assert s.serialize(td) == 90.5 diff --git a/pyproject.toml b/pyproject.toml index 946d5bc..659eea8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,3 +70,6 @@ include = ["moduletester*"] [tool.setuptools.dynamic] version = { attr = "moduletester.__version__" } + +[tool.pytest.ini_options] +testpaths = ["moduletester/tests"] diff --git a/scripts/run_coverage.bat b/scripts/run_coverage.bat index c3a577b..f70ca30 100644 --- a/scripts/run_coverage.bat +++ b/scripts/run_coverage.bat @@ -17,7 +17,7 @@ if exist sitecustomize.py ( del /q sitecustomize.py ) echo import coverage> sitecustomize.py echo coverage.process_startup()>> sitecustomize.py set COVERAGE_PROCESS_START=%SCRIPTPATH%\..\.coveragerc -coverage run -m cdl.tests.all_tests %* --timeout 600 +coverage run -m pytest %MODNAME% %* @REM coverage report -m coverage combine coverage html diff --git a/scripts/run_test_launcher.bat b/scripts/run_test_launcher.bat index 69256fe..b847f5d 100644 --- a/scripts/run_test_launcher.bat +++ b/scripts/run_test_launcher.bat @@ -12,5 +12,5 @@ call %~dp0utils GetScriptPath SCRIPTPATH call %FUNC% SetPythonPath call %FUNC% UsePython call %FUNC% GetModName MODNAME -python -m %MODNAME%.tests.__init__ +python -m pytest %MODNAME% %* call %FUNC% EndOfScript \ No newline at end of file diff --git a/scripts/run_unittests.bat b/scripts/run_unittests.bat index 0cfadfe..c85e285 100644 --- a/scripts/run_unittests.bat +++ b/scripts/run_unittests.bat @@ -12,5 +12,5 @@ call %~dp0utils GetScriptPath SCRIPTPATH call %FUNC% GetModName MODNAME call %FUNC% SetPythonPath call %FUNC% UsePython -python -m %MODNAME%.tests.all_tests +python -m pytest %MODNAME% call %FUNC% EndOfScript \ No newline at end of file From 0e6b0f9a1e4c9e8a1e031e1bd17fcfcaf08cbc8e Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Wed, 20 May 2026 17:41:58 +0200 Subject: [PATCH 6/9] add ruff linter and fix ruff warnings --- .pre-commit-config.yaml | 9 +++ .vscode/tasks.json | 81 +++++++++++++++++++ moduletester/exporter.py | 5 +- moduletester/gui/__init__.py | 2 +- moduletester/gui/components/body_component.py | 1 - .../gui/components/test_information.py | 8 +- .../gui/components/test_list_component.py | 1 - moduletester/gui/main.py | 2 - moduletester/gui/widgets/cli_widget.py | 20 ----- moduletester/gui/widgets/config_editor.py | 3 +- moduletester/gui/widgets/dockable_widget.py | 4 - moduletester/gui/widgets/editor_widget.py | 2 - .../gui/widgets/result_props_widget.py | 1 - .../gui/widgets/test_description_widget.py | 2 - moduletester/gui/widgets/test_list_widget.py | 1 - moduletester/gui/widgets/web_engine.py | 7 +- moduletester/gui/window.py | 16 ++-- moduletester/manager.py | 2 - moduletester/python_helpers.py | 7 +- moduletester/tests/test_model.py | 2 +- moduletester/tests/test_serializer.py | 4 +- pyproject.toml | 29 ++++++- scripts/run_black_isort.bat | 18 ----- 23 files changed, 143 insertions(+), 84 deletions(-) create mode 100644 .pre-commit-config.yaml delete mode 100644 scripts/run_black_isort.bat diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..2a49e2b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.12.2 + hooks: + # Run the linter. + - id: ruff-check + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1b8cfb0..ccdf3a1 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -3,6 +3,87 @@ // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ + { + "label": "🧽 Ruff Formatter", + "type": "shell", + "command": "${command:python.interpreterPath}", + "args": [ + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "ruff", + "format", + ], + "options": { + "cwd": "${workspaceFolder}", + "statusbar": { + "hide": true, + }, + }, + "group": { + "kind": "build", + "isDefault": true, + }, + "presentation": { + "clear": true, + "echo": true, + "focus": false, + "panel": "dedicated", + "reveal": "always", + "showReuseMessage": true, + }, + }, + { + "label": "🔦 Ruff Linter", + "type": "shell", + "command": "${command:python.interpreterPath}", + "args": [ + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "ruff", + "check", + "--fix", + ], + "options": { + "cwd": "${workspaceFolder}", + "statusbar": { + "hide": true, + }, + }, + "group": { + "kind": "build", + "isDefault": true, + }, + "presentation": { + "clear": true, + "echo": true, + "focus": false, + "panel": "dedicated", + "reveal": "always", + "showReuseMessage": true, + }, + }, + { + "label": "🧽🔦 Ruff", + "dependsOrder": "sequence", + "dependsOn": [ + "🧽 Ruff Formatter", + "🔦 Ruff Linter", + ], + "group": { + "kind": "build", + "isDefault": false, + }, + "presentation": { + "clear": true, + "echo": true, + "focus": false, + "panel": "dedicated", + "reveal": "always", + "showReuseMessage": true, + }, + }, { "label": "🔎 gettext - Scan", "type": "shell", diff --git a/moduletester/exporter.py b/moduletester/exporter.py index 58428c5..d07a121 100644 --- a/moduletester/exporter.py +++ b/moduletester/exporter.py @@ -30,9 +30,8 @@ def export( images, substitutes = self.export_images(test_images) title = f"\t{title:<{self.padding}}" - description = ( - f" .. include:: {desc_path}\n\t{' ' * (self.padding+2)}\t:parser: rst\n\n" - ) + pad = " " * (self.padding + 2) + description = f" .. include:: {desc_path}\n\t{pad}\t:parser: rst\n\n" template = "".join([title, description, *images]) diff --git a/moduletester/gui/__init__.py b/moduletester/gui/__init__.py index c961dae..127a241 100644 --- a/moduletester/gui/__init__.py +++ b/moduletester/gui/__init__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- # pylint: disable=unused-import -from moduletester import config # pylint: disable=unused-import +from moduletester import config as config # noqa: F401 diff --git a/moduletester/gui/components/body_component.py b/moduletester/gui/components/body_component.py index ee2a5e7..05dd2c0 100644 --- a/moduletester/gui/components/body_component.py +++ b/moduletester/gui/components/body_component.py @@ -178,7 +178,6 @@ def _run_on_double_click(self, clicked_item: QW.QTreeWidgetItem): and selected_test is not None and self.validate_command(selected_test) ): - self.set_item(is_test_modified=False) self.run_test() diff --git a/moduletester/gui/components/test_information.py b/moduletester/gui/components/test_information.py index 679122c..43d6c41 100644 --- a/moduletester/gui/components/test_information.py +++ b/moduletester/gui/components/test_information.py @@ -1,13 +1,11 @@ # pylint: disable=missing-class-docstring, missing-module-docstring # pylint: disable=missing-function-docstring -from typing import Optional from qtpy import QtCore as QC from qtpy import QtGui as QG from qtpy import QtWidgets as QW from moduletester.gui.states.signals import TMSignals -from moduletester.gui.widgets.dockable_widget import DockableQWidget from moduletester.gui.widgets.tab_image_widget import TabImageWidget from moduletester.gui.widgets.test_description_widget import TestDescriptionWidget from moduletester.gui.widgets.test_prop_widget import TestProps @@ -126,7 +124,9 @@ def validate_command(self, test: Test) -> bool: QW.QMessageBox( QW.QMessageBox.NoIcon, "Command Error", - "The following error occured while parsing the command " - f"line arguments:\n\n\t{str(e)}\n\nPlease check the command line arguments.", + "The following error occured while parsing the " + "command line arguments:" + f"\n\n\t{str(e)}\n\n" + "Please check the command line arguments.", ).exec() return False diff --git a/moduletester/gui/components/test_list_component.py b/moduletester/gui/components/test_list_component.py index 5e06dca..530286d 100644 --- a/moduletester/gui/components/test_list_component.py +++ b/moduletester/gui/components/test_list_component.py @@ -2,7 +2,6 @@ from typing import Optional -import qtpy.QtCore as QC import qtpy.QtWidgets as QW from guidata.configtools import get_icon diff --git a/moduletester/gui/main.py b/moduletester/gui/main.py index fefb14a..3713695 100644 --- a/moduletester/gui/main.py +++ b/moduletester/gui/main.py @@ -2,12 +2,10 @@ # pylint: disable=missing-function-docstring import argparse -import sys from importlib import import_module from typing import Optional from guidata.qthelpers import qt_app_context -from qtpy import QtWidgets as QW from moduletester.gui.states.signals import TMSignals from moduletester.gui.states.state_machine import TMStateMachine diff --git a/moduletester/gui/widgets/cli_widget.py b/moduletester/gui/widgets/cli_widget.py index 22a2d95..0e8a1ea 100644 --- a/moduletester/gui/widgets/cli_widget.py +++ b/moduletester/gui/widgets/cli_widget.py @@ -3,7 +3,6 @@ from typing import Optional -from click import Context from guidata.config import CONF from guidata.configtools import get_font from qtpy import QtCore as QC @@ -70,25 +69,6 @@ def run_menu(self, point: QC.QPoint): """ self.menu.exec_(self.command_label.mapToGlobal(point)) - def get_run_options(self, test: Test): - """Get the run options for the current test. - - Args: - test: Test for which to get the run options. - - Returns: - str: The run options for the current test. - """ - ctx = Context(cli) - run_params = run.get_params(ctx) - run_options = "" - for param in run_params: - if param.name in test.run_opts: - opt_index = test.run_opts.index(param.name) - opt_str = f"{param.opts[0]} {test.run_opts[opt_index + 1]} " - run_options += opt_str - return run_options - def copy_command_line(self): """Copy the command line to the clipboard.""" app = QW.QApplication.instance() diff --git a/moduletester/gui/widgets/config_editor.py b/moduletester/gui/widgets/config_editor.py index 58cdd77..ba166f9 100644 --- a/moduletester/gui/widgets/config_editor.py +++ b/moduletester/gui/widgets/config_editor.py @@ -46,8 +46,7 @@ def save_config(self) -> None: QW.QMessageBox.question( self, "Overwrite configuration file?", - "Do you want to overwrite the existing file?\n" - f"{self.config_path}", + f"Do you want to overwrite the existing file?\n{self.config_path}", QW.QMessageBox.Yes | QW.QMessageBox.No, ) == QW.QMessageBox.Yes diff --git a/moduletester/gui/widgets/dockable_widget.py b/moduletester/gui/widgets/dockable_widget.py index b21f542..495cf9c 100644 --- a/moduletester/gui/widgets/dockable_widget.py +++ b/moduletester/gui/widgets/dockable_widget.py @@ -1,10 +1,6 @@ -from abc import ABC, abstractmethod from typing import Optional -import qtpy.QtCore as QC import qtpy.QtWidgets as QW -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QWidget from moduletester.gui.widgets.abstract_widget import AbstractQWidget diff --git a/moduletester/gui/widgets/editor_widget.py b/moduletester/gui/widgets/editor_widget.py index 02c7e0e..0addc40 100644 --- a/moduletester/gui/widgets/editor_widget.py +++ b/moduletester/gui/widgets/editor_widget.py @@ -11,8 +11,6 @@ from qtpy import QtGui as QG from qtpy import QtWidgets as QW -import moduletester.config as cfg -from moduletester.gui.states.signals import TMSignals from moduletester.gui.widgets.dockable_widget import DockableQWidget diff --git a/moduletester/gui/widgets/result_props_widget.py b/moduletester/gui/widgets/result_props_widget.py index 2f7b990..ffd3f69 100644 --- a/moduletester/gui/widgets/result_props_widget.py +++ b/moduletester/gui/widgets/result_props_widget.py @@ -10,7 +10,6 @@ from guidata.dataset.qtwidgets import DataSetEditGroupBox from qtpy import QtWidgets as QW -from moduletester.config import _ from moduletester.gui.widgets.dockable_widget import DockableQWidget from moduletester.model import ResultEnum, Test diff --git a/moduletester/gui/widgets/test_description_widget.py b/moduletester/gui/widgets/test_description_widget.py index d6f7f9d..e0303b9 100644 --- a/moduletester/gui/widgets/test_description_widget.py +++ b/moduletester/gui/widgets/test_description_widget.py @@ -5,11 +5,9 @@ from typing import Optional -from guidata.qthelpers import get_std_icon # type: ignore from qtpy import QtWidgets as QW from qtpy.QtWebEngineWidgets import QWebEnginePage # type: ignore -from moduletester.gui.states.signals import TMSignals from moduletester.gui.widgets.web_engine import SimpleWebViewer from moduletester.model import Test diff --git a/moduletester/gui/widgets/test_list_widget.py b/moduletester/gui/widgets/test_list_widget.py index dd077ce..1dd45b8 100644 --- a/moduletester/gui/widgets/test_list_widget.py +++ b/moduletester/gui/widgets/test_list_widget.py @@ -12,7 +12,6 @@ from qtpy import QtGui as QG from qtpy import QtWidgets as QW -from moduletester.config import _ from moduletester.gui.external.pyqtspinner import WaitingSpinner from moduletester.model import ( ModuleErrorType, diff --git a/moduletester/gui/widgets/web_engine.py b/moduletester/gui/widgets/web_engine.py index 66a9bd5..fab7b76 100644 --- a/moduletester/gui/widgets/web_engine.py +++ b/moduletester/gui/widgets/web_engine.py @@ -43,7 +43,9 @@ class SimpleWebViewer(QWEB.QWebEngineView): # type: ignore """ def __init__( - self, *args, web_actions: Optional[list[QAction | QWEB.QWebEnginePage.WebAction]] = None # type: ignore + self, + *args, + web_actions: Optional[list[QAction | QWEB.QWebEnginePage.WebAction]] = None, # type: ignore ): super().__init__(*args) self.protect_settings() @@ -101,7 +103,8 @@ def protect_profile(self): self.page().profile().setUrlRequestInterceptor(UrlBloquer(self)) def setup_menu( - self, web_actions: list[QWEB.QWebEnginePage.WebAction] # type: ignore + self, + web_actions: list[QWEB.QWebEnginePage.WebAction], # type: ignore ) -> QMenu: """Setup the context menu. diff --git a/moduletester/gui/window.py b/moduletester/gui/window.py index 003802e..00fae17 100644 --- a/moduletester/gui/window.py +++ b/moduletester/gui/window.py @@ -48,7 +48,7 @@ def __init__( self.setMinimumSize(800, 480) font = get_font(CONF, "codeeditor") - ffamily, fsize = font.family(), font.pointSize() + _ffamily, fsize = font.family(), font.pointSize() bgurl = Path(get_image_file_path("ModuleTester-watermark.png")).as_posix() # self.ss_nobg = f"QWidget {{ font-family: '{ffamily}'; font-size: {fsize}pt;}}" self.ss_nobg = f"QWidget {{ font-size: {fsize}pt;}}" @@ -130,7 +130,9 @@ def closeEvent(self, a0: QG.QCloseEvent) -> None: # pylint: disable=C0103 return super().closeEvent(a0) def save_alert(self): - """Display a message box to ask the user if he wants to save the current file.""" + """ + Display a message box to ask the user if he wants to save the current file. + """ save_mb = QW.QMessageBox( QW.QMessageBox.Warning, APP_NAME, @@ -373,7 +375,6 @@ def _resolve_moduletester_config_error(self, e: config.ConfigConflictError) -> b == QW.QMessageBox.Apply ) if response: - config.load_package_conf(config_path, resolve=True) config.save_config(config.PACKAGE_CONF, config_path) print(config_path) @@ -400,7 +401,8 @@ def _handle_missing_module(self, missing_modules: list[Module]) -> None: + _("Missing modules:") + f"\n\n{missing_modules_names}\n\n" + _( - "Do you want to reimport all tests and override the current file?" + "Do you want to reimport all tests" + " and override the current file?" ) ), buttons=QW.QMessageBox.Ok | QW.QMessageBox.Cancel, @@ -558,7 +560,6 @@ def save(self): if manager.moduletester_path is None: self.save_as() else: - self.apply_all_changes() manager.save() self.signals.SIG_FILE_LOADED.emit(manager.moduletester_path) @@ -625,8 +626,9 @@ def _check_exports(self, abs_out_basename: str, fmts: Iterable[str]) -> bool: self, "File already exists.", ( - f"The following files aleady exist: {files_str}. \n" - f"File {abs_out_basename} already exists, do you want to overwite it?" + f"The following files already exist: {files_str}.\n" + f"File {abs_out_basename} already exists," + " do you want to overwrite it?" ), buttons=QW.QMessageBox.Yes | QW.QMessageBox.No, defaultButton=QW.QMessageBox.No, diff --git a/moduletester/manager.py b/moduletester/manager.py index 2d8ddc2..d81dc95 100644 --- a/moduletester/manager.py +++ b/moduletester/manager.py @@ -7,7 +7,6 @@ import os import shutil from dataclasses import dataclass, field -from importlib import import_module from typing import Callable, Optional from guidata.guitest import get_test_package # type: ignore @@ -15,7 +14,6 @@ from moduletester import config as cfg from .model import Module, TestSuite -from .python_helpers import rst2odt from .serializer import dumper, loader CONTEXT_SETTINGS = dict( diff --git a/moduletester/python_helpers.py b/moduletester/python_helpers.py index 14b58c7..0feca3e 100644 --- a/moduletester/python_helpers.py +++ b/moduletester/python_helpers.py @@ -6,7 +6,7 @@ import sys from dataclasses import fields, is_dataclass from itertools import zip_longest -from typing import Any, Dict, Generator, List, Protocol, Tuple, overload +from typing import Any, Dict, Generator, List, Protocol, Tuple from bs4 import BeautifulSoup @@ -118,10 +118,7 @@ def setup_sphinx( try: outs, errs = proc.communicate(timeout=0.5) print( - ( - f"[STDOUT] > {outs.decode('utf-8')}" - f"[STDERR] > {errs.decode('utf-8')}" - ) + (f"[STDOUT] > {outs.decode('utf-8')}[STDERR] > {errs.decode('utf-8')}") ) except subprocess.TimeoutExpired: pass diff --git a/moduletester/tests/test_model.py b/moduletester/tests/test_model.py index 9c6a062..2e28e6e 100644 --- a/moduletester/tests/test_model.py +++ b/moduletester/tests/test_model.py @@ -1,8 +1,8 @@ """Tests for the model module (enums, dataclasses).""" + from __future__ import annotations from datetime import datetime, timedelta -from io import StringIO from moduletester.model import ( ModuleNotFoundType, diff --git a/moduletester/tests/test_serializer.py b/moduletester/tests/test_serializer.py index 7d6e1ab..6ad2fcf 100644 --- a/moduletester/tests/test_serializer.py +++ b/moduletester/tests/test_serializer.py @@ -1,13 +1,11 @@ """Tests for the serializer module.""" + from __future__ import annotations -import json from datetime import datetime, timedelta from moduletester.serializer import ( - DataclassSerializer, DateTimeSerializer, - ObjectSerializerBase, TimedeltaSerializer, ) diff --git a/pyproject.toml b/pyproject.toml index 659eea8..2b13163 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ requires-python = ">=3.9, <4" dependencies = [ "guidata >= 3.14", "QtPy >= 1.9", - "click", "pyqtwebengine", "pypandoc", "jinja2", @@ -49,7 +48,7 @@ moduletester-cli = "moduletester.manager:cli" moduletester = "moduletester.gui.main:run_gui" [project.optional-dependencies] -dev = ["black", "isort", "pylint", "pytest", "Coverage", "build"] +dev = ["ruff", "pylint", "pytest", "Coverage", "build"] doc = ["PyQt5", "sphinx>6", "pydata_sphinx_theme"] [tool.setuptools.packages.find] @@ -73,3 +72,29 @@ version = { attr = "moduletester.__version__" } [tool.pytest.ini_options] testpaths = ["moduletester/tests"] + +[tool.ruff] +exclude = [".git", ".vscode", "build", "dist", "venv*", ".venv*"] +line-length = 88 +indent-width = 4 +target-version = "py39" + +[tool.ruff.lint] +select = [ + "E", # Pycodestyle error + "F", # Pyflakes + "I", # Isort + "W", # Pycodestyle warning +] +ignore = [ + "E203", # space before : (needed for how black formats slicing) +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.ruff.lint.per-file-ignores] +"doc/*" = ["E402"] diff --git a/scripts/run_black_isort.bat b/scripts/run_black_isort.bat deleted file mode 100644 index 9193cd1..0000000 --- a/scripts/run_black_isort.bat +++ /dev/null @@ -1,18 +0,0 @@ -@echo off -REM This script was derived from PythonQwt project -REM ====================================================== -REM Run black and isort code analysis tool -REM ====================================================== -REM Licensed under the terms of the MIT License -REM Copyright (c) 2020 Pierre Raybaut -REM (see PythonQwt LICENSE file for more details) -REM ====================================================== -setlocal -call %~dp0utils GetScriptPath SCRIPTPATH -call %FUNC% GetLibName LIBNAME -call %FUNC% SetPythonPath -set PYTHON=%CDL_PYTHONEXE% -call %FUNC% UsePython -python -m black . -python -m isort --profile black . -call %FUNC% EndOfScript \ No newline at end of file From 3dcd44029b7bcab1a95f44f5cc0d47601d912efa Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Wed, 20 May 2026 18:03:04 +0200 Subject: [PATCH 7/9] fix some easy pylint linter warnings --- moduletester/config.py | 11 ++++++++++- moduletester/gui/__init__.py | 2 ++ moduletester/gui/components/body_component.py | 2 +- moduletester/gui/components/test_list_component.py | 2 ++ moduletester/gui/external/pyqtspinner/__init__.py | 2 ++ moduletester/gui/external/pyqtspinner/configurator.py | 5 +++++ moduletester/gui/widgets/abstract_widget.py | 2 ++ moduletester/gui/widgets/config_editor.py | 4 ++++ moduletester/gui/widgets/dock_wrapper.py | 4 ++++ moduletester/gui/widgets/dockable_widget.py | 4 ++++ moduletester/gui/widgets/editor_widget.py | 6 ++++++ moduletester/gui/widgets/toolbox_widget.py | 2 ++ moduletester/gui/widgets/web_engine.py | 4 +++- moduletester/model.py | 2 +- moduletester/new_exporter.py | 3 +++ moduletester/test_exporter.py | 7 +++++++ moduletester/tests/test_model.py | 1 + moduletester/tests/test_serializer.py | 1 + 18 files changed, 60 insertions(+), 4 deletions(-) diff --git a/moduletester/config.py b/moduletester/config.py index 7e17704..dbfaf13 100644 --- a/moduletester/config.py +++ b/moduletester/config.py @@ -1,3 +1,5 @@ +"""Configuration management for ModuleTester.""" + from __future__ import annotations import configparser @@ -153,7 +155,8 @@ def _load_conf(config: configparser.ConfigParser, resolve=False) -> None: resolve: If True, tries to resolve conflicts in the configuration """ for section_name, section_obj in PACKAGE_CONF.items(): - section_values: configparser.SectionProxy = config.setdefault(section_name, {}) # type: ignore + # type: ignore + section_values: configparser.SectionProxy = config.setdefault(section_name, {}) missing_args, extra_args = _check_section(section_obj, **section_values) if len(missing_args) > 0 or len(extra_args) > 0: if resolve: @@ -261,15 +264,19 @@ def __post_init__(self): self.export_fmts = export_fmts def get_template_dir(self) -> str: + """Return the absolute path to the template directory.""" return os.path.join(MODULETESTER_CONFIG_DIR, self.template_dir) def get_docx_ref(self) -> str: + """Return the path to the DOCX reference template.""" return osp.join(self.template_dir, self.docx_reference) def get_odt_ref(self) -> str: + """Return the path to the ODT reference template.""" return osp.join(self.template_dir, self.odt_reference) def get_css_style(self) -> str: + """Return the path to the CSS style file.""" return osp.join(self.template_dir, self.css_style) def _to_abs_path(self, relative_path: str) -> str: @@ -367,6 +374,7 @@ def conf_obj_to_str(conf: ConfModel) -> str: def save_config(conf: ConfModel, filename: str) -> None: + """Save configuration to file.""" global PACKAGE_CONF PACKAGE_CONF.update(conf) with open(filename, "w") as f: @@ -374,5 +382,6 @@ def save_config(conf: ConfModel, filename: str) -> None: def reset_config(self): + """Reset configuration to defaults.""" global PACKAGE_CONF PACKAGE_CONF.update(new_config()) diff --git a/moduletester/gui/__init__.py b/moduletester/gui/__init__.py index 127a241..e2519a3 100644 --- a/moduletester/gui/__init__.py +++ b/moduletester/gui/__init__.py @@ -1,3 +1,5 @@ # -*- coding: utf-8 -*- +"""ModuleTester GUI package.""" + # pylint: disable=unused-import from moduletester import config as config # noqa: F401 diff --git a/moduletester/gui/components/body_component.py b/moduletester/gui/components/body_component.py index 05dd2c0..a17e314 100644 --- a/moduletester/gui/components/body_component.py +++ b/moduletester/gui/components/body_component.py @@ -214,7 +214,7 @@ def set_item( self.test_list.update_result(test) self.signals.SIG_PROJECT_MODIFIED.emit() - def update_result(self, index: int): + def update_result(self, _index: int): """Update the result for the test. Args: diff --git a/moduletester/gui/components/test_list_component.py b/moduletester/gui/components/test_list_component.py index 530286d..55bcd4a 100644 --- a/moduletester/gui/components/test_list_component.py +++ b/moduletester/gui/components/test_list_component.py @@ -1,3 +1,5 @@ +"""Test list dockable component.""" + from __future__ import annotations from typing import Optional diff --git a/moduletester/gui/external/pyqtspinner/__init__.py b/moduletester/gui/external/pyqtspinner/__init__.py index b938dfc..0c2f242 100644 --- a/moduletester/gui/external/pyqtspinner/__init__.py +++ b/moduletester/gui/external/pyqtspinner/__init__.py @@ -1 +1,3 @@ +"""Bundled pyqtspinner widget.""" + from .spinner import WaitingSpinner # noqa: F401 diff --git a/moduletester/gui/external/pyqtspinner/configurator.py b/moduletester/gui/external/pyqtspinner/configurator.py index ac5e6e6..2aae950 100644 --- a/moduletester/gui/external/pyqtspinner/configurator.py +++ b/moduletester/gui/external/pyqtspinner/configurator.py @@ -1,3 +1,5 @@ +"""Spinner configurator widget.""" + # noqa import math import sys @@ -23,6 +25,8 @@ # pylint: disable=too-many-instance-attributes,too-many-statements class SpinnerConfigurator(QWidget): + """Interactive configurator for the WaitingSpinner widget.""" + sb_roundness = None sb_opacity = None sb_fadeperc = None @@ -204,6 +208,7 @@ def show_init_args(self) -> None: def main(): + """Launch the spinner configurator application.""" app = QApplication(sys.argv) configurator = SpinnerConfigurator() # noqa sys.exit(app.exec()) diff --git a/moduletester/gui/widgets/abstract_widget.py b/moduletester/gui/widgets/abstract_widget.py index 07fb6c9..426b237 100644 --- a/moduletester/gui/widgets/abstract_widget.py +++ b/moduletester/gui/widgets/abstract_widget.py @@ -1,3 +1,5 @@ +"""Abstract base widget classes.""" + from abc import ABC, ABCMeta import qtpy.QtWidgets as QW diff --git a/moduletester/gui/widgets/config_editor.py b/moduletester/gui/widgets/config_editor.py index ba166f9..21d141e 100644 --- a/moduletester/gui/widgets/config_editor.py +++ b/moduletester/gui/widgets/config_editor.py @@ -1,3 +1,5 @@ +"""Configuration editor widget.""" + from __future__ import annotations import os @@ -11,6 +13,8 @@ class ConfigEditor(Editor): + """Widget for editing ModuleTester configuration files.""" + def __init__( self, parent: QWidget | None, diff --git a/moduletester/gui/widgets/dock_wrapper.py b/moduletester/gui/widgets/dock_wrapper.py index 2cd169a..d872ef2 100644 --- a/moduletester/gui/widgets/dock_wrapper.py +++ b/moduletester/gui/widgets/dock_wrapper.py @@ -1,3 +1,5 @@ +"""QDockWidget wrapper for dockable widgets.""" + from __future__ import annotations from typing import Generic, Optional, TypeVar @@ -18,6 +20,8 @@ class QDockWrapper(QW.QDockWidget, Generic[AnyDockableWidget]): + """QDockWidget wrapper for dockable widgets.""" + def __init__( self, parent: Optional[QW.QWidget], diff --git a/moduletester/gui/widgets/dockable_widget.py b/moduletester/gui/widgets/dockable_widget.py index 495cf9c..a3bcd26 100644 --- a/moduletester/gui/widgets/dockable_widget.py +++ b/moduletester/gui/widgets/dockable_widget.py @@ -1,3 +1,5 @@ +"""Dockable widget base class.""" + from typing import Optional import qtpy.QtWidgets as QW @@ -6,6 +8,8 @@ class DockableQWidget(AbstractQWidget): + """QWidget with a title label, usable as a dockable panel.""" + def __init__(self, parent: Optional[QW.QWidget], title: str = "") -> None: """Normal QWidget with a title and a label. This class is meant to be used as a base class for optionnally dockable widgets by being wrapped into a diff --git a/moduletester/gui/widgets/editor_widget.py b/moduletester/gui/widgets/editor_widget.py index 0addc40..5282f47 100644 --- a/moduletester/gui/widgets/editor_widget.py +++ b/moduletester/gui/widgets/editor_widget.py @@ -1,3 +1,5 @@ +"""Text editor widget with save/close support.""" + from __future__ import annotations from abc import ABC @@ -15,6 +17,8 @@ class DialogEditor(codeeditor.CodeEditor): + """Code editor with save and content sync signals.""" + sig_update_content = QC.Signal() # type: ignore sig_save_key = QC.Signal() # type: ignore @@ -30,11 +34,13 @@ def __init__( self.content_is_synched = True def closeEvent(self, event: QCloseEvent) -> None: # type: ignore # noqa: N802 + """Handle close event, emitting update signal if content changed.""" if not self.content_is_synched: self.sig_update_content.emit() super().closeEvent(event) def keyPressEvent(self, event: QG.QKeyEvent): # type: ignore # noqa: N802 + """Handle key press, detecting Ctrl+S for save.""" super().keyPressEvent(event) if ( event.modifiers() == QC.Qt.ControlModifier diff --git a/moduletester/gui/widgets/toolbox_widget.py b/moduletester/gui/widgets/toolbox_widget.py index f033b44..7e5b779 100644 --- a/moduletester/gui/widgets/toolbox_widget.py +++ b/moduletester/gui/widgets/toolbox_widget.py @@ -1,3 +1,5 @@ +"""Toolbox widget for collapsible sections.""" + from __future__ import annotations from PyQt5.QtWidgets import QWidget diff --git a/moduletester/gui/widgets/web_engine.py b/moduletester/gui/widgets/web_engine.py index fab7b76..28d8d1c 100644 --- a/moduletester/gui/widgets/web_engine.py +++ b/moduletester/gui/widgets/web_engine.py @@ -1,3 +1,5 @@ +"""Web engine widget for HTML content display.""" + from __future__ import annotations from typing import Optional @@ -45,7 +47,7 @@ class SimpleWebViewer(QWEB.QWebEngineView): # type: ignore def __init__( self, *args, - web_actions: Optional[list[QAction | QWEB.QWebEnginePage.WebAction]] = None, # type: ignore + web_actions: Optional[list[QAction | QWEB.QWebEnginePage.WebAction]] = None, ): super().__init__(*args) self.protect_settings() diff --git a/moduletester/model.py b/moduletester/model.py index d230278..7632db5 100644 --- a/moduletester/model.py +++ b/moduletester/model.py @@ -209,7 +209,7 @@ def __new__(cls, label: str, icon_name: Optional[str] = None): obj.icon_path = get_image_file_path(icon_name or "") return obj - def __init__(self, label: str, __ignored=None) -> None: ... + def __init__(self, _label: str, __ignored=None) -> None: ... """Fake init method used to get the correct linting/auto-completion.""" diff --git a/moduletester/new_exporter.py b/moduletester/new_exporter.py index 3185e05..dc88ab6 100644 --- a/moduletester/new_exporter.py +++ b/moduletester/new_exporter.py @@ -1,3 +1,5 @@ +"""Jinja2-based document exporter for test results and test lists.""" + from __future__ import annotations import os @@ -302,4 +304,5 @@ def __init_subclass__(cls) -> None: @classmethod def load_template(cls) -> None: + """Load the Jinja2 template from the configured directory.""" cls._template = JINJA_ENV.get_template(cls.template_name) diff --git a/moduletester/test_exporter.py b/moduletester/test_exporter.py index 30d8e22..0cb8961 100644 --- a/moduletester/test_exporter.py +++ b/moduletester/test_exporter.py @@ -1,3 +1,5 @@ +"""Test results and test list document generation.""" + from __future__ import annotations from dataclasses import dataclass, field @@ -48,6 +50,7 @@ def __post_init__(self): JINJA_ENV.loader = new_template_loader def get_images_paths(self, test: Test) -> list[str]: + """Return the image paths for a given test.""" if self.test_suite is None: return [] return test.get_images(self._image_dirs) @@ -55,9 +58,13 @@ def get_images_paths(self, test: Test) -> list[str]: @dataclass class TestResultsDocument(_TestExporter): + """Exporter for test results documents.""" + template_name: str = "test_results_template.j2" @dataclass class TestListDocument(_TestExporter): + """Exporter for test list documents.""" + template_name: str = "test_list_template.j2" diff --git a/moduletester/tests/test_model.py b/moduletester/tests/test_model.py index 2e28e6e..fb9bf45 100644 --- a/moduletester/tests/test_model.py +++ b/moduletester/tests/test_model.py @@ -1,5 +1,6 @@ """Tests for the model module (enums, dataclasses).""" +# pylint: disable=missing-class-docstring,missing-function-docstring from __future__ import annotations from datetime import datetime, timedelta diff --git a/moduletester/tests/test_serializer.py b/moduletester/tests/test_serializer.py index 6ad2fcf..5e9b00c 100644 --- a/moduletester/tests/test_serializer.py +++ b/moduletester/tests/test_serializer.py @@ -1,5 +1,6 @@ """Tests for the serializer module.""" +# pylint: disable=missing-class-docstring,missing-function-docstring from __future__ import annotations from datetime import datetime, timedelta From a27c61564826fd9f58f83857f5d42753548ec495 Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Wed, 20 May 2026 18:22:56 +0200 Subject: [PATCH 8/9] update documentation --- doc/changelog.rst | 14 +++++++++ doc/conf.py | 9 ++++-- doc/index.rst | 6 ++-- doc/installation.rst | 41 +++++++++++++++++++-------- doc/requirements.rst | 35 +++++++++++------------ doc/update_requirements.py | 11 +++++--- doc/usage.rst | 58 ++++++++++++++++++++++++++++++++++++-- 7 files changed, 132 insertions(+), 42 deletions(-) create mode 100644 doc/changelog.rst diff --git a/doc/changelog.rst b/doc/changelog.rst new file mode 100644 index 0000000..3a34dc5 --- /dev/null +++ b/doc/changelog.rst @@ -0,0 +1,14 @@ +Changelog +========= + +See the full changelog on `GitHub `_. + + +Version 1.0.0 +------------- + +First public release of ModuleTester. + +New features, bug fixes, and improvements — see the +`CHANGELOG.md `_ +for the complete list. diff --git a/doc/conf.py b/doc/conf.py index 913bc05..7ff14c4 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -13,13 +13,18 @@ project = "ModuleTester" author = "Pierre Raybaut" -copyright = "2023, Codra - " + author +copyright = "2023-2026, Codra - " + author html_logo = latex_logo = "_static/ModuleTester-title.png" release = moduletester.__version__ # -- General configuration --------------------------------------------------- -extensions = ["sphinx.ext.intersphinx", "sphinx.ext.napoleon", "sphinx.ext.mathjax"] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.napoleon", + "sphinx.ext.mathjax", +] templates_path = ["_templates"] exclude_patterns = [] diff --git a/doc/index.rst b/doc/index.rst index 60d4c61..b7a499a 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -39,16 +39,16 @@ External resources: installation usage example + changelog Copyrights and licensing ------------------------ -- Copyright © 2023 `Codra`_ +- Copyright © 2023-2026 `Codra`_ - Licensed under the terms of the `BSD 3-Clause`_ -.. _PlotPyStack: https://github.com/PlotPyStack .. _guidata: https://pypi.python.org/pypi/guidata .. _PyPI: https://pypi.python.org/pypi/ModuleTester .. _GitHub: https://github.com/Codra-Ingenierie-Informatique/ModuleTester .. _Codra: https://codra.net/ -.. _BSD 3-Clause: https://github.com/Codra-Ingenierie-Informatique/DataLab/blob/master/LICENSE +.. _BSD 3-Clause: https://github.com/Codra-Ingenierie-Informatique/ModuleTester/blob/main/LICENSE diff --git a/doc/installation.rst b/doc/installation.rst index ad66224..f6b5af3 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -8,31 +8,43 @@ Dependencies .. note:: - Python 3.11 and PyQt5 are the reference for production release + Python 3.9+ is required. PyQt5 or PySide2 can be used as Qt binding + (through `QtPy `_). How to install -------------- -Wheel package: -^^^^^^^^^^^^^^ +From PyPI: +^^^^^^^^^^ -On any operating system, using pip and the Wheel package is the easiest way to -install ModuleTester on an existing Python distribution: +The easiest way to install ModuleTester is from PyPI: .. code-block:: console - $ pip install --upgrade ModuleTester-1.0.0-py2.py3-none-any.whl + $ pip install ModuleTester + +From a wheel package: +^^^^^^^^^^^^^^^^^^^^^ + +On any operating system, using pip and the Wheel package: + +.. code-block:: console + + $ pip install --upgrade moduletester-1.0.0-py3-none-any.whl + +Pandoc (optional): +^^^^^^^^^^^^^^^^^^ ModuleTester uses Pandoc and PyPandoc bindings to generate documents and display test descriptions. You can get `Pandoc `_ from -`here `_ or by executing the following python code ( -All instructions are available on the +`here `_ or by executing the following python code +(all instructions are available on the `PyPandoc documentation `_). .. code-block:: python - pip install pypandoc + import pypandoc from pypandoc.pandoc_download import download_pandoc # see the documentation how to customize the installation path # but be aware that you then need to include it in the `PATH` @@ -41,11 +53,16 @@ All instructions are available on the print(pypandoc.get_pandoc_path()) +From source: +^^^^^^^^^^^^ + +Installing ModuleTester directly from the source package is straightforward: + +.. code-block:: console -Source package: -^^^^^^^^^^^^^^^ + $ pip install . -Installing ModuleTester directly from the source package is straigthforward: +Or to build a distribution package: .. code-block:: console diff --git a/doc/requirements.rst b/doc/requirements.rst index 76482c9..ae4ca2d 100644 --- a/doc/requirements.rst +++ b/doc/requirements.rst @@ -1,4 +1,4 @@ -The :mod:`moduletester` package requires the following Python modules: +The `ModuleTester` package requires the following Python modules: .. list-table:: :header-rows: 1 @@ -8,17 +8,14 @@ The :mod:`moduletester` package requires the following Python modules: - Version - Summary * - Python - - >=3.8, <4 - - - * - guidata - - >= 3.3 - - - * - QtPy + - >=3.9, <4 + - Python programming language + * - guidata + - >= 3.14 + - Automatic GUI generation for easy dataset editing and display + * - QtPy - >= 1.9 - - - * - click - - - - Composable command line interface toolkit + - Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6). * - pyqtwebengine - - Python bindings for the Qt WebEngine framework @@ -31,9 +28,6 @@ The :mod:`moduletester` package requires the following Python modules: * - beautifulsoup4 - - Screen-scraping library - * - PyQt5 - - >=5.11 - - Python bindings for the Qt cross platform application toolkit Optional modules for development: @@ -44,18 +38,21 @@ Optional modules for development: * - Name - Version - Summary - * - black - - - - The uncompromising code formatter. - * - isort + * - ruff - - - A Python utility / library to sort Python imports. + - An extremely fast Python linter and code formatter, written in Rust. * - pylint - - python code static checker + * - pytest + - + - pytest: simple powerful testing with Python * - Coverage - - Code coverage measurement for Python + * - build + - + - A simple, correct Python build frontend Optional modules for building the documentation: diff --git a/doc/update_requirements.py b/doc/update_requirements.py index fdefd02..f572be9 100644 --- a/doc/update_requirements.py +++ b/doc/update_requirements.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -"""Update requirements.rst file from pyproject.toml or setup.cfg file +"""Update requirements.rst file from pyproject.toml file. Warning: this has to be done manually at release time. It is not done automatically by the sphinx 'conf.py' file because it @@ -9,11 +9,14 @@ without internet connection like the Debian package management infrastructure). """ -from guidata.utils.genreqs import gen_module_req_rst # noqa: E402 +import os -import moduletester +from guidata.utils.genreqs import generate_requirements_rst # noqa: E402 if __name__ == "__main__": print("Updating requirements.rst file...", end=" ") - gen_module_req_rst(moduletester, ["Python>=3.8", "PyQt5>=5.11"]) + root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + pyproject_path = os.path.join(root_dir, "pyproject.toml") + doc_dir = os.path.join(root_dir, "doc") + generate_requirements_rst(pyproject_path, doc_dir) print("done.") diff --git a/doc/usage.rst b/doc/usage.rst index 023590f..913b012 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -1,6 +1,60 @@ Usage ===== -.. note:: +ModuleTester can be used either as a GUI application or as a command-line tool. - Work in progress. \ No newline at end of file + +Graphical User Interface +------------------------ + +To launch the graphical interface: + +.. code-block:: console + + $ moduletester + +Or from Python: + +.. code-block:: python + + from moduletester.gui.main import run_gui + run_gui() + +You can also open a project file or target a specific Python package directly: + +.. code-block:: console + + $ moduletester --path /path/to/project.mtf + $ moduletester --module mypackage + + +Command-Line Interface +---------------------- + +ModuleTester provides a CLI for running tests without the GUI: + +.. code-block:: console + + $ moduletester-cli --help + +Run all tests of a Python package: + +.. code-block:: console + + $ moduletester-cli run mypackage + +Export results to an HTML report: + +.. code-block:: console + + $ moduletester-cli export mypackage --output report.html + + +Configuration +------------- + +ModuleTester stores its configuration in a user-specific directory +managed by `guidata `_. + +The configuration can be edited from the GUI via the **Settings** menu +or by editing the configuration file directly through the built-in editor. \ No newline at end of file From 14a75fca6968a5b35f149bde6b8d9531fb484c3c Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Wed, 20 May 2026 18:29:02 +0200 Subject: [PATCH 9/9] add github ci workflow --- .github/workflows/build_deploy.yml | 42 ++++++++++++++++++++++++++++++ .github/workflows/test.yml | 40 ++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 .github/workflows/build_deploy.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/build_deploy.yml b/.github/workflows/build_deploy.yml new file mode 100644 index 0000000..dd3ca2e --- /dev/null +++ b/.github/workflows/build_deploy.yml @@ -0,0 +1,42 @@ +name: Build and upload to PyPI + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + test: + uses: ./.github/workflows/test.yml + + deploy: + needs: test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build guidata babel + + - name: Compile translations + run: | + python -m guidata.utils.translations compile --name moduletester --directory . + + - name: Build package + run: python -m build + + - name: Publish package + uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..7d8c7f0 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,40 @@ +name: Tests + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + python-version: ["3.9", "3.13"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[dev] + + - name: Lint with Ruff + run: ruff check --output-format=github moduletester + + - name: Test with pytest + run: pytest -v --tb=long