diff --git a/Dockerfile b/Dockerfile index 4476a734..3228512b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,9 +29,11 @@ RUN apt-get update && apt-get install -y \ python3-openbabel \ libgfortran5 \ libarpack++2-dev \ + python3-ipython \ && apt-get autoremove -y && apt-get autoclean -y && apt-get clean -y \ && rm -rf /var/lib/apt/lists/* COPY run_acpype.py /home COPY acpype_lib /home/acpype_lib -RUN ln -s /home/run_acpype.py /usr/local/bin/acpype \ No newline at end of file +RUN ln -s /home/run_acpype.py /usr/local/bin/acpype +ENV PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/amber21/bin" \ No newline at end of file diff --git a/README.md b/README.md index 27679390..58095da6 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@ # ACPYPE -![GitHub](https://img.shields.io/github/license/alanwilter/acpype?style=social) -![GitHub All Releases](https://img.shields.io/github/downloads/alanwilter/acpype/total?style=social) -![Docker Pulls](https://img.shields.io/docker/pulls/acpype/acpype?style=social&logo=docker) -![Docker Image Size (tag)](https://img.shields.io/docker/image-size/lpkagami/acpype/latest?style=social&logo=docker) -![GitHub Relase](https://img.shields.io/github/release-date/alanwilter/acpype?style=social) -![Conda Version](https://img.shields.io/conda/vn/conda-forge/acpype.svg) -![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/acpype.svg) +![GitHub](https://img.shields.io/github/license/alanwilter/acpype?style=plastic) +![GitHub release (latest by date)](https://img.shields.io/github/v/release/alanwilter/acpype?display_name=tag&logo=github&style=plastic) +![GitHub Relase](https://img.shields.io/github/release-date/alanwilter/acpype?style=plastic&logo=github) +![Docker Pulls](https://img.shields.io/docker/pulls/acpype/acpype?style=plastic&logo=docker) +![Docker Image Size (tag)](https://img.shields.io/docker/image-size/acpype/acpype/latest?style=plastic&logo=docker) +![Conda Version](https://img.shields.io/conda/vn/conda-forge/acpype.svg?style=plastic&logo=conda-forge) +![Conda Downloads](https://img.shields.io/conda/dn/conda-forge/acpype.svg?style=plastic&logo=conda-forge) +![PyPI](https://img.shields.io/pypi/v/acpype?style=plastic&logo=pypi) +![PyPI - Downloads](https://img.shields.io/pypi/dm/acpype?style=plastic&logo=pypi) +![GitHub Workflow Status](https://img.shields.io/github/workflow/status/alanwilter/acpype/check_acpype) + + ## AnteChamber PYthon Parser interfacE @@ -87,21 +92,33 @@ There are several ways of obtaining `acpype`: 1. Via **[CONDA](https://anaconda.org/search?q=acpype)**: + *(It should be wholesome, fully funcional, all batteries included)* + ```bash conda install -c conda-forge acpype ``` 2. Via **[PyPI](https://pypi.org/project/acpype/)**: + *(Make sure you have `AmberTools` and, optionally but highly recommended, `OpenBabel` )* + ```bash + # You can use conda to get the needed 3rd parties for example + conda create -n acpype --channel conda-forge ambertools openbabel + + pip install acpype + + # or if you feel daring + pip install git+https://github.com/alanwilter/acpype.git ``` - note that `pip install acpype`, unfortunately, is not *yet* picking the original one. - 3. By downloading it via `git`: + *(Make sure you have `AmberTools` and, optionally but highly recommended, `OpenBabel` )* + ```bash + # You can use conda to get the needed 3rd parties for example conda create -n acpype --channel conda-forge ambertools openbabel git clone https://github.com/alanwilter/acpype.git ``` @@ -110,39 +127,37 @@ There are several ways of obtaining `acpype`: 4. Via **[Docker](https://hub.docker.com/repository/docker/acpype/acpype/)**: - If you have Docker installed, you can run `acpype` by: - - ```bash - docker pull acpype/acpype:latest - ``` + *(It should be wholesome, fully funcional, all batteries included)* + + If you have Docker installed, you can run `acpype_docker.sh` by: - Then, on Linux / MacOS choose a folder where to work (e.g. contains your PDB, MOL2 or inpcrd/prmtop files) and do: + NOTE: first time may take some time as it pulls the `acpype` docker image. + + On Linux / MacOS: ```bash - - # it opens a terminal inside docker with access to your working folder - docker run -i -t --rm -v ${PWD}:/wdir -w /wdir acpype/acpype bash - - # use acpype - acpype -i CCCC - acpype -i DDD.pdb -c gas # if you have a DDD.pdb file there in "wdir" ($PWD) - exit # and your output files will be at your working directory ($PWD) + ln -fsv "$PWD/acpype_docker.sh" /usr/local/bin/acpype_docker + ``` On Windows: Using Command Prompt: + In the directory where the `acpype_docker.bat` file is found: + ```bash - docker run -i -t --rm -v%cd%:/wdir -w /wdir acpype bash + setx /M path "%path%;%cd%" ``` - - Using PowerShell: - + Commands: ```bash - docker run -i -t --rm -v ${PWD}:/wdir -w /wdir acpype bash + acpype_docker -i CCCC + + acpype_docker -i tests/DDD.pdb -c gas ``` +**NB:** -**NB:** Installing via `conda` or via `pip/git` you get `AmberTools v.21.11` and `OpenBabel v3.11`. Our `AmberTools v.21.11` comes with binary `charmmgen` from `AmberTools17` in order to generate CHARMM topologies. +- By installing via `conda` or using via `docker` you get `AmberTools v.21.11` and `OpenBabel v3.1.1`. Our `AmberTools v.21.11` comes with binary `charmmgen` from `AmberTools17` in order to generate CHARMM topologies. +- By installing via `pip` you get `AmberTools` (as described above) embeded. However, the included binaries may not work in your system (library dependecies issues) and with only provide binaries for Linux (Ubuntu20) and Mac OSX. ##### To Test, if doing via `git` @@ -174,7 +189,7 @@ To get help and more information, type: At folder `acpype/`, type: ```bash - ln -s $PWD/run_acpype.py /usr/local/bin/acpype + ln -fsv "$PWD/run_acpype.py" /usr/local/bin/acpype ``` Then re-login or start another shell session. @@ -241,4 +256,4 @@ cns < FFF_CNS.inp #### To Verify with NAMD -- see [TutorialNAMD] +- see [TutorialNAMD](../../wiki/Tutorial-NAMD) diff --git a/acpype_docker.bat b/acpype_docker.bat new file mode 100755 index 00000000..ff9d0232 --- /dev/null +++ b/acpype_docker.bat @@ -0,0 +1,2 @@ +@echo on +docker run -i -t -v %cd%:/wdir -w /wdir acpype/acpype acpype %* \ No newline at end of file diff --git a/acpype_docker.sh b/acpype_docker.sh new file mode 100755 index 00000000..f8827b85 --- /dev/null +++ b/acpype_docker.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +docker run -i -t -v "${PWD}":/wdir -w /wdir acpype/acpype acpype "$@" diff --git a/acpype_lib/__init__.py b/acpype_lib/__init__.py index 05d4d6c6..42bb8fdb 100644 --- a/acpype_lib/__init__.py +++ b/acpype_lib/__init__.py @@ -1,3 +1,4 @@ # from https://packaging.python.org/guides/single-sourcing-package-version/ # using option 2 -__version__ = "2021.11.27" +# updated automatically via pre-commit git-hook +__version__ = "2021.11.29" diff --git a/acpype_lib/acpype.py b/acpype_lib/acpype.py index 87accab0..aa949568 100755 --- a/acpype_lib/acpype.py +++ b/acpype_lib/acpype.py @@ -59,36 +59,43 @@ import time import os import sys -import sysconfig from shutil import rmtree, which from acpype_lib.topol import MolTopol, ACTopol, header from acpype_lib.parser_args import get_option_parser -from acpype_lib.utils import while_replace -from acpype_lib.utils import checkSmiles, elapsedTime +from acpype_lib.utils import while_replace, elapsedTime +from acpype_lib.params import binaries -# For pip package -if which("antechamber") is None: - LOCAL_PATH = sysconfig.get_paths()["purelib"] - if sys.platform == "linux": - os.environ["PATH"] += os.pathsep + LOCAL_PATH + "/amber21-11_linux/bin:" + LOCAL_PATH + "/amber21-11_linux/dat/" - os.environ["AMBERHOME"] = LOCAL_PATH + "/amber21-11_linux/" - os.environ["ACHOME"] = LOCAL_PATH + "/amber21-11_linux/bin/" - 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:" + LOCAL_PATH + "/amber21-11_os/dat/" - os.environ["AMBERHOME"] = LOCAL_PATH + "/amber21-11_os/" - os.environ["ACHOME"] = LOCAL_PATH + "/amber21-11_os/bin/" - os.environ["LD_LIBRARY_PATH"] = LOCAL_PATH + "/amber21-11_os/lib/" -if sys.version_info < (3, 6): - raise Exception("Sorry, you need python 3.6 or higher") +def set_for_pip(): + # For pip package + if which(binaries["ac_bin"]) is None: + try: + LOCAL_PATH = os.path.dirname(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/" + except Exception: + print("ERROR: AmberTools NOT FOUND") -def init_main(argv=None): +def chk_py_ver(): + if sys.version_info < (3, 6): + raise Exception("Sorry, you need python 3.6 or higher") + + +def init_main(binaries=binaries, argv=None): """ Main function, to satisfy Conda """ + chk_py_ver() + set_for_pip() if argv is None: argv = sys.argv[1:] @@ -141,6 +148,7 @@ def init_main(argv=None): else: molecule = ACTopol( args.input, + binaries=binaries, chargeType=args.charge_method, chargeVal=args.net_charge, debug=args.debug, @@ -196,8 +204,8 @@ def init_main(argv=None): except Exception: pass - if not amb2gmxF and molecule.babelExe: - if checkSmiles(args.input): + if not amb2gmxF and molecule.obabelExe: + if molecule.checkSmiles(): afile = "smiles_molecule.mol2" if os.path.exists(afile): os.remove(afile) diff --git a/acpype_lib/params.py b/acpype_lib/params.py index 80bea120..de515203 100644 --- a/acpype_lib/params.py +++ b/acpype_lib/params.py @@ -1,3 +1,5 @@ +binaries = {"ac_bin": "antechamber", "obabel_bin": "obabel"} + MAXTIME = 3 * 3600 cal = 4.184 Pi = 3.141593 diff --git a/acpype_lib/parser_args.py b/acpype_lib/parser_args.py index c748ae08..eeab4b3d 100644 --- a/acpype_lib/parser_args.py +++ b/acpype_lib/parser_args.py @@ -3,6 +3,7 @@ def get_option_parser(): + # not used yet: -e -l -r parser = argparse.ArgumentParser(usage=usage + epilog) group = parser.add_mutually_exclusive_group() parser.add_argument( diff --git a/acpype_lib/topol.py b/acpype_lib/topol.py index d8c87e98..3391ff56 100644 --- a/acpype_lib/topol.py +++ b/acpype_lib/topol.py @@ -1,3 +1,4 @@ +import re import signal import math import os @@ -10,10 +11,10 @@ from acpype_lib.mol import Atom, Angle, AtomType, Bond, Dihedral from acpype_lib import __version__ as version from acpype_lib.params import minDist, minDist2, maxDist, maxDist2, MAXTIME, TLEAP_TEMPLATE, leapAmberFile -from acpype_lib.params import ionOrSolResNameList, radPi, cal, outTopols, qDict, qConv, diffTol +from acpype_lib.params import binaries, ionOrSolResNameList, radPi, cal, outTopols, qDict, qConv, diffTol from acpype_lib.params import dictAtomTypeAmb2OplsGmxCode, dictAtomTypeGaff2OplsGmxCode, oplsCode2AtomTypeDict from acpype_lib.utils import _getoutput, while_replace, distanceAA, job_pids_family, checkOpenBabelVersion -from acpype_lib.utils import checkSmiles, find_antechamber, elapsedTime, imprDihAngle, parmMerge +from acpype_lib.utils import find_bin, elapsedTime, imprDihAngle, parmMerge year = datetime.today().year tag = version @@ -209,7 +210,7 @@ def __init__(self): self.tmpDir = None self.absInputFile = None self.chargeType = None - self.babelExe = None + self.obabelExe = None self.baseName = None self.acExe = None self.force = None @@ -250,7 +251,7 @@ def __init__(self): self.tleapLog = None self.parmchkLog = None self.inputFile = None - self.babelLog = None + self.obabelLog = None self.absHomeDir = None self.molTopol = None self.topFileData = None @@ -315,6 +316,35 @@ def search(self, name=None, alist=False): ll = ll[0] return ll + def checkSmiles(self): + + if find_bin(self.binaries["obabel_bin"]): + if checkOpenBabelVersion() >= 300: + from openbabel import openbabel as ob + from openbabel import pybel + + ob.cvar.obErrorLog.StopLogging() + + elif checkOpenBabelVersion() >= 200 and checkOpenBabelVersion() < 300: + import openbabel as ob + import pybel # type: ignore + + ob.cvar.obErrorLog.StopLogging() + else: + print("WARNING: your input may be a SMILES but") + print(" without OpenBabel, this functionality won't work") + return False + + # Check if input is a smiles string + try: + ob.obErrorLog.SetOutputLevel(0) + pybel.readstring("smi", self.smiles) + return True + except Exception: + ob.obErrorLog.SetOutputLevel(0) + + return False + def guessCharge(self): """ Guess the charge of a system based on antechamber @@ -343,7 +373,7 @@ def guessCharge(self): self.printWarn("no charge value given, trying to guess one...") mol2FileForGuessCharge = self.inputFile if self.ext == ".pdb": - cmd = f"{self.babelExe} -ipdb {self.inputFile} -omol2 -O {self.baseName}.mol2" + cmd = f"{self.obabelExe} -ipdb {self.inputFile} -omol2 -O {self.baseName}.mol2" self.printDebug(f"guessCharge: {cmd}") out = _getoutput(cmd) self.printDebug(out) @@ -954,23 +984,24 @@ def checkFrcmod(self): return check def convertPdbToMol2(self): - """Convert PDB to MOL2 by using babel""" + """Convert PDB to MOL2 by using obabel""" if self.ext == ".pdb": - if self.execBabel(): - self.printError("convert pdb to mol2 via babel failed") + if self.execObabel(): + self.printError(f"convert pdb to mol2 via {binaries['obabel_bin']} failed") return True return False def convertSmilesToMol2(self): - if not self.babelExe: - raise Exception("SMILES needs openbabel python module") + """Convert Smiles to MOL2 by using obabel""" + + if not self.obabelExe: + raise Exception("SMILES needs OpenBabel python module") if checkOpenBabelVersion() >= 300: from openbabel import pybel elif checkOpenBabelVersion() >= 200 and checkOpenBabelVersion() < 300: import pybel # type: ignore - """Convert Smiles to MOL2 by using babel""" try: mymol = pybel.readstring("smi", str(self.smiles)) mymol.addh() @@ -980,20 +1011,20 @@ def convertSmilesToMol2(self): except Exception: return False - def execBabel(self): - """Execute babel""" + def execObabel(self): + """Execute obabel""" self.makeDir() - cmd = f"{self.babelExe} -ipdb {self.inputFile} -omol2 -O {self.baseName}.mol2" + cmd = f"{self.obabelExe} -ipdb {self.inputFile} -omol2 -O {self.baseName}.mol2" self.printDebug(cmd) - self.babelLog = _getoutput(cmd) + self.obabelLog = _getoutput(cmd) self.ext = ".mol2" self.inputFile = self.baseName + self.ext self.acParDict["ext"] = "mol2" if os.path.exists(self.inputFile): self.printMess("* Babel OK *") else: - self.printQuoted(self.babelLog) + self.printQuoted(self.obabelLog) return True return False @@ -1351,7 +1382,7 @@ def getChirals(self): Get chiral atoms, its 4 neighbours and improper dihedral angle to store non-planar improper dihedrals for CNS (and CNS only!) """ - if not self._parent.babelExe: + if not self._parent.obabelExe: self.printWarn("No Openbabel python module, no chiral groups") self.chiralGroups = [] return @@ -3021,6 +3052,7 @@ class ACTopol(AbstractTopol): def __init__( self, inputFile, + binaries=binaries, chargeType="bcc", chargeVal=None, multiplicity="1", @@ -3042,6 +3074,7 @@ def __init__( amb2gmx=False, ): super().__init__() + self.binaries = binaries self.amb2gmx = amb2gmx self.debug = debug self.verbose = verbose @@ -3050,17 +3083,34 @@ def __init__( self.direct = direct self.sorted = is_sorted self.chiral = chiral + self.acExe = find_bin(binaries["ac_bin"]) + if not os.path.exists(self.acExe): + self.printError(f"no '{binaries['ac_bin']}' executable... aborting! ") + hint1 = "HINT1: is 'AMBERHOME' environment variable set?" + hint2 = ( + f"HINT2: is '{binaries['ac_bin']}' in your $PATH?" + + f"\n What 'which {binaries['ac_bin']}' in your terminal says?" + + "\n 'alias' doesn't work for ACPYPE." + ) + self.printMess(hint1) + self.printMess(hint2) + raise Exception("Missing ANTECHAMBER") self.inputFile = os.path.basename(inputFile) self.rootDir = os.path.abspath(".") self.absInputFile = os.path.abspath(inputFile) - if not os.path.exists(self.absInputFile) and checkSmiles(inputFile): - self.is_smiles = True + + if not os.path.exists(self.absInputFile) and not re.search(r"\.mol2$|\.mdl$|\.pdb$", self.inputFile): self.smiles = inputFile - if not basename: - self.inputFile = "smiles_molecule.mol2" + if self.checkSmiles(): + self.is_smiles = True + if not basename: + self.inputFile = "smiles_molecule.mol2" + else: + self.inputFile = f"{basename}.mol2" + self.absInputFile = os.path.abspath(self.inputFile) else: - self.inputFile = f"{basename}.mol2" - self.absInputFile = os.path.abspath(self.inputFile) + self.is_smiles = False + self.smiles = None elif not os.path.exists(self.absInputFile): raise Exception(f"Input file {inputFile} DOES NOT EXIST") baseOriginal, ext = os.path.splitext(self.inputFile) @@ -3068,14 +3118,14 @@ def __init__( self.baseOriginal = baseOriginal self.ext = ext self.baseName = base # name of the input file without ext. - self.babelExe = which("obabel") or "" - if not os.path.exists(self.babelExe): + self.obabelExe = find_bin(binaries["obabel_bin"]) + if not os.path.exists(self.obabelExe): if self.ext != ".mol2" and self.ext != ".mdl": - self.printError("no 'babel' executable; you need it if input is PDB or SMILES") + 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!") - raise Exception("Missing BABEL") + raise Exception("Missing OBABEL") else: - self.printWarn("no 'babel' executable, no PDB file as input can be used!") + self.printWarn(f"no '{binaries['obabel_bin']}' executable, no PDB file can be used as input!") if self.is_smiles: self.convertSmilesToMol2() self.timeTol = timeTol @@ -3097,18 +3147,6 @@ def __init__( self.gaffDatfile = "gaff2.dat" self.force = force self.allhdg = allhdg - self.acExe = find_antechamber() - if not os.path.exists(self.acExe): - self.printError("no 'antechamber' executable... aborting ! ") - hint1 = "HINT1: is 'AMBERHOME' or 'ACHOME' environment variable set?" - hint2 = ( - "HINT2: is 'antechamber' in your $PATH?" - + "\n What 'which antechamber' in your terminal says?" - + "\n 'alias' doesn't work for ACPYPE." - ) - self.printMess(hint1) - self.printMess(hint2) - raise Exception("Missing ANTECHAMBER") self.tleapExe = which("tleap") or "" self.parmchkExe = which("parmchk2") or "" acBase = base + "_AC" diff --git a/acpype_lib/utils.py b/acpype_lib/utils.py index 415cf324..bf88662b 100644 --- a/acpype_lib/utils.py +++ b/acpype_lib/utils.py @@ -5,6 +5,10 @@ from acpype_lib.params import Pi +def find_bin(abin): + return which(abin) or "" + + def checkOpenBabelVersion(): "check openbabel version" import openbabel as obl @@ -14,30 +18,6 @@ def checkOpenBabelVersion(): return int(obl.OBReleaseVersion().replace(".", "")) -def checkSmiles(smiles): - - if checkOpenBabelVersion() >= 300: - from openbabel import openbabel as ob - from openbabel import pybel - - ob.cvar.obErrorLog.StopLogging() - elif checkOpenBabelVersion() >= 200 and checkOpenBabelVersion() < 300: - import openbabel as ob - import pybel # type: ignore - - ob.cvar.obErrorLog.StopLogging() - - " Check if input is a smiles string " - - try: - ob.obErrorLog.SetOutputLevel(0) - pybel.readstring("smi", smiles) - return True - except Exception: - ob.obErrorLog.SetOutputLevel(0) - return False - - def dotproduct(aa, bb): """scalar product""" return sum((a * b) for a, b in zip(aa, bb)) @@ -273,17 +253,3 @@ def while_replace(string): while " " in string: string = string.replace(" ", " ") return string - - -def find_antechamber(): - acExe = "" - dirAmber = os.getenv("AMBERHOME", os.getenv("ACHOME")) - if dirAmber: - for ac_bin in ["bin", "exe"]: - ac_path = os.path.join(dirAmber, ac_bin, "antechamber") - if os.path.exists(ac_path): - acExe = ac_path - break - if not acExe: - acExe = which("antechamber") or "" - return acExe diff --git a/release.sh b/release.sh new file mode 100755 index 00000000..c38d434c --- /dev/null +++ b/release.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +# Create releases for pip or docker or both + +set -euo pipefail + +version="$(grep -o '[0-9]\{4\}\.[0-9]\{2\}\.[0-9]\{2\}' acpype_lib/__init__.py)" + +function usage() { + echo "syntax: $0 < [-p, -d] | -a > to create a release for pip or docker or both" + echo " -p : for pip, create wheel and upload to https://pypi.org/project/acpype/ (if you have permission)" + echo " -d : for docker, create images and upload to https://hub.docker.com/u/acpype (if you have permission)" + echo " -a : do both above" + echo " -v : verbose mode, print all commands" + echo " -h : prints this message" + exit 1 +} + +function run_pip() { + echo ">>> Creating pip package" + python3 -m build + python3 -m twine upload --repository testpypi dist/*"$version"* # TestPyPI + python3 -m twine upload --repository pypi dist/*"$version"* # official release + rm -vfr dist/*"$version"* +} + +function run_docker() { + echo ">>> Creating docker images" + docker build -t acpype/acpype:latest -t acpype/acpype:"$version" . + echo ">>> Pushing docker images" + docker push acpype/acpype --all-tags + docker image rm acpype/acpype:"$version" +} + +function run_both() { + run_docker + run_pip +} + +do_pip=false +do_doc=false +do_all=false +verb=false +no_args=true + +while getopts "adpvh" optionName; do + case "$optionName" in + a) do_all=true ;; + d) do_doc=true ;; + p) do_pip=true ;; + v) verb=true ;; + h) usage ;; + ?) usage ;; + *) usage ;; + esac + no_args=false +done + +if "${no_args}"; then + usage +elif $do_all && ($do_doc || $do_pip); then + usage +fi + +if ${verb}; then + set -x +fi + +if $do_pip; then + run_pip +fi + +if $do_doc; then + run_docker +fi + +if $do_all; then + run_both +fi diff --git a/tests/conftest.py b/tests/conftest.py index aae41e23..3f681d1c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,13 @@ +""" +Requirements: +* pytest +* pytest-html + +To run: +pytest -s --html=report.html +pytest --cov=acpype_lib --cov-report=term-missing:skip-covered +""" + from acpype_lib import __version__ as version diff --git a/tests/test_acpype.py b/tests/test_acpype.py index 1ea4997d..788d6aa3 100644 --- a/tests/test_acpype.py +++ b/tests/test_acpype.py @@ -1,15 +1,3 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Requirements: -* pytest -* pytest-html - -To run: -pytest -s --html=report.html - -""" import os import shutil import pytest @@ -132,13 +120,17 @@ def test_ildn_gmx4_fail(): shutil.rmtree(molecule.absHomeDir) -def test_smiles(): +@pytest.mark.parametrize( + ("base", "msg"), [(None, "smiles_molecule.mol2"), ("thalidomide_smiles", "thalidomide_smiles.mol2")], +) +def test_smiles(base, msg): os.chdir(os.path.dirname(os.path.abspath(__file__))) smiles = "c1ccc2c(c1)C(=O)N(C2=O)C3CCC(=NC3=O)O" - molecule = ACTopol(smiles, basename="thalidomide_smiles", chargeType="gas", debug=True) + molecule = ACTopol(smiles, basename=base, chargeType="gas", debug=True) molecule.createACTopol() molecule.createMolTopol() assert molecule + assert molecule.inputFile == msg assert len(molecule.molTopol.atoms) == 29 shutil.rmtree(molecule.absHomeDir) os.remove(molecule.absInputFile) @@ -241,7 +233,7 @@ def test_charge_user(): def test_inputs(capsys, argv): os.chdir(os.path.dirname(os.path.abspath(__file__))) temp_base = "vir_temp" - init_main(argv + ["-b", temp_base]) + init_main(argv=argv + ["-b", temp_base]) captured = capsys.readouterr() assert "Total time of execution:" in captured.out os.chdir(os.path.dirname(os.path.abspath(__file__))) @@ -254,14 +246,15 @@ def test_inputs(capsys, argv): (None, 2, " error: "), # NOTE: None -> sys.argv from pystest (["-v"], 0, version), ([], 2, "error: missing input files"), - (["-di", " 123"], 19, "ACPYPE FAILED: Input file 123 DOES NOT EXIST"), + (["-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"), (["-di", " 123", "-u"], 2, "option -u is only meaningful in 'amb2gmx' mode (args '-p' and '-x')"), ], ) def test_args_wrong_inputs(capsys, argv, code, msg): with pytest.raises(SystemExit) as e_info: - init_main(argv) + init_main(argv=argv) captured = capsys.readouterr() assert msg in captured.err + captured.out assert e_info.typename == "SystemExit" diff --git a/tests/test_acs_api.py b/tests/test_acs_api.py index 8865b575..ab0db65c 100644 --- a/tests/test_acs_api.py +++ b/tests/test_acs_api.py @@ -1,15 +1,3 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Requirements: -* pytest -* pytest-html - -To run: -pytest -s --html=report.html - -""" import os import ujson as json from acpype_lib.acs_api import acpype_api @@ -48,7 +36,8 @@ def test_json_simple(): def test_json_failed(): os.chdir(os.path.dirname(os.path.abspath(__file__))) jj = json.loads(acpype_api(inputFile="_fake_", debug=True)) - assert jj.get("file_name") == "ERROR: Input file _fake_ DOES NOT EXIST" + assert "ERROR: [Errno 2] No such file or directory" in jj.get("file_name") + assert "tests/_fake_" in jj.get("file_name") def get_json(): diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py new file mode 100644 index 00000000..9a2ed52d --- /dev/null +++ b/tests/test_scenarios.py @@ -0,0 +1,63 @@ +import os +import pytest +from acpype_lib.acpype import init_main +from acpype_lib.utils import _getoutput + + +def test_no_ac(capsys): + binaries = {"ac_bin": "no_ac", "obabel_bin": "obabel"} + msg = f"ERROR: no '{binaries['ac_bin']}' executable... aborting!" + inp = "AAA.mol2" + os.chdir(os.path.dirname(os.path.abspath(__file__))) + with pytest.raises(SystemExit) as e_info: + init_main(argv=["-di", inp, "-c", "gas"], binaries=binaries) + captured = capsys.readouterr() + assert msg in captured.out + assert e_info.typename == "SystemExit" + assert e_info.value.code == 19 + + +def test_only_ac(capsys): + binaries = {"ac_bin": "antechamber", "obabel_bin": "no_obabel"} + msg1 = f"WARNING: no '{binaries['obabel_bin']}' executable, no PDB file can be used as input!" + msg2 = "Total time of execution:" + msg3 = "WARNING: No Openbabel python module, no chiral groups" + inp = "AAA.mol2" + temp_base = "vir_temp" + os.chdir(os.path.dirname(os.path.abspath(__file__))) + init_main(argv=["-di", inp, "-c", "gas", "-b", temp_base], binaries=binaries) + captured = capsys.readouterr() + assert msg1 in captured.out + assert msg2 in captured.out + assert msg3 in captured.out + _getoutput(f"rm -fr {temp_base}*") + + +@pytest.mark.parametrize( + ("inp", "msg"), + [ + ("AAA.pdb", "ERROR: no 'no_obabel' executable; you need it if input is PDB or SMILES"), + ("cccc", "WARNING: your input may be a SMILES but"), + ], +) +def test_no_obabel(capsys, inp, msg): + binaries = {"ac_bin": "antechamber", "obabel_bin": "no_obabel"} + os.chdir(os.path.dirname(os.path.abspath(__file__))) + with pytest.raises(SystemExit) as e_info: + init_main(argv=["-di", inp, "-c", "gas"], binaries=binaries) + captured = capsys.readouterr() + assert msg in captured.out + assert e_info.typename == "SystemExit" + assert e_info.value.code == 19 + + +def test_amb2gmx_no_bins(capsys): + binaries = {"ac_bin": "no_ac", "obabel_bin": "no_obabel"} + os.chdir(os.path.dirname(os.path.abspath(__file__))) + argv = ["-x", "Base.inpcrd", "-p", "Base.prmtop"] + temp_base = "vir_temp" + init_main(argv=argv + ["-b", temp_base], binaries=binaries) + captured = capsys.readouterr() + assert "Total time of execution:" in captured.out + os.chdir(os.path.dirname(os.path.abspath(__file__))) + _getoutput(f"rm -fr {temp_base}*")