diff --git a/Acpype_API_Jupyter.ipynb b/Acpype_API_Jupyter.ipynb index 2ceb25b9..a94c74b2 100644 --- a/Acpype_API_Jupyter.ipynb +++ b/Acpype_API_Jupyter.ipynb @@ -34,7 +34,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "2021.12.14\n" + "2021.12.24\n" ] }, { @@ -91,13 +91,6 @@ "==> * Antechamber OK *\n", "==> * Parmchk OK *\n", "==> Executing Tleap...\n", - "++++++++++start_quote+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n", - "Checking 'DMP'....\n", - "Checking parameters for unit 'DMP'.\n", - "Checking for bond parameters.\n", - "Checking for angle parameters.\n", - "Unit is OK.\n", - "++++++++++end_quote+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n", "==> * Tleap OK *\n", "==> Removing temporary files...\n" ] diff --git a/acpype/__init__.py b/acpype/__init__.py index f5593736..fbb1a8c9 100644 --- a/acpype/__init__.py +++ b/acpype/__init__.py @@ -1,4 +1,4 @@ # from https://packaging.python.org/guides/single-sourcing-package-version/ # using option 2 # updated automatically via pre-commit git-hook -__version__ = "2021.12.23" +__version__ = "2021.12.24" diff --git a/acpype/cli.py b/acpype/cli.py index 53ae2a03..4151e578 100644 --- a/acpype/cli.py +++ b/acpype/cli.py @@ -55,31 +55,19 @@ import time import os import sys -from shutil import rmtree, which +from shutil import rmtree from acpype.topol import MolTopol, ACTopol, header from acpype.parser_args import get_option_parser -from acpype.utils import while_replace, elapsedTime +from acpype.utils import while_replace, elapsedTime, set_for_pip from acpype.params import binaries - - -def set_for_pip(binaries): - # For pip package - if which(binaries["ac_bin"]) is None: - LOCAL_PATH = os.path.dirname(__file__) - if sys.platform == "linux": - os.environ["PATH"] += os.pathsep + LOCAL_PATH + "/amber21-11_linux/bin" - os.environ["AMBERHOME"] = LOCAL_PATH + "/amber21-11_linux/" - os.environ["LD_LIBRARY_PATH"] = LOCAL_PATH + "/amber21-11_linux/lib/" - elif sys.platform == "darwin": - os.environ["PATH"] += os.pathsep + LOCAL_PATH + "/amber21-11_os/bin" - os.environ["AMBERHOME"] = LOCAL_PATH + "/amber21-11_os/" - os.environ["LD_LIBRARY_PATH"] = LOCAL_PATH + "/amber21-11_os/lib/" - os.environ["DYLD_LIBRARY_PATH"] = LOCAL_PATH + "/amber21-11_os/lib/" +from acpype.logger import copy_log, set_logging_conf as logger def chk_py_ver(): if sys.version_info < (3, 6): - raise Exception("Sorry, you need python 3.6 or higher") + msg = "Sorry, you need python 3.6 or higher" + logger().exception(msg) + raise Exception(msg) def init_main(binaries=binaries, argv=None): @@ -103,7 +91,13 @@ def init_main(binaries=binaries, argv=None): print(header) sys.exit(0) - print(header) + level = 20 + if not args.verboseless: + level = 100 + if args.debug: + level = 10 + + logger(level).info(header) if not args.input: amb2gmxF = True @@ -112,16 +106,16 @@ def init_main(binaries=binaries, argv=None): elif args.inpcrd or args.prmtop: parser.error("either '-i' or ('-p', '-x'), but not both") - if args.debug: - texta = "Python Version %s" % sys.version - print("DEBUG: %s" % while_replace(texta)) + logger(level).debug(f"CLI: {' '.join(argv)}") + texta = "Python Version %s" % sys.version + logger(level).debug(while_replace(texta)) if args.direct and not amb2gmxF: parser.error("option -u is only meaningful in 'amb2gmx' mode (args '-p' and '-x')") try: if amb2gmxF: - print("Converting Amber input files to Gromacs ...") + logger(level).info("Converting Amber input files to Gromacs ...") system = MolTopol( acFileXyz=args.inpcrd, acFileTop=args.prmtop, @@ -138,6 +132,7 @@ def init_main(binaries=binaries, argv=None): system.printDebug("prmtop and inpcrd files parsed") system.writeGromacsTopolFiles() + copy_log(system) else: molecule = ACTopol( args.input, @@ -165,11 +160,11 @@ def init_main(binaries=binaries, argv=None): molecule.createACTopol() molecule.createMolTopol() - + copy_log(molecule) acpypeFailed = False except Exception: _exceptionType, exceptionValue, exceptionTraceback = sys.exc_info() - print("ACPYPE FAILED: %s" % exceptionValue) + logger(level).error("ACPYPE FAILED: %s" % exceptionValue) if args.debug: traceback.print_tb(exceptionTraceback, file=sys.stdout) acpypeFailed = True @@ -179,7 +174,7 @@ def init_main(binaries=binaries, argv=None): amsg = "less than a second" else: amsg = elapsedTime(execTime) - print("Total time of execution: %s" % amsg) + logger(level).info("Total time of execution: %s" % amsg) if args.ipython: import IPython # pylint: disable=import-outside-toplevel @@ -190,6 +185,7 @@ def init_main(binaries=binaries, argv=None): rmtree(molecule.tmpDir) except Exception: pass + if acpypeFailed: sys.exit(19) try: diff --git a/acpype/logger.py b/acpype/logger.py new file mode 100644 index 00000000..38fe1a52 --- /dev/null +++ b/acpype/logger.py @@ -0,0 +1,71 @@ +import os +from shutil import move +import sys +import logging +from tempfile import NamedTemporaryFile + +tmpLogFile = NamedTemporaryFile().name + + +class LogFormatter(logging.Formatter): + + """ + Define log formatter + """ + + err_fmt = "ERROR: %(msg)s" + warn_fmt = "WARNING: %(msg)s" + dbg_fmt = "DEBUG: %(msg)s" + info_fmt = "%(msg)s" + + def __init__(self): + super().__init__(fmt="%(levelno)d: %(msg)s", datefmt=None, style="%") + + def format(self, record): + + # Save the original format configured by the user + # when the logger formatter was instantiated + format_orig = self._style._fmt + + # Replace the original format with one customized by logging level + if record.levelno == logging.DEBUG: + self._style._fmt = LogFormatter.dbg_fmt + elif record.levelno == logging.INFO: + self._style._fmt = LogFormatter.info_fmt + elif record.levelno == logging.ERROR: + self._style._fmt = LogFormatter.err_fmt + elif record.levelno == logging.WARNING: + self._style._fmt = LogFormatter.warn_fmt + # Call the original formatter class to do the grunt work + result = logging.Formatter.format(self, record) + # Restore the original format configured by the user + self._style._fmt = format_orig + return result + + +def copy_log(molecule): + local_log = os.path.join(molecule.absHomeDir, "acpype.log") + if os.path.exists(local_log): + os.remove(local_log) + if os.path.exists(tmpLogFile): + move(tmpLogFile, local_log) + + +def set_logging_conf(level=20): + # Setting logging configurations + logging.root.setLevel(0) + logger = logging.getLogger(__name__) + if logger.handlers: + logger.handlers.pop() + + fmt = LogFormatter() + file_handler = logging.FileHandler(filename=tmpLogFile) + stdout_handler = logging.StreamHandler(sys.stdout) + file_handler.setLevel(logging.DEBUG) + stdout_handler.setLevel(level) + stdout_handler.setFormatter(fmt) + file_handler.setFormatter(fmt) + if not logger.handlers: + logger.addHandler(file_handler) + logger.addHandler(stdout_handler) + return logger diff --git a/acpype/parser_args.py b/acpype/parser_args.py index 95d4c872..259c8227 100644 --- a/acpype/parser_args.py +++ b/acpype/parser_args.py @@ -100,6 +100,14 @@ def get_option_parser(): dest="debug", help="for debugging purposes, keep any temporary file created (not allowed with arg -w)", ) + group.add_argument( + "-w", + "--verboseless", + action="store_false", + default=True, + dest="verboseless", + help="print nothing (not allowed with arg -d)", + ) parser.add_argument( "-o", "--outtop", @@ -139,14 +147,6 @@ def get_option_parser(): dest="ipython", help="start iPython interpreter", ) - group.add_argument( - "-w", - "--verboseless", - action="store_false", - default=True, - dest="verboseless", - help="print nothing (not allowed with arg -d)", - ) parser.add_argument( "-g", "--merge", diff --git a/acpype/topol.py b/acpype/topol.py index f3092665..cdc03994 100644 --- a/acpype/topol.py +++ b/acpype/topol.py @@ -15,6 +15,7 @@ from acpype.params import dictAtomTypeAmb2OplsGmxCode, dictAtomTypeGaff2OplsGmxCode, oplsCode2AtomTypeDict from acpype.utils import _getoutput, while_replace, distanceAA, job_pids_family, checkOpenBabelVersion from acpype.utils import find_bin, elapsedTime, imprDihAngle, parmMerge +from acpype.logger import set_logging_conf as logger year = datetime.today().year tag = version @@ -93,7 +94,7 @@ def read_amber_topology(self, buff): try: setattr(self, attributes[i], self.p7_array_read(buff, flag_strings[i])) except Exception: - print("Skipping non-existent attributes", attributes[i], flag_strings[i]) + logger().info("Skipping non-existent attributes", attributes[i], flag_strings[i]) @staticmethod def skipline(buff, index): @@ -203,6 +204,7 @@ class AbstractTopol: @abc.abstractmethod def __init__(self): if self.__class__ is AbstractTopol: + logger().exception("Attempt to create instance of abstract class AbstractTopol") raise TypeError("Attempt to create instance of abstract class AbstractTopol") self.debug = None self.verbose = None @@ -281,30 +283,25 @@ def __init__(self): def printDebug(self, text=""): """Debug log level""" - if self.debug: - print(f"DEBUG: {while_replace(text)}") + logger(self.level).debug(f"{while_replace(text)}") def printWarn(self, text=""): """Warn log level""" - if self.verbose: - print(f"WARNING: {while_replace(text)}") + logger(self.level).warning(f"{while_replace(text)}") def printError(self, text=""): """Error log level""" - if self.verbose: - print(f"ERROR: {while_replace(text)}") + logger(self.level).error(f"{while_replace(text)}") def printMess(self, text=""): """Info log level""" - if self.verbose: - print(f"==> {while_replace(text)}") + logger(self.level).info(f"==> {while_replace(text)}") def printQuoted(self, text=""): """Print quoted messages""" - if self.verbose: - print(10 * "+" + "start_quote" + 59 * "+") - print(while_replace(text)) - print(10 * "+" + "end_quote" + 61 * "+") + logger(self.level).debug(10 * "+" + "start_quote" + 59 * "+") + logger(self.level).debug(while_replace(text)) + logger(self.level).debug(10 * "+" + "end_quote" + 61 * "+") def search(self, name=None, alist=False): """ @@ -331,8 +328,8 @@ def checkSmiles(self): ob.cvar.obErrorLog.StopLogging() else: - print("WARNING: your input may be a SMILES but") - print(" without OpenBabel, this functionality won't work") + logger(self.level).warning("WARNING: your input may be a SMILES but") + logger(self.level).warning(" without OpenBabel, this functionality won't work") return False # Check if input is a smiles string @@ -386,10 +383,9 @@ def guessCharge(self): cmd = f"{self.acExe} -dr no -i {mol2FileForGuessCharge} -fi {in_mol} -o tmp -fo mol2 -c gas -pf y" - if self.debug: - self.printMess("Debugging...") - cmd = cmd.replace("-pf y", "-pf n") - print(while_replace(cmd)) + self.printDebug("Debugging...") + cmd = cmd.replace("-pf y", "-pf n") + logger(self.level).debug(while_replace(cmd)) log = _getoutput(cmd).strip() @@ -417,6 +413,7 @@ def guessCharge(self): if drift > diffTol: self.printError("Net charge drift '%3.5f' bigger than tolerance '%3.5f'" % (drift, diffTol)) if not self.force: + logger(self.level).exception("Error with calculated charge") raise Exception("Error with calculated charge") self.chargeVal = str(charge2) self.printMess(f"... charge set to {charge2}") @@ -463,11 +460,11 @@ def setResNameCheckCoords(self): coords[cs].append(at) else: coords[cs] = [at] - # self.printDebug(coords) if len(residues) > 1: self.printError(f"more than one residue detected '{str(residues)}'") self.printError(f"verify your input file '{self.inputFile}'. Aborting ...") + logger(self.level).exception("Only ONE Residue is allowed for ACPYPE to work") raise Exception("Only ONE Residue is allowed for ACPYPE to work") dups = "" @@ -517,6 +514,7 @@ def setResNameCheckCoords(self): else: self.printError("Use '-f' option if you want to proceed anyway. Aborting ...") rmtree(self.tmpDir) + logger(self.level).exception("Coordinates issues with your system") raise Exception("Coordinates issues with your system") try: # scape resname list index out of range resname = list(residues)[0].strip() @@ -642,9 +640,8 @@ def readMol2TotalCharge(self, mol2File): charge = 0.0 ll = [] cmd = f"{self.acExe} -dr no -i {mol2File} -fi mol2 -o tmp -fo mol2 -c wc -cf tmp.crg -pf y" - if self.debug: - self.printMess("Debugging...") - cmd = cmd.replace("-pf y", "-pf n") + self.printDebug("Debugging...") + cmd = cmd.replace("-pf y", "-pf n") self.printDebug(cmd) @@ -656,7 +653,7 @@ def readMol2TotalCharge(self, mol2File): for line in tmpData: ll += line.split() charge = sum(map(float, ll)) - if not log.isspace() and self.debug: + if not log.isspace(): self.printQuoted(log) self.printDebug("readMol2TotalCharge: " + str(charge)) @@ -801,9 +798,8 @@ def execAntechamber(self, chargeType=None, atomType=None): self.ekFlag, ) - if self.debug: - self.printMess("Debugging...") - cmd = cmd.replace("-pf y", "-pf n") + self.printDebug("Debugging...") + cmd = cmd.replace("-pf y", "-pf n") self.printDebug(cmd) @@ -838,6 +834,7 @@ def signal_handler(self, _signum, _frame): # , pid = 0): # os.system('kill -15 %s' % pids) for i in pids.split(): os.kill(int(i), 15) + logger(self.level).exception("Semi-QM taking too long to finish... aborting!") raise Exception("Semi-QM taking too long to finish... aborting!") def delOutputFiles(self): @@ -1002,6 +999,7 @@ def convertSmilesToMol2(self): """Convert Smiles to MOL2 by using obabel""" if not self.obabelExe: + logger(self.level).exception("SMILES needs OpenBabel python module") raise Exception("SMILES needs OpenBabel python module") if checkOpenBabelVersion() >= 300: from openbabel import pybel @@ -1083,7 +1081,8 @@ def createMolTopol(self): self.printError("pickleSave failed") pass - self.delOutputFiles() # required to use on Jupyter Notebook + if not self.debug: + self.delOutputFiles() # required to use on Jupyter Notebook os.chdir(self.rootDir) def pickleSave(self): @@ -1119,6 +1118,7 @@ def getFlagData(self, flag): data = "" if not self.topFileData: + logger(self.level).exception("PRMTOP file empty?") raise Exception("PRMTOP file empty?") for rawLine in self.topFileData: @@ -1185,6 +1185,7 @@ def getCoords(self): [[x1,y1,z1],[x2,y2,z2], etc.] """ if not self.xyzFileData: + logger(self.level).exception("INPCRD file empty?") raise Exception("INPCRD file empty?") data = "" for rawLine in self.xyzFileData[2:]: @@ -1569,6 +1570,7 @@ def setProperDihedralsCoef(self): phaseRaw = dih.phase * radPi # in degree phase = int(phaseRaw) # in degree if period > 4 and self.gmx4: + logger(self.level).exception("Likely trying to convert ILDN to RB, DO NOT use option '-z'") raise Exception("Likely trying to convert ILDN to RB, DO NOT use option '-z'") if phase in [0, 180]: properDihedralsGmx45.append([item[0].atoms, phaseRaw, kPhi, period]) @@ -1630,13 +1632,11 @@ def writeCharmmTopolFiles(self): % (self.acExe, self.acMol2FileName, self.charmmBase, at, res) ) - if self.debug: - cmd = cmd.replace("-pf y", "-pf n") - self.printDebug(cmd) + cmd = cmd.replace("-pf y", "-pf n") + self.printDebug(cmd) log = _getoutput(cmd) - if self.debug: - self.printQuoted(log) + self.printQuoted(log) def writePdb(self, file_): """ @@ -3144,6 +3144,7 @@ def __init__( is_sorted=False, chiral=False, amb2gmx=False, + level=20, ): super().__init__() self.binaries = binaries @@ -3155,6 +3156,13 @@ def __init__( self.direct = direct self.sorted = is_sorted self.chiral = chiral + + if not self.verbose: + level = 100 + if self.debug: + level = 10 + self.level = level or 20 + self.acExe = find_bin(binaries["ac_bin"]) if not os.path.exists(self.acExe): self.printError(f"no '{binaries['ac_bin']}' executable... aborting! ") @@ -3166,6 +3174,7 @@ def __init__( ) self.printMess(hint1) self.printMess(hint2) + logger(self.level).exception("Missing ANTECHAMBER") raise Exception("Missing ANTECHAMBER") self.inputFile = os.path.basename(inputFile) self.rootDir = os.path.abspath(".") @@ -3184,6 +3193,7 @@ def __init__( self.is_smiles = False self.smiles = None elif not os.path.exists(self.absInputFile): + logger(self.level).exception(f"Input file {inputFile} DOES NOT EXIST") raise Exception(f"Input file {inputFile} DOES NOT EXIST") baseOriginal, ext = os.path.splitext(self.inputFile) base = basename or baseOriginal @@ -3195,6 +3205,7 @@ def __init__( if self.ext != ".mol2" and self.ext != ".mdl": self.printError(f"no '{binaries['obabel_bin']}' executable; you need it if input is PDB or SMILES") self.printError("otherwise use only MOL2 or MDL file as input ... aborting!") + logger(self.level).exception("Missing OBABEL") raise Exception("Missing OBABEL") else: self.printWarn(f"no '{binaries['obabel_bin']}' executable, no PDB file can be used as input!") @@ -3275,12 +3286,14 @@ def __init__( is_sorted=False, chiral=False, amb2gmx=False, + level=20, ): super().__init__() self.amb2gmx = amb2gmx self.chiral = chiral self.allhdg = False self.debug = debug + self.level = level self.gmx4 = gmx4 self.merge = merge self.direct = direct @@ -3288,6 +3301,12 @@ def __init__( self.verbose = verbose self.inputFile = acFileTop + if not self.verbose: + level = 100 + if self.debug: + level = 10 + self.level = level + if acTopolObj: if not acFileXyz: acFileXyz = acTopolObj.acXyzFileName diff --git a/acpype/utils.py b/acpype/utils.py index 9c304c7b..9c610995 100644 --- a/acpype/utils.py +++ b/acpype/utils.py @@ -1,4 +1,5 @@ import os +import sys import math import subprocess as sub from shutil import which @@ -256,3 +257,18 @@ def while_replace(string): while " " in string: string = string.replace(" ", " ") return string + + +def set_for_pip(binaries): + # For pip package + if which(binaries["ac_bin"]) is None: + LOCAL_PATH = os.path.dirname(__file__) + if sys.platform == "linux": + os.environ["PATH"] += os.pathsep + LOCAL_PATH + "/amber21-11_linux/bin" + os.environ["AMBERHOME"] = LOCAL_PATH + "/amber21-11_linux/" + os.environ["LD_LIBRARY_PATH"] = LOCAL_PATH + "/amber21-11_linux/lib/" + elif sys.platform == "darwin": + os.environ["PATH"] += os.pathsep + LOCAL_PATH + "/amber21-11_os/bin" + os.environ["AMBERHOME"] = LOCAL_PATH + "/amber21-11_os/" + os.environ["LD_LIBRARY_PATH"] = LOCAL_PATH + "/amber21-11_os/lib/" + os.environ["DYLD_LIBRARY_PATH"] = LOCAL_PATH + "/amber21-11_os/lib/" diff --git a/pyproject.toml b/pyproject.toml index c843f81b..28ba800a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ exclude = ''' [tool.poetry] name = "acpype" -version = "2021.12.23" +version = "2021.12.24" description = "ACPYPE - AnteChamber PYthon Parser interfacE" authors = ["Alan Silva "] license = "GPL-3.0" diff --git a/tests/test_acpype.py b/tests/test_acpype.py index 1715e7ba..25713910 100644 --- a/tests/test_acpype.py +++ b/tests/test_acpype.py @@ -192,6 +192,7 @@ def test_inputs(capsys, argv): (None, 2, " error: "), # NOTE: None -> sys.argv from pytest (["-v"], 0, version), ([], 2, "error: missing input files"), + (["-d", "-w"], 2, "error: argument -w/--verboseless: not allowed with argument -d/--debug"), (["-di", "AAAx.mol2"], 19, "ACPYPE FAILED: Input file AAAx.mol2 DOES NOT EXIST"), (["-di", " 123"], 19, "ACPYPE FAILED: [Errno 2] No such file or directory"), (["-di", " 123", "-x", "abc"], 2, "either '-i' or ('-p', '-x'), but not both"),