Source code for schrodinger.application.matsci.codeutils

"""
Module for utilities related to code maintenance

Copyright Schrodinger, LLC. All rights reserved.
"""
import functools
import importlib
import pathlib
import os
import warnings

import decorator

MATSCI_MODULE_BASE = 'schrodinger.application.matsci.'
THIRD_PARTY_MODULE_DIRS = ['qexsd', 'qb_sdk', 'qeschema']
DESMOND_PACKAGES = [
    'analysis', 'cui', 'energygroup', 'parch', 'pfx', 'staf', 'timer', 'topo',
    'traj', 'traj_util', 'viparr', 'msys'
]


[docs]def check_moved_variables(var_name, moved_variables): """ Check if the target variable has been moved, and if yes, post a warning about it and return the variable in the new module Raises AttributeError if a moved variable isn't found. :param str var_name: Name of the target variable :param tuple moved_variables: Tuple of tuples. Each inner tuple has a format of (module, remove_release, variables), where `module` is the new module name, `remove_release` is the release in which the link will stop working, and `variables` are the set of variables that were moved :raise AttributeError: If `var_name` is not a moved variable :rtype: Any :return: The moved variable """ for new_module_name, remove_release, variables in moved_variables: if var_name not in variables: continue # The variable was moved. Show a warning and return the new variable. if not new_module_name.startswith('schrodinger'): # Convert to full path new_module_name = MATSCI_MODULE_BASE + new_module_name msg = ( f"'{var_name}' has been moved to the '{new_module_name}' module. The" f" old usage will stop functioning in {remove_release} release.") warnings.warn(msg, FutureWarning, stacklevel=3) new_module = importlib.import_module(new_module_name) return getattr(new_module, var_name) raise AttributeError
[docs]@decorator.decorator def deprecate(func, to_remove_in=None, replacement=None, *args, **kwargs): """ Post a warning about the function being deprecated :param callable func: The function that is deprecated :param str to_remove_in: The release in which the function will be removed :param callable replacement: The function to call instead """ def name(x): # qualname includes the method's class too return f"{x.__module__}.{x.__qualname__}" msg = (f"{name(func)} is deprecated and will be " f"removed in {to_remove_in} release. ") if replacement: msg += f'Please use {name(replacement)} instead.' warnings.warn(msg, FutureWarning, stacklevel=3) return func(*args, **kwargs)
[docs]def is_python_file(path): """ Return whether the passed path is a python file :param str path: The file path :rtype: bool :return: Whether the path is a python file """ return os.path.splitext(path)[1].lower() == '.py'
[docs]def get_matsci_module_paths(): """ Return a dict of file paths and dot paths of all matsci modules, sorted :return dict: A dict mapping file paths to dot paths """ base_dir = os.path.dirname(__file__) file_path_to_dot_path = {} for root, _, files in os.walk(base_dir): if any([dir_name in root for dir_name in THIRD_PARTY_MODULE_DIRS]): continue for afile in files: if not is_python_file(afile) or afile == '__init__.py': continue abs_path = os.path.join(root, afile) rel_path = os.path.relpath(abs_path, start=base_dir) dot_path = (MATSCI_MODULE_BASE + '.'.join(pathlib.Path(rel_path).parts))[:-3] file_path_to_dot_path[abs_path] = dot_path return dict(sorted(file_path_to_dot_path.items()))
[docs]class MissingModule: """ Dummy class to return instead of missing modules. Will raise if any attribute is accessed. """ def __getattr__(self, name): raise ImportError('Unable to import desmond packages.')
[docs]def get_safe_package(name): """ Get a desmond or jaguar package without raising if the package doesn't exist :param str name: "namespace.package" where namespace is either desmond or jaguar :raises ValueError: If the namespace is not included or correct :raises ImportError: If the package name is incorrect :rtype: module or MissingModule :return: The module or a MissingModule object """ if name.startswith('desmond.'): package = name.split('.')[-1] if package in DESMOND_PACKAGES: try: return importlib.import_module( "schrodinger.application.desmond.packages." + package) except ImportError as err: return MissingModule() else: raise ImportError( f'"{package}" is not the name of a desmond package.') else: raise ValueError('The name should start with "desmond" or "jaguar".')
[docs]class readonly_cached_property(functools.cached_property): """ A cached property that cannot be set or deleted """ def __set__(self, instance, value): msg = f'Assignment of "{self.attrname}" is not supported' raise AttributeError(msg) def __delete__(self, instance): msg = f'Deletion of "{self.attrname}" is not supported' raise AttributeError(msg)
[docs]class frozenset_cached_property(functools.cached_property): """ A cached property that automatically converts the property into a frozenset. Useful for ensuring that a `set` property is immutable. """ def __get__(self, *args, **kwargs): prop = super().__get__(*args, **kwargs) return frozenset(prop)