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""" + +
+ +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 @@
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. ++
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. ++
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)