diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca1260af3f..a5a753fce4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -310,6 +310,12 @@ jobs: run: | uv pip install pyinstaller + - name: Generate the embedded credits file for all packages + if: ${{ matrix.installer }} + run: | + uv pip install pip-licenses + python build_tools/compile_licenses.py installers/credits.html + - name: Build sasview with pyinstaller if: ${{ matrix.installer }} run: | diff --git a/.gitignore b/.gitignore index a5e95879cd..589e8dabd6 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,4 @@ tests.log /installers/build /installers/dist *.exe +installers/credits.html diff --git a/build_tools/compile_licenses.py b/build_tools/compile_licenses.py new file mode 100755 index 0000000000..c11d3187c8 --- /dev/null +++ b/build_tools/compile_licenses.py @@ -0,0 +1,128 @@ +#!/usr/bin/python3 + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +from mako.template import Template + +# Define Mako template for HTML output +html_template = Template( + r""" + + + + SasView Dependencies + + + +

SasView Dependencies

+ SasView is built upon a foundation of free and open-source software packages. + % if minimal: + The following modules are part of this release of SasView. + % else: + The following modules are used as part of the build process and are bundled + in the binary distributions that are released. + % endif + + + % for pkg in modules: +

${pkg['Name']}

+

Author(s): ${pkg.get('Author', 'N/A')}

+

License: ${pkg.get('License', 'N/A')}

+
${pkg.get('LicenseText', 'No license text available.')}
+ % endfor + + +""" +) + + +# minimal list of modules to include when distributing only in wheel form +minimal_modules = [ + "sasdata", + "sasmodels", + "sasview", +] + + +def get_modules(): + """Load pip-licenses JSON output""" + result = subprocess.run( + [ + "pip-licenses", + "--format=json", + "--with-system", + "--with-authors", + "--with-license-file", + ], + capture_output=True, + text=True, + check=True, + ) + modules = json.loads(result.stdout) + # Sort the data by package name (case-insensitive) + modules.sort(key=lambda pkg: pkg["Name"].lower()) + return modules + + +def filter_modules(modules, included): + """Filter the list of packages to only include the specified packages""" + return [m for m in modules if m["Name"].lower() in included] + + +def format_html(filename, modules, minimal): + """Create the HTML output""" + # Render the template with license data + html_output = html_template.render(modules=modules, minimal=minimal) + + # Save the HTML to a file + Path(filename).write_text(html_output, encoding="utf-8") + + +def main(argv): + parser = argparse.ArgumentParser( + description="Extract license information for modules in the environment", + ) + + parser.add_argument( + "--minimal", + action="store_true", + help="Only include information about a minimal set of modules", + ) + parser.add_argument( + "filename", + help="output filename", + metavar="OUTPUT", + ) + + args = parser.parse_args(argv) + + minimal = args.minimal + filename = args.filename + + modules = get_modules() + + if minimal: + modules = filter_modules(modules, minimal_modules) + + format_html(filename, modules, minimal) + + return True + + +if __name__ == "__main__": + sys.exit(not main(sys.argv[1:])) diff --git a/build_tools/release_automation.py b/build_tools/release_automation.py index 6eb2ffd476..a444fd5d01 100644 --- a/build_tools/release_automation.py +++ b/build_tools/release_automation.py @@ -3,13 +3,14 @@ import json import logging import os +import subprocess import sys from csv import DictReader from pathlib import Path import requests -from sas.system.legal import legal +from sas.system import legal USAGE = '''This script should be run from one directory above the base sasview directory. This script also requires both sasmodels and sasdata repositories to be in the same directory as the sasview repository. @@ -293,6 +294,17 @@ def update_file(license_file: Path, license_line: str, line_to_update: int): f.writelines(output_lines) +def update_credits(credits_file: Path): + """Update the credits.html file with relevant license info""" + subprocess.check_call( + [ + sys.executable, + "--minimal", + "build_tools/release_automation.py", + credits_file, + ]) + + def update_acknowledgement_widget(): """ @@ -308,7 +320,7 @@ def prepare_release_notes(issues_list, repository, username, password): """ issue_titles = [] for issue in issues_list: - # WARNING: One can try running with auth but there is limitted number of requests + # WARNING: One can try running with auth but there is limited number of requests response = requests.get('https://api.github.com/repos/SasView/' + repository + '/issues/' + issue, auth=(username, password)) if response.status_code != 200: @@ -365,6 +377,7 @@ def main(args=None): update_file(SASMODELS_PATH / 'LICENSE.txt', license_line, 0) update_file(SASDATA_PATH / 'LICENSE.TXT', license_line, 0) update_file(SASVIEW_PATH / 'installers' / 'license.txt', license_line, -1) + update_credits(SASVIEW_PATH / "src" / "sas" / "system" / "credits.html") sasview_issues_list = args.sasview_list sasmodels_issues_list = args.sasmodels_list diff --git a/build_tools/requirements-dev.txt b/build_tools/requirements-dev.txt index 71a966d47d..f5ddedb017 100644 --- a/build_tools/requirements-dev.txt +++ b/build_tools/requirements-dev.txt @@ -1,4 +1,8 @@ -build +# The list of dependencies needed to build sasview, this should be kept in +# sync with pyproject.toml's build-system.requires list. +# Additional tools for developers can be listed in this file. +# Developers may also want the test dependencies in requirements-test.txt + bumps dominate hatchling @@ -8,7 +12,6 @@ hatch-sphinx hatch-vcs html2text numpy -pre-commit periodictable pyopengl pyside6 @@ -17,3 +20,9 @@ scipy superqt twisted uncertainties + +# not build-dependencies but important for developers +build # build is special: it doesn't get listed in build-system.requires but is needed +pip-licenses +pre-commit +-r requirements-test.txt diff --git a/installers/sasview.spec b/installers/sasview.spec index c33e093a70..d20dc4d606 100644 --- a/installers/sasview.spec +++ b/installers/sasview.spec @@ -17,6 +17,9 @@ datas.append((os.path.join(PYTHON_PACKAGES, 'jedi'), 'jedi')) datas.append((os.path.join(PYTHON_PACKAGES, 'zmq'), 'zmq')) datas.append(('../src/sas/example_data', './example_data')) +# clobber the minimal file with the full one for the installer bundle +datas.append(('credits.html', 'sas/system/')) + def add_data(data): for component in data: target = component[0] diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py index 5903e9f265..3937c9e307 100644 --- a/src/sas/qtgui/MainWindow/GuiManager.py +++ b/src/sas/qtgui/MainWindow/GuiManager.py @@ -40,6 +40,7 @@ from sas.qtgui.Perspectives.perspective import Perspective from sas.qtgui.Perspectives.SizeDistribution.SizeDistributionPerspective import SizeDistributionWindow from sas.qtgui.Utilities.About.About import About +from sas.qtgui.Utilities.About.Credits import Credits # from sas.qtgui.Utilities.DocViewWidget import DocViewWindow from sas.qtgui.Utilities.FileConverter import FileConverterWidget @@ -775,6 +776,7 @@ def addTriggers(self): self._workspace.actionModel_Marketplace.triggered.connect(self.actionMarketplace) self._workspace.actionAcknowledge.triggered.connect(self.actionAcknowledge) self._workspace.actionAbout.triggered.connect(self.actionAbout) + self._workspace.actionCredits.triggered.connect(self.actionCredits) self._workspace.actionWelcomeWidget.triggered.connect(self.actionWelcome) self._workspace.actionCheck_for_update.triggered.connect(self.actionCheck_for_update) self._workspace.actionWhat_s_New.triggered.connect(self.actionWhatsNew) @@ -1318,6 +1320,14 @@ def actionAbout(self): about = About() about.exec() + def actionCredits(self): + """ + Open the Credits/Licenses box + """ + # TODO: proper sizing + credits = Credits() + credits.exec() + def actionCheck_for_update(self): """ Menu Help/Check for Update diff --git a/src/sas/qtgui/MainWindow/PackageGatherer.py b/src/sas/qtgui/MainWindow/PackageGatherer.py index 38da8127b9..89655026b0 100644 --- a/src/sas/qtgui/MainWindow/PackageGatherer.py +++ b/src/sas/qtgui/MainWindow/PackageGatherer.py @@ -1,8 +1,8 @@ +import importlib.metadata import logging -import pathlib +import pkgutil import sys - -import pkg_resources +import sysconfig import sas import sas.system.version @@ -31,14 +31,14 @@ def log_installed_modules(self): # Get python modules installed locally - installed_packages = pkg_resources.working_set - + pkgdetails = [] + for dist in importlib.metadata.distributions(): + pkgdetails.append(f"{dist.metadata['Name']}: {dist.version}") python_str = f'python:{sys.version}\n' - print_str = "\n".join(f"{package.key}: {package.version}" for package in installed_packages) - msg = f"Installed packages:\n{python_str+print_str}" + print_str = "\n".join(sorted(pkgdetails)) + msg = f"Installed packages:\n{python_str}{print_str}" logger.info(msg) - def log_imported_packages(self): """ Log version number of python packages imported in this instance of SasView. @@ -50,16 +50,16 @@ def log_imported_packages(self): """ imported_packages_dict = self.get_imported_packages() - res_str = "\n".join(f"{module}: {version_num}" for module, version_num - in imported_packages_dict["results"].items()) - no_res_str = "\n".join(f"{module}: {version_num}" for module, version_num - in imported_packages_dict["no_results"].items()) - errs_res_str = "\n".join(f"{module}: {version_num}" for module, version_num - in imported_packages_dict["errors"].items()) + def fmt(section): + s = [] + for module in sorted(imported_packages_dict[section]): + s.append(f"{module}: {imported_packages_dict[section][module]}") + return "\n".join(s) - msg = f"Imported modules:\n {res_str}\n {no_res_str}\n {errs_res_str}" - logger.info(msg) + sections = ("results", "no_results", "errors") + msg = f"Imported modules:\n{'\n\n'.join(fmt(s) for s in sections)}" + logger.info(msg) def get_imported_packages(self): """ Get a dictionary of imported package version numbers @@ -76,8 +76,8 @@ def get_imported_packages(self): no_version_list = [] # Generate a list of standard modules by looking at the local python library try: - standard_lib = [path.stem.split('.')[0] for path in pathlib.Path(pathlib.__file__) - .parent.absolute().glob('*')] + stdlib_path = sysconfig.get_paths()["stdlib"] + standard_lib = [name for _, name, _ in pkgutil.iter_modules([stdlib_path])] except Exception: standard_lib = ['abc', 'aifc', 'antigravity', 'argparse', 'ast', 'asynchat', 'asyncio', 'asyncore', 'base64', 'bdb', 'binhex', 'bisect', 'bz2', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmd', @@ -109,10 +109,18 @@ def get_imported_packages(self): standard_lib.extend(sys.builtin_module_names) standard_lib.append("sas") - for module_name in sys.modules.keys(): + # extract all module distributions already known + dists = importlib.metadata.packages_distributions() + + for module_name in list(sys.modules): package_name = module_name.split('.')[0] + # skip modules that start with _ as they are internal implementation details + # of the real modules and not interesting. + if package_name.startswith("_"): + continue + # A built in python module or a local file, which have no version, only the python/SasView version if package_name in standard_lib or package_name in package_versions_dict: continue @@ -136,12 +144,13 @@ def get_imported_packages(self): f"version using .__version__" pass - # Retrieving the modules version using the pkg_resources package + # Retrieving the modules version from importlib.metadata # Unreliable, so second option try: - package_versions_dict[package_name] = pkg_resources.get_distribution(package_name).version + dist_name = dists[package_name][0] + package_versions_dict[package_name] = importlib.metadata.distribution(dist_name).version except Exception: - # Modules that cannot be found by pkg_resources + # Modules that cannot be found by importlib.metadata pass else: continue @@ -171,7 +180,6 @@ def get_imported_packages(self): return {"results": package_versions_dict, "no_results": no_version_dict, "errors": err_version_dict} - def remove_duplicate_modules(self, modules_dict): """ Strip duplicate instances of each module @@ -191,7 +199,7 @@ def remove_duplicate_modules(self, modules_dict): """ output_dict = dict() - for module_name in modules_dict.keys(): + for module_name in modules_dict: parent_module = module_name.split('.')[0] # Save one instance of each module if parent_module not in output_dict: @@ -204,7 +212,6 @@ def remove_duplicate_modules(self, modules_dict): return output_dict - def format_no_version_list(self, modules_dict, no_version_list): """ Format module names in the no_version_list list @@ -229,7 +236,7 @@ def format_no_version_list(self, modules_dict, no_version_list): for module_name in no_version_list: parent_module = module_name.split('.')[0] # Version number exists for this module - if parent_module in modules_dict.keys(): + if parent_module in modules_dict: pass # Module is already in output_list elif parent_module in output_dict: diff --git a/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui b/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui index 3829434509..66c6c97829 100755 --- a/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui +++ b/src/sas/qtgui/MainWindow/UI/MainWindowUI.ui @@ -159,6 +159,7 @@ + @@ -517,6 +518,11 @@ About + + + Credits and licenses + + Check for update diff --git a/src/sas/qtgui/Utilities/About/Credits.py b/src/sas/qtgui/Utilities/About/Credits.py new file mode 100644 index 0000000000..cb4de44b3e --- /dev/null +++ b/src/sas/qtgui/Utilities/About/Credits.py @@ -0,0 +1,63 @@ +from PySide6.QtCore import QSize +from PySide6.QtGui import QIcon +from PySide6.QtWidgets import ( + QDialog, + QSizePolicy, + QTextBrowser, + QVBoxLayout, +) + +from sas.system import SAS_RESOURCES + + +class Credits(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + + self.setWindowTitle("Credits and Licences for SasView") + + icon = QIcon() + icon.addFile(":/res/ball.ico", QSize(), QIcon.Normal, QIcon.Off) + self.setWindowIcon(icon) + + self.mainLayout = QVBoxLayout() + self.creditsText = QTextBrowser() + + self.creditsText.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + self.setText() + + self.mainLayout.addWidget(self.creditsText) + + self.setLayout(self.mainLayout) + self.setModal(True) + + if parent is not None: + # SasView doesn't set parents though? + w = int(parent.width() * 0.33) + h = int(parent.height() * 0.75) + self.resize(w, h) + else: + w = 800 + h = 800 + self.resize(w, h) + + def setText(self): + """ + Modify the labels so the text corresponds to the current version + """ + with SAS_RESOURCES.resource("system/credits.html") as path: + credits = path.read_text() + self.creditsText.setText(credits) + + +if __name__ == "__main__": + import sys + + from PySide6.QtWidgets import QApplication + + app = QApplication([]) + + credits = Credits() + credits.show() + + sys.exit(app.exec()) diff --git a/src/sas/qtgui/Utilities/Reports/reports.py b/src/sas/qtgui/Utilities/Reports/reports.py index 4f95415c90..1887563373 100644 --- a/src/sas/qtgui/Utilities/Reports/reports.py +++ b/src/sas/qtgui/Utilities/Reports/reports.py @@ -1,6 +1,5 @@ import base64 import datetime -import importlib.resources as pkg_resources import logging import os import sys @@ -20,6 +19,7 @@ from sas.qtgui.Plotting.PlotterBase import Data1D from sas.qtgui.Utilities import GuiUtils from sas.qtgui.Utilities.Reports.reportdata import ReportData +from sas.system import SAS_RESOURCES logger = logging.getLogger(__name__) @@ -100,8 +100,8 @@ def __init__(self, tags.link(rel="stylesheet", href=style_link) else: - style_data = pkg_resources.read_text("sas.qtgui.Utilities.Reports", "report_style.css") - tags.style(style_data) + with SAS_RESOURCES.resource("qtgui/Utilities/Reports/report_style.css") as res_path: + tags.style(res_path.read_text()) with self._html_doc.body: with tags.div(id="main"): diff --git a/src/sas/system/credits.html b/src/sas/system/credits.html new file mode 100644 index 0000000000..e72101c527 --- /dev/null +++ b/src/sas/system/credits.html @@ -0,0 +1,93 @@ + + + + + SasView Dependencies + + + +

SasView Dependencies

+ SasView is built upon a foundation of free and open-source software packages. + The following modules are part of this release of SasView. + + +

sasdata

+

Author(s): SasView Team

+

License: BSD License

+
Copyright (c) 2009-2025, SasView Developers
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+

sasmodels

+

Author(s): SasView Team

+

License: Public Domain

+
Copyright (c) 2009-2025, SasView Developers
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+may be used to endorse or promote products derived from this software without
+specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+

sasview

+

Author(s): SasView Team

+

License: BSD License

+
Copyright (c) 2009-2025, SasView Developers
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+ + diff --git a/src/sas/system/legal.py b/src/sas/system/legal.py index 2868421728..0d1ce73162 100644 --- a/src/sas/system/legal.py +++ b/src/sas/system/legal.py @@ -1,4 +1,7 @@ import datetime +from pathlib import Path + +from ._resources import SAS_RESOURCES class Legal: @@ -6,5 +9,9 @@ def __init__(self): year = datetime.datetime.now().year self.copyright = f"Copyright (c) 2009-{year}, SasView Developers" + @property + def credits_html(self) -> Path: + return SAS_RESOURCES.resource("system/credits.html") + legal = Legal() diff --git a/src/sas/system/resources.py b/src/sas/system/resources.py index 7a0354f573..646a376208 100644 --- a/src/sas/system/resources.py +++ b/src/sas/system/resources.py @@ -35,6 +35,7 @@ # glob matching; until we're using that, we have some messier regular # expressions in the code below. +import contextlib import enum import functools import importlib.metadata @@ -42,6 +43,8 @@ import itertools import logging import re +import tempfile +from collections.abc import Generator from pathlib import Path, PurePath logger = logging.getLogger(__name__) @@ -106,6 +109,32 @@ def extract_resource_tree(self, src: str | PurePath, dest: Path | str) -> bool: raise NotADirectoryError(f"Resource tree {src} not found in module {self.module}") + @contextlib.contextmanager + def resource(self, src: str | PurePath) -> Generator[Path, None, None]: + """Provide a filesystem path to a file resource, in a temporary directory if needed + + If the resource is already available on the filesystem, then provide + the path to it directly; if it is available as an extractable resource, + then provide it in a temporary directory that will get cleaned up + when the context manager is completed. + + If the resource can't be found by any means, then a FileNotFoundError + is raised. + """ + # step 1: look for the resource already on disk + try: + path = self.path_to_resource(src) + yield path + return + except FileNotFoundError: + pass + + # step 2: if not already on disk then it's time for a temp dir + with tempfile.TemporaryDirectory() as tmp_dir: + tmp_path = Path(tmp_dir) / Path(src).name + self.extract_resource(src, tmp_path) + yield tmp_path + def path_to_resource(self, src: str | PurePath) -> Path: """Provide the filesystem path to a file resource diff --git a/test/system/utest_resources.py b/test/system/utest_resources.py index bc4a1b735b..fb07700249 100644 --- a/test/system/utest_resources.py +++ b/test/system/utest_resources.py @@ -67,7 +67,19 @@ def test_path_to_resource_directory(self, input_type): assert Path(str(pth)).is_dir() assert len(list(pth.iterdir())) > 10 - def test_resource(self, tmp_path, input_type): + def test_resource(self, input_type): + """Test extracting single resource recorded by installed module""" + resources = sas.system.resources.ModuleResources("sas") + + source = Path("docs/index.html") + src = input_type(source) + + with resources.resource(src) as dest: + assert dest.exists() + assert dest.as_posix().endswith(source.name) + assert dest.stat().st_size > 1000 + + def test_extract_resource(self, tmp_path, input_type): """Test extracting single resource recorded by installed module""" resources = sas.system.resources.ModuleResources("sas") @@ -83,7 +95,7 @@ def test_resource(self, tmp_path, input_type): assert resources.extract_resource(src, dest) assert dest.stat().st_size > 1000 - def test_resource_tree(self, tmp_path, input_type): + def test_extract_resource_tree(self, tmp_path, input_type): """Test extracting resource tree recorded by installed module""" resources = sas.system.resources.ModuleResources("sas") @@ -133,7 +145,19 @@ def test_path_to_resource_directory(self, input_type): assert Path(str(pth)).is_dir() assert len(list(pth.iterdir())) > 10 - def test_resource(self, tmp_path, input_type): + def test_resource(self, input_type): + """Test extracting single resource recorded by installed module""" + resources = sas.system.resources.ModuleResources("sas") + + source = Path("qtgui/images/ball.ico") + src = input_type(source) + + with resources.resource(src) as dest: + assert dest.exists() + assert dest.as_posix().endswith(source.name) + assert dest.stat().st_size > 1000 + + def test_extract_resource(self, tmp_path, input_type): """Test extracting single resource adjacent to code in module""" resources = sas.system.resources.ModuleResources("sas") @@ -149,7 +173,7 @@ def test_resource(self, tmp_path, input_type): assert resources.extract_resource(src, dest) assert dest.stat().st_size > 1000 - def test_resource_tree(self, tmp_path, input_type): + def test_extract_resource_tree(self, tmp_path, input_type): """Test extracting resource tree adjacent to code in module""" resources = sas.system.resources.ModuleResources("sas") @@ -183,7 +207,7 @@ def test_nonexisting_path_to_resource(self, input_type): for pth in self.check_paths: with pytest.raises(FileNotFoundError): src = input_type(pth) - assert resources.path_to_resource(src) + resources.path_to_resource(src) def test_nonexisting_extract_resource(self, tmp_path, input_type): """Test exception raised for extracting non-existent resource""" @@ -193,7 +217,17 @@ def test_nonexisting_extract_resource(self, tmp_path, input_type): for pth in self.check_paths: with pytest.raises(FileNotFoundError): src = input_type(pth) - assert resources.extract_resource(src, dest) + resources.extract_resource(src, dest) + + def test_nonexisting_resource(self, input_type): + """Test exception raised for extracting non-existent resource""" + resources = sas.system.resources.ModuleResources("sas") + + for pth in self.check_paths: + with pytest.raises(FileNotFoundError): + src = input_type(pth) + with resources.resource(src): + pass def test_nonexisting_extract_resource_tree(self, tmp_path, input_type): """Test exception raised for extracting non-existent resource tree""" @@ -203,4 +237,4 @@ def test_nonexisting_extract_resource_tree(self, tmp_path, input_type): for pth in self.check_paths + ["sas/docs/"]: with pytest.raises(NotADirectoryError): src = input_type(pth) - assert resources.extract_resource_tree(src, dest) + resources.extract_resource_tree(src, dest)