Skip to content

Commit

Permalink
fixes run's single method (#85)
Browse files Browse the repository at this point in the history
* Fixes #82
* Changes:
    * input arguments now reflects the changes made to read_molecule_file in #84
    * Writing of pKa file is now optional (default behaviour has been kept). This will be particularly 
       useful downstream where we would just want to have access to the MoleculeContainer object.
     * new test_run file specific for testing run.
* add tests
* add docs
  • Loading branch information
IAlibay authored Jul 17, 2020
1 parent d16257a commit 7563a70
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 31 deletions.
3 changes: 3 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,6 @@
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']

# Enable intersphinx mapping
intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
27 changes: 14 additions & 13 deletions propka/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from pathlib import Path
from pkg_resources import resource_filename
from propka.lib import protein_precheck
from propka.output import write_propka
from propka.atom import Atom
from propka.conformation_container import ConformationContainer
from propka.group import initialize_atom_group
Expand Down Expand Up @@ -41,34 +40,36 @@ def read_molecule_file(filename: str, mol_container, stream=None):
Args:
filename(str): name of input file. If not using a filestream via the
``stream`` argument, should be a path to the file to be read.
mol_container: MolecularContainer object.
mol_container: :class:`~propka.molecular_container.MolecularContainer`
object.
stream: optional filestream handle. If ``None``, then open
``filename`` as a local file for reading.
Returns:
updated MolecularContainer object
updated :class:`~propka.molecular_container.MolecularContainer` object.
Raises:
ValuError if invalid input given
ValuError: if invalid input given
Examples:
There are two main cases for using ``read_molecule_file``. The first
(and most common) is to pass the input file (``filename``) as a
string which gives the path of the molecule file to be read (here we
also pass a ``MoleculeContainer`` object named ``mol_container``).
also pass a :class:`~propka.molecular_container.MolecularContainer`
object named ``mol_container``).
>>> read_molecule_file('test.pdb', mol_container)
<propka.molecular_container.MolecularContainer at 0x7f6e0c8f2310>
The other use case is when passing a file-like object, e.g. a
``StringIO`` class, instance. This is done by passing the object via
the ``stream`` argument. Since file-like objects do not usually have
an associated file name, an appropirate file name should be passed to
the ``filename`` argument. In this case, ``filename`` is not opened for
reading, but instead is used to help recognise the file type (based on
the extension being either `.pdb` or `.propka_input`) and also uses
that given ``filename`` to assign a name to the input
MolecularContainer object.
:class:`io.StringIO` class, instance. This is done by passing the
object via the ``stream`` argument. Since file-like objects do not
usually have an associated file name, an appropirate file name should
be passed to the ``filename`` argument. In this case, ``filename`` is
not opened for reading, but instead is used to help recognise the file
type (based on the extension being either `.pdb` or `.propka_input`)
and also uses that given ``filename`` to assign a name to the input
:class:`~propka.molecular_container.MolecularContainer` object.
>>> read_molecule_file('test.pdb', mol_container,
stream=string_io_object)
Expand Down
88 changes: 70 additions & 18 deletions propka/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,34 +36,86 @@ def main(optargs=None):
my_molecule.write_propka()


def single(pdbfile, optargs=None):
"""Run a single PROPKA calculation using *pdbfile* as input.
def single(filename: str, optargs: tuple = (), stream=None,
write_pka: bool = True):
"""Run a single PROPKA calculation using ``filename`` as input.
Commandline options can be passed as a **list** in *optargs*.
Args:
filename (str): name of input file. If filestream is not passed via
``stream``, should be a path to the file to be read.
optargs (tuple): Optional, commandline options for propka. Extra files
passed via ``optargs`` will be ignored, see Notes.
stream : optional filestream handle. If ``None``, then ``filename``
will be used as path to input file for reading.
write_pka (bool): Controls if the pKa file should be writen to disk.
Example
-------
Given an input file "protein.pdb", run the equivalent of ``propka3
--mutation=N25R/N181D -v --pH=7.2 protein.pdb`` as::
Returns:
:class:`~propka.molecular_container.MolecularContainer` object.
propka.run.single("protein.pdb",
optargs=["--mutation=N25R/N181D", "-v", "--pH=7.2"])
Examples:
Given an input file "protein.pdb", run the equivalent of ``propka3
--mutation=N25R/N181D -v --pH=7.2 protein.pdb`` as::
propka.run.single("protein.pdb",
optargs=["--mutation=N25R/N181D", "-v", "--pH=7.2"])
.. todo::
Test :func:`single`, not sure if it is correctly processing ``pdbfile``.
By default, a pKa file will be written. However in some cases one may
wish to not output this file and just have access to the
:class:`~propka.molecular_container.MolecularContainer` object. If so,
then pass ``False`` to ``write_pka``::
mol = propka.run.single("protein.pdb", write_pka=False)
In some cases, one may also want to pass a file-like (e.g.
:class:`io.StringIO`) object instead of a file path as a string. In
these cases the file-like object should be passed to the ``stream``
argument and a string indicating the file type in the ``filename``
argument; this string only has to look like a valid file name, it does
not need to exist because the data are actually read from ``stream``.
This approach is necessary because file-like objects do not usually
have names, and propka uses the ``filename`` argument to determine the
input file type, and assigns the file name for the
:class:`~propka.molecular_container.MolecularContainer` object::
mol = propka.run.single('input.pdb', stream=string_io_file)
In this case, a PDB file-like object was passed as `string_io_file`.
The resultant pKa file will be written out as `input.pka`.
Notes:
* Only a single input structure file will be processed, defined by
``filename`` (and ``stream`` if passing a file-like object). Any
additional files passed via the `-f` or `--file` flag to optargs will
be ignored.
.. seealso::
:func:`propka.input.read_molecule_file`
"""
optargs = optargs if optargs is not None else []
options = loadOptions(*optargs)
pdbfile = options.filenames.pop(0)
# Deal with input optarg options
optargs = tuple(optargs)
optargs += (filename,)
options = loadOptions(optargs)

parameters = read_parameter_file(options.parameters, Parameters())
if len(options.filenames) > 0:
_LOGGER.warning("Ignoring filenames: {0:s}".format(options.filenames))

# Only filename present should be the one passed via the arguments
# Anything else will probably have been passed using optargs' `-f` flag.
ignored_list = [i for i in options.filenames if i != filename]
if ignored_list:
_LOGGER.warning(f"Ignoring extra filenames passed: {ignored_list}")
options.filenames = [filename]

my_molecule = MolecularContainer(parameters, options)
my_molecule = read_molecule_file(pdbfile, my_molecule)
my_molecule = read_molecule_file(filename, my_molecule, stream=stream)
my_molecule.calculate_pka()
my_molecule.write_pka()

# write outputs
if options.generate_propka_input:
my_molecule.write_propka()
if write_pka:
my_molecule.write_pka()

return my_molecule
101 changes: 101 additions & 0 deletions tests/test_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Tests for PROPKA's run module"""
import logging
import os
from pathlib import Path
from io import StringIO
import pytest
import propka.run as pkrun

from .test_basic_regression import compare_output
from .test_streamio import get_paths


_LOGGER = logging.getLogger(__name__)


@pytest.mark.parametrize("pdb, options", [
pytest.param("1FTJ-Chain-A", (), id="1FTJ-Chain-A: no options"),
pytest.param('3SGB-subset', (
"--titrate_only",
"E:17,E:18,E:19,E:29,E:44,E:45,E:46,E:118,E:119,E:120,E:139"),
id="3SGB: --titrate_only"),
pytest.param('1HPX-warn', ('--quiet',), id="1HPX-warn: --quiet"),
])
def test_single_file(tmpdir, pdb, options):
"""Basic regression test using propka.run.single and local file for the
input PDB file"""
ref_path, pdb_path = get_paths(pdb)
filename = str(pdb_path)

with tmpdir.as_cwd():
pkrun.single(filename, options)
compare_output(pdb, Path.cwd(), ref_path)


@pytest.mark.parametrize("pdb, options", [
pytest.param("1FTJ-Chain-A", (), id="1FTJ-Chain-A: no options"),
pytest.param('3SGB-subset', (
"--titrate_only",
"E:17,E:18,E:19,E:29,E:44,E:45,E:46,E:118,E:119,E:120,E:139"),
id="3SGB: --titrate_only"),
pytest.param('1HPX-warn',('--quiet',), id="1HPX-warn: --quiet"),
])
def test_single_filestream(tmpdir, pdb, options):
"""Basic regression test using StringIO streams for the input PDB file"""
ref_path, pdb_path = get_paths(pdb)
filename = f"{pdb}.pdb"

with open(pdb_path, 'r') as writer:
filestream = StringIO(writer.read())

with tmpdir.as_cwd():
pkrun.single(filename, options, stream=filestream)
compare_output(pdb, Path.cwd(), ref_path)

filestream.close()


def test_single_nopka(tmpdir):
"""Basic test to check that the pKa file is not written when write_pka is
`False`"""
pdb = "1FTJ-Chain-A"
ref_path, pdb_path = get_paths(pdb)
filename = f"{pdb}.pdb"

with open(pdb_path, 'r') as writer:
filestream = StringIO(writer.read())

pkrun.single(filename, stream=filestream, write_pka=False)
assert not os.path.isfile(f"{pdb}.pka")


def test_single_propka_input(tmpdir):
"""Basic test to check that the propka_input file is written when
`--generate-propka-input` is passed"""
pdb = "1FTJ-Chain-A"
options = ('--generate-propka-input',)
ref_path, pdb_path = get_paths(pdb)
filename = f"{pdb}.pdb"

with open(pdb_path, 'r') as writer:
filestream = StringIO(writer.read())

with tmpdir.as_cwd():
pkrun.single(filename, options, stream=filestream)
assert os.path.isfile(f"{pdb}.propka_input")


def test_single_extra_files_logwarn(tmpdir, caplog):
"""Tests that a logging warning is thrown if passing files via optargs"""
pdb = "1FTJ-Chain-A"
options = ('-f foo.pdb bar.pdb', '-f test.pdb test2.pdb',
'--generate-propka-input')
ref_path, pdb_path = get_paths(pdb)
filename = str(pdb_path)

with tmpdir.as_cwd():
pkrun.single(filename, options)

wmsg = ("Ignoring extra filenames passed: [' foo.pdb bar.pdb', "
"' test.pdb test2.pdb']")
assert wmsg in caplog.records[0].message

0 comments on commit 7563a70

Please sign in to comment.