Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes run's single method #85

Merged
merged 6 commits into from
Jul 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
IAlibay marked this conversation as resolved.
Show resolved Hide resolved
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"""
IAlibay marked this conversation as resolved.
Show resolved Hide resolved
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