From b9479bffba4d57936169c1a0bfb45c9af5ff2895 Mon Sep 17 00:00:00 2001 From: Marti Municoy Date: Thu, 10 Sep 2020 19:46:19 +0200 Subject: [PATCH 1/4] Prints to logging.info messages --- offpele/main.py | 49 +++++++------ offpele/solvent/solvent.py | 4 +- offpele/topology/molecule.py | 37 ++++++---- offpele/utils/utils.py | 129 ++++++++++++++++++++++++++++++++++- 4 files changed, 180 insertions(+), 39 deletions(-) diff --git a/offpele/main.py b/offpele/main.py index da4492c6..23e073ab 100644 --- a/offpele/main.py +++ b/offpele/main.py @@ -13,7 +13,7 @@ import argparse as ap import offpele -from offpele.utils import check_if_path_exists, create_path +from offpele.utils import check_if_path_exists, create_path, Logger DEFAULT_OFF_FORCEFIELD = 'openff_unconstrained-1.2.0.offxml' @@ -177,27 +177,24 @@ def run_offpele(pdb_file, forcefield=DEFAULT_OFF_FORCEFIELD, Whether to save output files following PELE's DataLocal hierarchy or not """ - print('-' * 60) - print('Open Force Field parameterizer for PELE', offpele.__version__) - print('-' * 60) - print(' - General:') - print(' - Input PDB:', pdb_file) - print(' - Output path:', output) - print(' - Write solvent parameters:', with_solvent) - print(' - DataLocal-like output:', as_datalocal) - print(' - Parameterization:') - print(' - Force field:', forcefield) - print(' - Charges method:', charges_method) - print(' - Use OPLS nonbonding parameters:', use_OPLS_nb_params) - print(' - Use OPLS bonds and angles:', use_OPLS_bonds_and_angles) - print(' - Rotamer library:') - print(' - Resolution:', resolution) - print(' - Exclude terminal rotamers:', exclude_terminal_rotamers) - print('-' * 60) - - # Supress OpenForceField toolkit warnings - import logging - logging.getLogger().setLevel(logging.ERROR) + log = Logger() + log.info('-' * 60) + log.info('Open Force Field parameterizer for PELE', offpele.__version__) + log.info('-' * 60) + log.info(' - General:') + log.info(' - Input PDB:', pdb_file) + log.info(' - Output path:', output) + log.info(' - Write solvent parameters:', with_solvent) + log.info(' - DataLocal-like output:', as_datalocal) + log.info(' - Parameterization:') + log.info(' - Force field:', forcefield) + log.info(' - Charges method:', charges_method) + log.info(' - Use OPLS nonbonding parameters:', use_OPLS_nb_params) + log.info(' - Use OPLS bonds and angles:', use_OPLS_bonds_and_angles) + log.info(' - Rotamer library:') + log.info(' - Resolution:', resolution) + log.info(' - Exclude terminal rotamers:', exclude_terminal_rotamers) + log.info('-' * 60) from offpele.topology import Molecule from offpele.template import Impact @@ -247,6 +244,14 @@ def main(): exclude_terminal_rotamers = not args.include_terminal_rotamers + # Supress OpenForceField toolkit warnings + import logging + logging.getLogger().setLevel(logging.ERROR) + + # Set offpele logger to INFO level + logger = Logger() + logger.set_level('INFO') + run_offpele(args.pdb_file, args.forcefield, args.resolution, args.charges_method, args.use_OPLS_nb_params, args.use_OPLS_bonds_and_angles, exclude_terminal_rotamers, diff --git a/offpele/solvent/solvent.py b/offpele/solvent/solvent.py index 217429bc..422383d4 100644 --- a/offpele/solvent/solvent.py +++ b/offpele/solvent/solvent.py @@ -7,6 +7,7 @@ from offpele.utils import get_data_file_path, warning_on_one_line from offpele.utils.toolkits import OpenForceFieldToolkitWrapper +from offpele.utils import Logger class _SolventWrapper(object): @@ -42,7 +43,8 @@ def _initialize_from_molecule(self): """ Initializes a SolventWrapper object using an offpele's Molecule. """ - print(' - Loading solvent parameters') + logger = Logger() + logger.info(' - Loading solvent parameters') off_toolkit = OpenForceFieldToolkitWrapper() diff --git a/offpele/topology/molecule.py b/offpele/topology/molecule.py index 7b30ac02..60c2c49c 100644 --- a/offpele/topology/molecule.py +++ b/offpele/topology/molecule.py @@ -12,6 +12,7 @@ SchrodingerToolkitWrapper) from offpele.charge import (Am1bccCalculator, GasteigerCalculator, OPLSChargeCalculator) +from offpele.utils import Logger class Atom(object): @@ -562,37 +563,39 @@ def _initialize_from_pdb(self, path): path : str The path to a PDB with the molecule structure """ - print(' - Initializing molecule from PDB') + logger = Logger() + logger.info(' - Initializing molecule from PDB') self._initialize() - print(' - Loading molecule from RDKit') + logger.info(' - Loading molecule from RDKit') rdkit_toolkit = RDKitToolkitWrapper() self._rdkit_molecule = rdkit_toolkit.from_pdb(path) # Use RDKit template, if any, to assign the connectivity to # the current Molecule object if self.connectivity_template is not None: - print(' - Assigning connectivity from template') + logger.info(' - Assigning connectivity from template') rdkit_toolkit.assign_connectivity_from_template(self) # RDKit must generate stereochemistry specifically from 3D coords - print(' - Assigning stereochemistry from 3D coordinates') + logger.info(' - Assigning stereochemistry from 3D coordinates') rdkit_toolkit.assign_stereochemistry_from_3D(self) # Set molecule name according to PDB name if self.name == '': from pathlib import Path name = Path(path).stem - print(' - Setting molecule name to \'{}\''.format(name)) + logger.info(' - Setting molecule name to \'{}\''.format(name)) self.set_name(name) # Set molecule tag according to PDB's residue name if self.tag == 'UNK': tag = rdkit_toolkit.get_residue_name(self) - print(' - Setting molecule tag to \'{}\''.format(tag)) + logger.info(' - Setting molecule tag to \'{}\''.format(tag)) self.set_tag(tag) - print(' - Representing molecule with the Open Force Field Toolkit') + logger.info(' - Representing molecule with the Open Force Field ' + + 'Toolkit') openforcefield_toolkit = OpenForceFieldToolkitWrapper() self._off_molecule = openforcefield_toolkit.from_rdkit(self) @@ -605,10 +608,11 @@ def _initialize_from_smiles(self, smiles): smiles : str The SMILES tag to construct the molecule structure with """ - print(' - Initializing molecule from a SMILES tag') + logger = Logger() + logger.info(' - Initializing molecule from a SMILES tag') self._initialize() - print(' - Loading molecule from RDKit') + logger.info(' - Loading molecule from RDKit') rdkit_toolkit = RDKitToolkitWrapper() self._rdkit_molecule = rdkit_toolkit.from_smiles(smiles) @@ -618,17 +622,19 @@ def _initialize_from_smiles(self, smiles): # Set molecule name according to the SMILES tag if self.name == '': - print(' - Setting molecule name to \'{}\''.format(smiles)) + logger.info(' - Setting molecule name to \'{}\''.format(smiles)) self.set_name(smiles) - print(' - Representing molecule with the Open Force Field Toolkit') + logger.info(' - Representing molecule with the Open Force Field ' + + 'Toolkit') openforcefield_toolkit = OpenForceFieldToolkitWrapper() self._off_molecule = openforcefield_toolkit.from_rdkit(self) def _build_rotamers(self): """It builds the rotamers of the molecule.""" + logger = Logger() if self.off_molecule and self.rdkit_molecule: - print(' - Generating rotamer library') + logger.info(' - Generating rotamer library') self._graph = MolecularGraph(self) self._rotamers = self._graph.get_rotamers() @@ -694,7 +700,8 @@ def parameterize(self, forcefield, charges_method=None, raise Exception('OpenForceField molecule was not initialized ' + 'correctly') - print(' - Loading forcefield') + logger = Logger() + logger.info(' - Loading forcefield') openforcefield_toolkit = OpenForceFieldToolkitWrapper() parameters = openforcefield_toolkit.get_parameters_from_forcefield( forcefield, self) @@ -706,8 +713,8 @@ def parameterize(self, forcefield, charges_method=None, charges_calculator = self._get_charges_calculator(charges_method) - print(' - Computing partial charges with ' - + '{}'.format(charges_calculator.name)) + logger.info(' - Computing partial charges with ' + + '{}'.format(charges_calculator.name)) self._assign_charges(charges_calculator) self._clean_lists() diff --git a/offpele/utils/utils.py b/offpele/utils/utils.py index 62487505..efd271f0 100644 --- a/offpele/utils/utils.py +++ b/offpele/utils/utils.py @@ -7,7 +7,8 @@ "temporary_cd", "warning_on_one_line", "check_if_path_exists", - "create_path" + "create_path", + "Logger" ] @@ -93,3 +94,129 @@ def create_path(path): The path that will be created """ os.makedirs(str(path), exist_ok=True) + + +class Logger(object): + """ + It contains all the required methods to handle logging messages. + """ + import logging + DEFAULT_LEVEL = logging.INFO + + def __init__(self): + """It initializes a Logger object""" + import logging + + # Get offpele logger and set level only the first time + if 'offpele_log' not in logging.root.manager.loggerDict: + self._logger = logging.getLogger('offpele_log') + self._logger.setLevel(self.DEFAULT_LEVEL) + else: + self._logger = logging.getLogger('offpele_log') + + # If no handler is found add stream handler + if not len(self._logger.handlers): + ch = logging.StreamHandler() + ch.setLevel(self.DEFAULT_LEVEL) + formatter = logging.Formatter('%(message)s') + ch.setFormatter(formatter) + self._logger.addHandler(ch) + + def set_level(self, level): + """ + It sets the logging level. + + Parameters + ---------- + level : str + The logging level to set. One of [DEBUG, INFO, WARNING, ERROR, + CRITICAL] + """ + import logging + + if level.upper() == 'DEBUG': + logging_level = logging.DEBUG + elif level.upper() == 'INFO': + logging_level = logging.INFO + elif level.upper() == 'WARNING': + logging_level = logging.WARNING + elif level.upper() == 'ERROR': + logging_level = logging.ERROR + elif level.upper() == 'CRITICAL': + logging_level = logging.CRITICAL + else: + raise ValueError('Invalid level type') + + self._logger.setLevel(logging_level) + for handler in self._logger.handlers: + handler.setLevel(logging_level) + + def debug(self, *messages): + """ + It pulls a debug message. + + Parameters + ---------- + messages : list[str] + The list of messages to print + """ + if len(messages) > 1: + self._logger.debug(' '.join(map(str, messages))) + else: + self._logger.debug(messages[0]) + + def info(self, *messages): + """ + It pulls an info message. + + Parameters + ---------- + messages : list[str] + The list of messages to print + """ + if len(messages) > 1: + self._logger.info(' '.join(map(str, messages))) + else: + self._logger.info(messages[0]) + + def warning(self, *messages): + """ + It pulls a warning message. + + Parameters + ---------- + messages : list[str] + The list of messages to print + """ + if len(messages) > 1: + self._logger.warning(' '.join(map(str, messages))) + else: + self._logger.warning(messages[0]) + + def error(self, *messages): + """ + It pulls a error message. + + Parameters + ---------- + messages : list[str] + The list of messages to print + """ + if len(messages) > 1: + self._logger.error(' '.join(map(str, messages))) + else: + self._logger.error(messages[0]) + + def critical(self, *messages): + """ + It pulls a critical message. + + Parameters + ---------- + messages : list[str] + The list of messages to print + """ + if len(messages) > 1: + self._logger.critical(' '.join(map(str, messages))) + else: + self._logger.critical(messages[0]) From 3c06df1057a68de2ababe3c22d1e4228a862a817 Mon Sep 17 00:00:00 2001 From: Marti Municoy Date: Mon, 14 Sep 2020 14:40:57 +0200 Subject: [PATCH 2/4] Add test for log --- offpele/tests/test_utils.py | 151 ++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 offpele/tests/test_utils.py diff --git a/offpele/tests/test_utils.py b/offpele/tests/test_utils.py new file mode 100644 index 00000000..3e72fd1b --- /dev/null +++ b/offpele/tests/test_utils.py @@ -0,0 +1,151 @@ +""" +This module contains the tests to check some handy classes and functions +of offpele. +""" + + +import pytest + +import io +from contextlib import redirect_stdout +from offpele.utils import Logger + + +class TestLogger(object): + def test_logger_levels(self): + """ + It checks the correct behaviour of the different log levels. + """ + def push_messages(log): + """Pull some messages at different levels.""" + log.debug('Debug message') + log.info('Info message') + log.warning('Warn message') + log.error('Error message') + log.critical('Critical message') + + import logging + + # Initiate logger + log = Logger() + + # Try the default level (INFO) + # Catch logger messages to string buffer + with io.StringIO() as buf: + # Add custom handler to logger + log_handler = logging.StreamHandler(buf) + log._logger.handlers = list() + log._logger.addHandler(log_handler) + + # Push messages + push_messages(log) + + # Get string from buffer + output = buf.getvalue() + + assert output == 'Info message\nWarn message\n' \ + + 'Error message\nCritical message\n', \ + 'Unexpected logger message at standard output' + + # Try DEBUG level + # Catch logger messages to string buffer + with io.StringIO() as buf: + # Add custom handler to logger + log_handler = logging.StreamHandler(buf) + log._logger.handlers = list() + log._logger.addHandler(log_handler) + + # Try DEBUG level + log.set_level('DEBUG') + + # Push messages + push_messages(log) + + # Get string from buffer + output = buf.getvalue() + + assert output == 'Debug message\nInfo message\n'\ + + 'Warn message\nError message\nCritical message\n', \ + 'Unexpected logger message at standard output' + + # Try INFO level + # Catch logger messages to string buffer + with io.StringIO() as buf: + # Add custom handler to logger + log_handler = logging.StreamHandler(buf) + log._logger.handlers = list() + log._logger.addHandler(log_handler) + + # Try INFO level + log.set_level('INFO') + + # Push messages + push_messages(log) + + # Get string from buffer + output = buf.getvalue() + + assert output == 'Info message\nWarn message\n' \ + + 'Error message\nCritical message\n', \ + 'Unexpected logger message at standard output' + + # Try WARNING level + # Catch logger messages to string buffer + with io.StringIO() as buf: + # Add custom handler to logger + log_handler = logging.StreamHandler(buf) + log._logger.handlers = list() + log._logger.addHandler(log_handler) + + # Try WARNING level + log.set_level('WARNING') + + # Push messages + push_messages(log) + + # Get string from buffer + output = buf.getvalue() + + assert output == 'Warn message\nError message\n' \ + + 'Critical message\n', \ + 'Unexpected logger message at standard output' + + # Try ERROR level + # Catch logger messages to string buffer + with io.StringIO() as buf: + # Add custom handler to logger + log_handler = logging.StreamHandler(buf) + log._logger.handlers = list() + log._logger.addHandler(log_handler) + + # Try ERROR level + log.set_level('ERROR') + + # Push messages + push_messages(log) + + # Get string from buffer + output = buf.getvalue() + + assert output == 'Error message\nCritical message\n', \ + 'Unexpected logger message at standard output' + + # Try CRITICAL level + # Catch logger messages to string buffer + with io.StringIO() as buf: + # Add custom handler to logger + log_handler = logging.StreamHandler(buf) + log._logger.handlers = list() + log._logger.addHandler(log_handler) + + # Try CRITICAL level + log.set_level('CRITICAL') + + # Push messages + push_messages(log) + + # Get string from buffer + output = buf.getvalue() + + assert output == 'Critical message\n', \ + 'Unexpected logger message at standard output' From c796b375003dab3ab95c7939e11f050ebdb05d72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Municoy?= Date: Mon, 14 Sep 2020 14:50:32 +0200 Subject: [PATCH 3/4] Update releasehistory.rst --- docs/releasehistory.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/releasehistory.rst b/docs/releasehistory.rst index 04bdb62a..be7338e4 100644 --- a/docs/releasehistory.rst +++ b/docs/releasehistory.rst @@ -13,6 +13,10 @@ Releases follow the ``major.minor.micro`` scheme recommended by `PEP440 `_: Standard output prints follow the logging hierarchy and can be modified by the user. + Bugfixes """""""" - `PR #48 `_: Fixes CLI's default output paths. @@ -22,6 +26,8 @@ Tests added """"""""""" - `PR #48 `_: Adds tests to validate the assignment of the default output paths. - `PR #52 `_: Adds tests to validate the initialization using a connectivity template. +- `PR #55 `_: Adds tests for the new Logger class. + 0.3.0 - Rotamers, OPLS2005, SMILES and stability improvements ------------------------------------------------------------- From 64c970ed19f245fd8ffd39fa28dd697d4ad3d1f6 Mon Sep 17 00:00:00 2001 From: Marti Municoy Date: Mon, 14 Sep 2020 16:28:06 +0200 Subject: [PATCH 4/4] Add silent and debug modes --- offpele/main.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/offpele/main.py b/offpele/main.py index 23e073ab..825ce601 100644 --- a/offpele/main.py +++ b/offpele/main.py @@ -73,12 +73,22 @@ def parse_args(): action='store_true', help="Use OPLS to set the parameters for bonds " + "and angles") + parser.add_argument('-s', '--silent', + dest="silent", + action='store_true', + help="Activate silent mode") + parser.add_argument('-d', '--debug', + dest="silent", + action='store_true', + help="Activate debug mode") parser.set_defaults(as_datalocal=False) parser.set_defaults(with_solvent=False) parser.set_defaults(include_terminal_rotamers=False) parser.set_defaults(use_OPLS_nb_params=False) parser.set_defaults(use_OPLS_bonds_and_angles=False) + parser.set_defaults(silent=False) + parser.set_defaults(debug=False) args = parser.parse_args() @@ -248,9 +258,14 @@ def main(): import logging logging.getLogger().setLevel(logging.ERROR) - # Set offpele logger to INFO level + # Set offpele logger to the corresponding level logger = Logger() - logger.set_level('INFO') + if args.silent: + logger.set_level('CRITICAL') + elif args.debug: + logger.set_level('DEBUG') + else: + logger.set_level('INFO') run_offpele(args.pdb_file, args.forcefield, args.resolution, args.charges_method, args.use_OPLS_nb_params,