Skip to content

Commit

Permalink
Merge pull request #1961 from SasView/revert-1958-revert-1903-1869_WI…
Browse files Browse the repository at this point in the history
…P_easily_accessible_dependencies

Second attempt for easily accessible dependencies
  • Loading branch information
Wojciech Potrzebowski authored Nov 28, 2021
2 parents dc31ff4 + 3a2a1b0 commit afc53ff
Show file tree
Hide file tree
Showing 3 changed files with 304 additions and 0 deletions.
28 changes: 28 additions & 0 deletions src/sas/qtgui/MainWindow/GuiManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from sas.qtgui.MainWindow.AboutBox import AboutBox
from sas.qtgui.MainWindow.WelcomePanel import WelcomePanel
from sas.qtgui.MainWindow.CategoryManager import CategoryManager
from sas.qtgui.MainWindow.PackageGatherer import PackageGatherer

from sas.qtgui.MainWindow.DataManager import DataManager

Expand Down Expand Up @@ -456,6 +457,7 @@ def quitApplication(self):

# Exit if yes
if reply == QMessageBox.Yes:

# save the paths etc.
self.saveCustomConfig()
reactor.callFromThread(reactor.stop)
Expand Down Expand Up @@ -484,6 +486,18 @@ def checkUpdate(self):
except ValueError as ex:
logging.info("Failed to connect to www.sasview.org:", ex)

def log_installed_packages(self):
"""
Log version number of locally installed python packages
"""
PackageGatherer().log_installed_packages()

def log_imported_packages(self):
"""
Log version number of python packages imported in this instance of SasView.
"""
PackageGatherer().log_imported_packages()

def processVersion(self, version_info):
"""
Call-back method for the process of checking for updates.
Expand Down Expand Up @@ -644,6 +658,8 @@ def addTriggers(self):
self._workspace.actionAbout.triggered.connect(self.actionAbout)
self._workspace.actionWelcomeWidget.triggered.connect(self.actionWelcome)
self._workspace.actionCheck_for_update.triggered.connect(self.actionCheck_for_update)
self._workspace.actionLog_installed_packages.triggered.connect(self.actionLog_installed_packages)
self._workspace.actionLog_imported_packages.triggered.connect(self.actionLog_imported_packages)

self.communicate.sendDataToGridSignal.connect(self.showBatchOutput)
self.communicate.resultPlotUpdateSignal.connect(self.showFitResults)
Expand Down Expand Up @@ -1165,6 +1181,18 @@ def actionCheck_for_update(self):
"""
self.checkUpdate()

def actionLog_installed_packages(self):
"""
Log version number of locally installed python packages
"""
self.log_installed_packages()

def actionLog_imported_packages(self):
"""
Log version number of python packages imported in this instance of SasView.
"""
self.log_imported_packages()

def updateTheoryFromPerspective(self, index):
"""
Catch the theory update signal from a perspective
Expand Down
263 changes: 263 additions & 0 deletions src/sas/qtgui/MainWindow/PackageGatherer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import sys
import logging
import subprocess
import pkg_resources
import json
import pathlib

import sas



class PackageGatherer:
""" A class used to gather packages/modules used by SasView and their current installed version
:method log_installed_packages: Log version number of locally installed python packages
:method log_imported_packages: Log version number of python packages imported in this instance of SasView.
:method get_imported_packages: Get a dictionary of imported module version numbers
:method remove_duplicate_modules: Strip duplicate instances of each module
:method format_unattainable_packages_list: Format module names in the unattainable_modules list
"""

def log_installed_modules(self):
""" Log version number of locally installed python packages
Use pip list to create a dictionary of installed modules as the keys, with their respective version numbers
as the values. Only packages available through pip will be included.
:returns:Nothing
:rtype: None
"""
installed_packages = {'python': sys.version}

# Get python modules installed locally
installed_packages_json = json.loads(subprocess.check_output("pip list -l --format=json"))
for package in installed_packages_json:
installed_packages[package['name']] = package['version']

print_str = "\n".join(f"{key}: {value}" for key, value in installed_packages.items())
logging.info(f"Installed packages:\n{print_str}")


def log_imported_packages(self):
""" Log version number of python packages imported in this instance of SasView.
Use the get_imported_packages method to to create a dictionary of installed modules as the keys, with their
respective version numbers as the values. There may be some packages whose version number is unattainable.
:returns: Nothing
:rtype: None
"""
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())

logging.info(f"Imported modules:\n"
f"{res_str}\n"
f"{no_res_str}\n"
f"{errs_res_str}")


def get_imported_packages(self):
""" Get a dictionary of imported package version numbers
Use a variety of method, for example a module.version call, to attempt to get the module version of each
module that has been imported in this instance of running SasView. The sys.modules command lists the
imported modules. A list of modules whose version number cannot be found is also included.
:returns: A dictionary with the package names as the key, with their respective version numbers as the value.
:rtype: dict
"""
package_versions_dict = {'python': sys.version, 'SasView': sas.sasview.__version__}
err_version_dict = {}
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('*')]
except Exception:
standard_lib = ['abc', 'aifc', 'antigravity', 'argparse', 'ast', 'asynchat', 'asyncio', 'asyncore',
'base64', 'bdb', 'binhex', 'bisect', 'bz2', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmd',
'code', 'codecs', 'codeop', 'collections', 'colorsys', 'compileall', 'concurrent',
'configparser', 'contextlib', 'contextvars', 'copy', 'copyreg', 'cProfile', 'crypt',
'csv', 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbm', 'decimal', 'difflib',
'dis', 'distutils', 'doctest', 'email', 'encodings', 'ensurepip', 'enum', 'filecmp',
'fileinput', 'fnmatch', 'formatter', 'fractions', 'ftplib', 'functools', 'genericpath',
'getopt', 'getpass', 'gettext', 'glob', 'graphlib', 'gzip', 'hashlib', 'heapq', 'hmac',
'html', 'http', 'idlelib', 'imaplib', 'imghdr', 'imp', 'importlib', 'inspect', 'io',
'ipaddress', 'json', 'keyword', 'lib2to3', 'linecache', 'locale', 'logging', 'lzma',
'mailbox', 'mailcap', 'mimetypes', 'modulefinder', 'msilib', 'multiprocessing', 'netrc',
'nntplib', 'ntpath', 'nturl2path', 'numbers', 'opcode', 'operator', 'optparse', 'os',
'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform', 'plistlib',
'poplib', 'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pyclbr', 'pydoc',
'pydoc_data', 'py_compile', 'queue', 'quopri', 'random', 're', 'reprlib', 'rlcompleter',
'runpy', 'sched', 'secrets', 'selectors', 'shelve', 'shlex', 'shutil', 'signal',
'site-packages', 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', 'sqlite3',
'sre_compile', 'sre_constants', 'sre_parse', 'ssl', 'stat', 'statistics', 'string',
'stringprep', 'struct', 'subprocess', 'sunau', 'symbol', 'symtable', 'sysconfig',
'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'test', 'textwrap', 'this', 'threading',
'timeit', 'tkinter', 'token', 'tokenize', 'trace', 'traceback', 'tracemalloc', 'tty',
'turtle', 'turtledemo', 'types', 'typing', 'unittest', 'urllib', 'uu', 'uuid', 'venv',
'warnings', 'wave', 'weakref', 'webbrowser', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc',
'zipapp', 'zipfile', 'zipimport', 'zoneinfo', '_aix_support', '_bootlocale',
'_bootsubprocess', '_collections_abc', '_compat_pickle', '_compression', '_markupbase',
'_osx_support', '_pydecimal', '_pyio', '_py_abc', '_sitebuiltins', '_strptime',
'_threading_local', '_weakrefset', '__future__', '__phello__', '__pycache__']
standard_lib.extend(sys.builtin_module_names)
standard_lib.append("sas")

for module_name in sys.modules.keys():

package_name = module_name.split('.')[0]

# 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

# Modules that require specific methods to get the version number
if "PyQt5" in module_name:
try:
from PyQt5.QtCore import PYQT_VERSION_STR
except Exception as e:
# Unable to access PyQt5
err_version_dict[module_name] = f"Unknown: {e} when attempting to get PyQt5 version"
pass
else:
package_versions_dict["PyQt"] = PYQT_VERSION_STR
try:
from PyQt5.QtCore import QT_VERSION_STR
except Exception as e:
# Unable to access Qt
err_version_dict[module_name] = f"Unknown: {e} when attempting to get Qt version"
pass
else:
package_versions_dict["Qt"] = QT_VERSION_STR
continue

# Import module
try:
package = __import__(package_name)
except Exception as e:
err_version_dict[package_name] = f"Unknown: {e} when attempting to import module"
continue

# Retrieving the modules version using the __version__ attribute
if hasattr(package, '__version__'):
# Module has __version__ attribute
try:
package_versions_dict[package_name] = package.__version__
continue
except Exception as e:
# Unable to access module
err_version_dict[package_name] = f"Unknown: {e} when attempting to access {package_name} " \
f"version using .__version__"
pass

# Retrieving the modules version using the pkg_resources package
# Unreliable, so second option
try:
package_versions_dict[package_name] = pkg_resources.get_distribution(package_name).version
except Exception:
# Modules that cannot be found by pkg_resources
pass
else:
continue

# Modules version number could not be attained by any of the previous methods

no_version_list.append(package_name)

# Currently not required for any packages used by SasView
# Retrieving the modules version using the version attribute
# if hasattr(package, 'version'):
# # Module has version attribute
# try:
# if isinstance(package.version, str):
# print(package)
# package_versions_dict[package_name] = package.version
# continue
# except Exception as e:
# # Unable to access module
# err_version_dict[package_name] = f"Unknown: {e} when attempting to access {package_name} " \
# f"version using .version"
# pass

# Clean up
package_versions_dict = self.remove_duplicate_modules(package_versions_dict)
no_version_dict = self.format_no_version_list(package_versions_dict, no_version_list)

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
Multiple instances of one module can be keys of the dictionary of module version numbers generated by the
method get_imported_packages. This is because if an individual class is imported from a module, then each class
would be listed in sys.modules. For example the command from PyQt5.QtWidgets import QMainWindow, QMdiArea
lead to both QMainWindow and QMdiArea being keys, when in reality they are both part of PyQt5. This method
save the first instance of each module, unless the version numbers are different.
:param modules_dict: A dictionary with the module names as the key, with their respective version numbers as
the value.
:type modules_dict: dict
:return: A reduced / cleaned dictionary with the module names as the key, with their respective version
numbers as the value.
:rtype: dict
"""
output_dict = dict()

for module_name in modules_dict.keys():
parent_module = module_name.split('.')[0]
# Save one instance of each module
if parent_module not in output_dict.keys():
output_dict[parent_module] = modules_dict[module_name]
else:
# Modules versions are not the same
if output_dict[parent_module] != modules_dict[module_name]:
output_dict[f"{parent_module}_from_{module_name}"] = modules_dict[module_name]
pass

return output_dict


def format_no_version_list(self, modules_dict, no_version_list):
""" Format module names in the no_version_list list
The unattainable_modules is a list of modules whose version number could not be found. This method rename each
module in the unattainable_modules to it's parent modules name, remove modules that already have a version
number and remove duplicate modules from the no_version_list list. Entries may appear in the
no_version_list if they are a class in a module, and the version number could not be ascertained from
the class.
:param modules_dict: A dictionary with the module names as the key, with their respective version numbers as
the value.
:type modules_dict: dict
:param no_version_list: A list of modules whose version number could not be found.
:type no_version_list: list
:return: A reduced / clean list of modules whose version number could not be found
:rtype: dict
"""

output_dict = {}

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():
pass
# Module is already in output_list
elif parent_module in output_dict.keys():
pass
# Append module to output_list
else:
output_dict[module_name] = f"Unknown: Version number could not be found"

return output_dict
13 changes: 13 additions & 0 deletions src/sas/qtgui/MainWindow/UI/MainWindowUI.ui
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@
<addaction name="actionWelcomeWidget"/>
<addaction name="separator"/>
<addaction name="actionCheck_for_update"/>
<addaction name="separator"/>
<addaction name="actionLog_installed_packages"/>
<addaction name="actionLog_imported_packages"/>
</widget>
<addaction name="menu_File"/>
<addaction name="menuEdit"/>
Expand Down Expand Up @@ -514,6 +517,16 @@
<string>Check for update</string>
</property>
</action>
<action name="actionLog_installed_packages">
<property name="text">
<string>Log installed packages</string>
</property>
</action>
<action name="actionLog_imported_packages">
<property name="text">
<string>Log imported packages</string>
</property>
</action>
<action name="actionExcel">
<property name="text">
<string>Excel</string>
Expand Down

0 comments on commit afc53ff

Please sign in to comment.