Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 33 additions & 26 deletions src/sas/qtgui/MainWindow/PackageGatherer.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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
Expand All @@ -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',
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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

Expand All @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions src/sas/qtgui/Utilities/Reports/reports.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import base64
import datetime
import importlib.resources as pkg_resources
import logging
import os
import sys
Expand All @@ -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__)

Expand Down Expand Up @@ -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"):
Expand Down
29 changes: 29 additions & 0 deletions src/sas/system/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@
# 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
import importlib.resources
import itertools
import logging
import re
import tempfile
from collections.abc import Generator
from pathlib import Path, PurePath

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -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

Expand Down
48 changes: 41 additions & 7 deletions test/system/utest_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand All @@ -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")

Expand Down Expand Up @@ -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")

Expand All @@ -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")

Expand Down Expand Up @@ -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"""
Expand All @@ -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"""
Expand All @@ -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)