Typing: Options

This commit is contained in:
Thomas Holder
2023-12-13 15:18:39 +01:00
parent 723609cc33
commit 0aafca7f73
10 changed files with 184 additions and 92 deletions

View File

@@ -6,7 +6,8 @@ Container data structure for molecular conformations.
"""
import logging
import functools
from typing import Iterable, List, NoReturn, Optional, TYPE_CHECKING, Set
from typing import Callable, Dict, Iterable, Iterator, List, NoReturn, Optional, TYPE_CHECKING, Set
from propka.lib import Options
from propka.version import Version
@@ -26,6 +27,8 @@ from propka.parameters import Parameters
_LOGGER = logging.getLogger(__name__)
CallableGroupToGroups = Callable[[Group], List[Group]]
#: A large number that gets multipled with the integer obtained from applying
#: :func:`ord` to the atom chain ID. Used in calculating atom keys for
@@ -276,7 +279,7 @@ class ConformationContainer:
return penalised_labels
@staticmethod
def share_determinants(groups):
def share_determinants(groups: Iterable[Group]):
"""Share sidechain, backbone, and Coloumb determinants between groups.
Args:
@@ -286,7 +289,7 @@ class ConformationContainer:
types = ['sidechain', 'backbone', 'coulomb']
for type_ in types:
# find maximum value for each determinant
max_dets = {}
max_dets: Dict[Group, float] = {}
for group in groups:
for det in group.determinants[type_]:
# update max dets
@@ -302,7 +305,11 @@ class ConformationContainer:
for group in groups:
group.set_determinant(new_determinant, type_)
def get_coupled_systems(self, groups, get_coupled_groups):
def get_coupled_systems(
self,
groups: Iterable[Group],
get_coupled_groups: CallableGroupToGroups,
) -> Iterator[Set[Group]]:
"""A generator that yields covalently coupled systems.
Args:
@@ -314,15 +321,16 @@ class ConformationContainer:
groups = set(groups)
while len(groups) > 0:
# extract a system of coupled groups ...
system = set()
system: Set[Group] = set()
self.get_a_coupled_system_of_groups(
groups.pop(), system, get_coupled_groups)
# ... and remove them from the list
groups -= system
yield system
def get_a_coupled_system_of_groups(self, new_group, coupled_groups,
get_coupled_groups):
def get_a_coupled_system_of_groups(self, new_group: Group,
coupled_groups: Set[Group],
get_coupled_groups: CallableGroupToGroups):
"""Set up coupled systems of groups.
Args:
@@ -353,7 +361,7 @@ class ConformationContainer:
reference=reference)
return ddg
def calculate_charge(self, parameters, ph: float):
def calculate_charge(self, parameters: Parameters, ph: float):
"""Calculate charge for folded and unfolded states.
Args:
@@ -371,7 +379,7 @@ class ConformationContainer:
state='folded')
return unfolded, folded
def get_backbone_groups(self):
def get_backbone_groups(self) -> List[Group]:
"""Get backbone groups needed for the pKa calculations.
Returns:

View File

@@ -6,9 +6,11 @@ Describe and analyze energetic coupling between groups.
"""
import logging
import itertools
from typing import Optional
import propka.lib
from propka.group import Group
from propka.output import make_interaction_map
from propka.parameters import Parameters
_LOGGER = logging.getLogger(__name__)
@@ -16,9 +18,8 @@ _LOGGER = logging.getLogger(__name__)
class NonCovalentlyCoupledGroups:
"""Groups that are coupled without covalent bonding."""
def __init__(self):
self.parameters = None
self.do_prot_stat = True
parameters: Optional[Parameters] = None
do_prot_stat = True
def is_coupled_protonation_state_probability(self, group1, group2,
energy_method,
@@ -264,7 +265,7 @@ class NonCovalentlyCoupledGroups:
_LOGGER.info(swap_info)
@staticmethod
def get_interaction(group1, group2, include_side_chain_hbs=True):
def get_interaction(group1: Group, group2: Group, include_side_chain_hbs=True):
"""Get interaction energy between two groups.
Args:

View File

@@ -15,6 +15,11 @@ Provides the :class:`Determinant` class.
"""
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from propka.group import Group
class Determinant:
"""Determinant class.
@@ -25,7 +30,7 @@ class Determinant:
TODO - figure out what this class does.
"""
def __init__(self, group, value):
def __init__(self, group: "Group", value: float):
"""Initialize the object.
Args:
@@ -36,7 +41,7 @@ class Determinant:
self.label = group.label
self.value = value
def add(self, value):
def add(self, value: float):
"""Increment determinant value.
Args:

View File

@@ -9,8 +9,7 @@ Input routines.
Methods to read PROPKA input files (:func:`read_propka` and
:func:`get_atom_lines_from_input`) have been removed.
"""
import typing
from typing import Iterator, Tuple, Union
from typing import IO, ContextManager, Dict, Iterable, Iterator, Optional, Tuple
import contextlib
import io
import zipfile
@@ -19,19 +18,18 @@ from propka.lib import protein_precheck
from propka.atom import Atom
from propka.conformation_container import ConformationContainer
from propka.molecular_container import MolecularContainer
from propka.output import _PathArg, _PathLikeTypes, _TextIOSource
from propka.parameters import Parameters
def open_file_for_reading(
input_file: typing.Union[str, Path, typing.TextIO]
) -> typing.ContextManager[typing.TextIO]:
def open_file_for_reading(input_file: _TextIOSource) -> ContextManager[IO[str]]:
"""Open file or file-like stream for reading.
Args:
input_file: path to file or file-like object. If file-like object,
then will attempt seek(0).
"""
if not isinstance(input_file, (str, Path)):
if not isinstance(input_file, _PathLikeTypes):
input_file.seek(0)
return contextlib.nullcontext(input_file)
@@ -48,7 +46,11 @@ def open_file_for_reading(
return contextlib.closing(open(input_file, 'rt'))
def read_molecule_file(filename: str, mol_container: MolecularContainer, stream=None) -> MolecularContainer:
def read_molecule_file(
filename: _PathArg,
mol_container: MolecularContainer,
stream: Optional[IO[str]] = None,
) -> MolecularContainer:
"""Read input file or stream (PDB or PROPKA) for a molecular container
Args:
@@ -96,11 +98,7 @@ def read_molecule_file(filename: str, mol_container: MolecularContainer, stream=
input_path = Path(filename)
mol_container.name = input_path.stem
input_file_extension = input_path.suffix
if stream is not None:
input_file = stream
else:
input_file = filename
input_file = filename if stream is None else stream
if input_file_extension.lower() == '.pdb':
# input is a pdb file. read in atoms and top up containers to make
@@ -133,7 +131,7 @@ def read_molecule_file(filename: str, mol_container: MolecularContainer, stream=
return mol_container
def read_parameter_file(input_file: Union[Path, str], parameters: Parameters) -> Parameters:
def read_parameter_file(input_file: _PathArg, parameters: Parameters) -> Parameters:
"""Read a parameter file.
Args:
@@ -161,8 +159,13 @@ def conformation_sorter(conf: str) -> int:
return model*100+ord(altloc)
def get_atom_lines_from_pdb(pdb_file, ignore_residues=[], keep_protons=False,
tags=['ATOM ', 'HETATM'], chains=None) -> Iterator[Tuple[str, Atom]]:
def get_atom_lines_from_pdb(
pdb_file: _TextIOSource,
ignore_residues: Iterable[str] = (),
keep_protons: bool = False,
tags: Iterable[str] = ('ATOM ', 'HETATM'),
chains: Optional[Iterable[str]] = None,
) -> Iterator[Tuple[str, Atom]]:
"""Get atom lines from PDB file.
Args:
@@ -228,7 +231,8 @@ def get_atom_lines_from_pdb(pdb_file, ignore_residues=[], keep_protons=False,
terminal = None
def read_pdb(pdb_file, parameters, molecule):
def read_pdb(pdb_file: _TextIOSource, parameters: Parameters,
molecule: MolecularContainer):
"""Parse a PDB file.
Args:
@@ -240,7 +244,7 @@ def read_pdb(pdb_file, parameters, molecule):
1. list of conformations
2. list of names
"""
conformations = {}
conformations: Dict[str, ConformationContainer] = {}
# read in all atoms in the file
lines = get_atom_lines_from_pdb(
pdb_file, ignore_residues=parameters.ignore_residues,
@@ -253,4 +257,4 @@ def read_pdb(pdb_file, parameters, molecule):
conformations[name].add_atom(atom)
# make a sorted list of conformation names
names = sorted(conformations.keys(), key=conformation_sorter)
return [conformations, names]
return conformations, names

View File

@@ -304,7 +304,7 @@ def add_determinants(iterative_interactions: List[Interaction], version: Version
for itres in iteratives:
for type_ in ['sidechain', 'backbone', 'coulomb']:
for interaction in itres.determinants[type_]:
value = interaction[1]
value: float = interaction[1]
if value > UNK_MIN_VALUE or value < -UNK_MIN_VALUE:
group = interaction[0]
new_det = Determinant(group, value)

View File

@@ -5,16 +5,20 @@ Set-up of a PROPKA calculation
Implements many of the main functions used to call PROPKA.
"""
import sys
import logging
import argparse
from pathlib import Path
from typing import List, TYPE_CHECKING
from typing import Iterable, Iterator, List, TYPE_CHECKING, NoReturn, Optional, Tuple, TypeVar
if TYPE_CHECKING:
from propka.atom import Atom
T = TypeVar("T")
Number = TypeVar("Number", int, float)
_T_RESIDUE_TUPLE = Tuple[str, int, str]
_LOGGER = logging.getLogger(__name__)
@@ -23,7 +27,28 @@ EXPECTED_ATOM_NUMBERS = {'ALA': 5, 'ARG': 11, 'ASN': 8, 'ASP': 8, 'CYS': 6,
'LEU': 8, 'LYS': 9, 'MET': 8, 'PHE': 11, 'PRO': 7,
'SER': 6, 'THR': 7, 'TRP': 14, 'TYR': 12, 'VAL': 7}
Options = argparse.Namespace
class Options:
# Note: All the "NoReturn" members appear to be unused
alignment: NoReturn # Optional[List[str]]
chains: Optional[List[str]]
display_coupled_residues: bool = False
filenames: List[str] # List[Path]?
grid: Tuple[float, float, float] = (0.0, 14.0, 0.1)
input_pdb: str # Path?
keep_protons: bool = False
log_level: str = 'INFO'
mutations: NoReturn # Optional[List[str]]
mutator: NoReturn # Optional[str] # alignment/scwrl/jackal
mutator_options: NoReturn # Optional[List[str]]
pH: NoReturn # float = 7.0
parameters: Path
protonate_all: bool = False
reference: NoReturn # str = 'neutral'
reuse_ligand_mol2_file: bool = False # only used by unused function
thermophiles: NoReturn # Optional[List[str]]
titrate_only: Optional[List[_T_RESIDUE_TUPLE]]
window: Tuple[float, float, float] = (0.0, 14.0, 1.0)
def protein_precheck(conformations, names):
@@ -112,11 +137,11 @@ def make_molecule(atom: "Atom", atoms: List["Atom"]):
return res_atoms
def make_grid(min_, max_, step):
def make_grid(min_: Number, max_: Number, step: Number) -> Iterator[Number]:
"""Make a grid across the specified tange.
TODO - figure out if this duplicates existing generators like `range` or
numpy function.
Like range() for integers or numpy.arange() for floats, except that `max_`
is not excluded from the range.
Args:
min_: minimum value of grid
@@ -129,7 +154,7 @@ def make_grid(min_, max_, step):
x += step
def generate_combinations(interactions):
def generate_combinations(interactions: Iterable[T]) -> List[List[T]]:
"""Generate combinations of interactions.
Args:
@@ -137,14 +162,14 @@ def generate_combinations(interactions):
Returns:
list of combinations
"""
res = [[]]
res: List[List[T]] = [[]]
for interaction in interactions:
res = make_combination(res, interaction)
res.remove([])
return res
def make_combination(combis, interaction):
def make_combination(combis: List[List[T]], interaction: T) -> List[List[T]]:
"""Make a specific set of combinations.
Args:
@@ -160,7 +185,7 @@ def make_combination(combis, interaction):
return res
def parse_res_string(res_str):
def parse_res_string(res_str: str) -> _T_RESIDUE_TUPLE:
"""Parse a residue string.
Args:
@@ -189,6 +214,16 @@ def parse_res_string(res_str):
return chain, resnum, inscode
def parse_res_list(titrate_only: str):
res_list: List[_T_RESIDUE_TUPLE] = []
for res_str in titrate_only.split(','):
try:
res_list.append(parse_res_string(res_str))
except ValueError as ex:
raise argparse.ArgumentTypeError(f'{ex}: "{res_str:s}"')
return res_list
def build_parser(parser=None):
"""Build an argument parser for PROPKA.
@@ -233,6 +268,7 @@ def build_parser(parser=None):
'" " for chains without ID [all]'))
group.add_argument(
"-i", "--titrate_only", dest="titrate_only",
type=parse_res_list,
help=('Treat only the specified residues as titratable. Value should '
'be a comma-separated list of "chain:resnum" values; for '
'example: -i "A:10,A:11"'))
@@ -252,8 +288,8 @@ def build_parser(parser=None):
"--version", action="version", version=f"%(prog)s {propka.__version__}")
group.add_argument(
"-p", "--parameters", dest="parameters",
default=str(Path(__file__).parent / "propka.cfg"),
help="set the parameter file [{default:s}]")
type=Path, default=Path(__file__).parent / "propka.cfg",
help="set the parameter file")
try:
group.add_argument(
"--log-level",
@@ -325,18 +361,6 @@ def loadOptions(args=None) -> Options:
# adding specified filenames to arguments
options.filenames.append(options.input_pdb)
# Convert titrate_only string to a list of (chain, resnum) items:
if options.titrate_only is not None:
res_list = []
for res_str in options.titrate_only.split(','):
try:
chain, resnum, inscode = parse_res_string(res_str)
except ValueError:
_LOGGER.critical(
'Invalid residue string: "{0:s}"'.format(res_str))
sys.exit(1)
res_list.append((chain, resnum, inscode))
options.titrate_only = res_list
# Set the no-print variable
level = getattr(logging, options.log_level)
_LOGGER.setLevel(level)

View File

@@ -157,7 +157,7 @@ class MolecularContainer:
conformation='AVR', reference=reference)
def get_folding_profile(self, conformation='AVR', reference="neutral",
grid=[0., 14., 0.1]):
grid: Tuple[float, float, float] = (0., 14., 0.1)):
"""Get a folding profile.
Args:
@@ -174,25 +174,25 @@ class MolecularContainer:
4. stability_range
"""
# calculate stability profile
profile = []
profile: List[Tuple[float, float]] = []
for ph in make_grid(*grid):
conf = self.conformations[conformation]
ddg = conf.calculate_folding_energy(ph=ph, reference=reference)
profile.append([ph, ddg])
profile.append((ph, ddg))
# find optimum
opt = [None, 1e6]
opt: Tuple[Optional[float], float] = (None, 1e6)
for point in profile:
opt = min(opt, point, key=lambda v: v[1])
# find values within 80 % of optimum
range_80pct = [None, None]
range_80pct: Tuple[Optional[float], Optional[float]] = (None, None)
values_within_80pct = [p[0] for p in profile if p[1] < 0.8*opt[1]]
if len(values_within_80pct) > 0:
range_80pct = [min(values_within_80pct), max(values_within_80pct)]
range_80pct = (min(values_within_80pct), max(values_within_80pct))
# find stability range
stability_range = [None, None]
stability_range: Tuple[Optional[float], Optional[float]] = (None, None)
stable_values = [p[0] for p in profile if p[1] < 0.0]
if len(stable_values) > 0:
stability_range = [min(stable_values), max(stable_values)]
stability_range = (min(stable_values), max(stable_values))
return profile, opt, range_80pct, stability_range
def get_charge_profile(self, conformation: str = 'AVR', grid=[0., 14., .1]):

View File

@@ -12,13 +12,29 @@ Output routines.
import logging
from datetime import date
from decimal import Decimal
from os import PathLike
from pathlib import Path
from typing import IO, AnyStr, List, NoReturn, Optional, Union, TYPE_CHECKING
import warnings
from .parameters import Parameters
from . import __version__
if TYPE_CHECKING:
from .atom import Atom
from .conformation_container import ConformationContainer
from .molecular_container import MolecularContainer
# https://docs.python.org/3/glossary.html#term-path-like-object
_PathLikeTypes = (PathLike, str)
_PathArg = Union[PathLike, str]
_IOSource = Union[IO[AnyStr], PathLike, str]
_TextIOSource = _IOSource[str]
_LOGGER = logging.getLogger(__name__)
def open_file_for_writing(input_file):
def open_file_for_writing(input_file: _TextIOSource) -> IO[str]:
"""Open file or file-like stream for writing.
TODO - convert this to a context manager.
@@ -27,17 +43,13 @@ def open_file_for_writing(input_file):
input_file: path to file or file-like object. If file-like object,
then will attempt to get file mode.
"""
try:
if not input_file.writable():
raise IOError("File/stream not open for writing")
return input_file
except AttributeError:
pass
try:
file_ = open(input_file, 'wt')
except FileNotFoundError:
raise Exception('Could not open {0:s}'.format(input_file))
return file_
if isinstance(input_file, _PathLikeTypes):
return open(input_file, 'wt')
if not input_file.writable():
raise IOError("File/stream not open for writing")
return input_file
def write_file(filename, lines):
@@ -47,6 +59,7 @@ def write_file(filename, lines):
filename: name of file
lines: lines to write to file
"""
warnings.warn("unused and untested by propka")
file_ = open_file_for_writing(filename)
for line in lines:
file_.write("{0:s}\n".format(line))
@@ -101,17 +114,22 @@ def write_pdb_for_protein(
pdbfile.close()
def write_pdb_for_conformation(conformation, filename):
def write_pdb_for_conformation(conformation: "ConformationContainer",
filename: _PathArg):
"""Write PDB conformation to a file.
Args:
conformation: conformation container
filename: filename for output
"""
warnings.warn("unused and untested by propka")
write_pdb_for_atoms(conformation.atoms, filename)
def write_pka(protein, parameters, filename=None, conformation='1A',
def write_pka(protein: "MolecularContainer",
parameters: Parameters,
filename: Optional[_PathArg] = None,
conformation='1A',
reference="neutral", _="folding", verbose=False,
__=None):
"""Write the pKa-file based on the given protein.
@@ -129,8 +147,6 @@ def write_pka(protein, parameters, filename=None, conformation='1A',
verbose = True
if filename is None:
filename = "{0:s}.pka".format(protein.name)
# TODO - this would be much better with a context manager
file_ = open(filename, 'w')
if verbose:
_LOGGER.info("Writing {0:s}".format(filename))
# writing propka header
@@ -149,11 +165,10 @@ def write_pka(protein, parameters, filename=None, conformation='1A',
# printing Protein Charge Profile
str_ += get_charge_profile_section(protein, conformation=conformation)
# now, writing the pka text to file
file_.write(str_)
file_.close()
Path(filename).write_text(str_, encoding="utf-8")
def print_tm_profile(protein, reference="neutral", window=[0., 14., 1.],
def print_tm_profile(protein: NoReturn, reference="neutral", window=[0., 14., 1.],
__=[0., 0.], tms=None, ref=None, _=False,
options=None):
"""Print Tm profile.
@@ -186,7 +201,7 @@ def print_tm_profile(protein, reference="neutral", window=[0., 14., 1.],
_LOGGER.info(str_)
def print_result(protein, conformation, parameters):
def print_result(protein: "MolecularContainer", conformation: str, parameters: Parameters):
"""Prints all resulting output from determinants and down.
Args:
@@ -197,7 +212,7 @@ def print_result(protein, conformation, parameters):
print_pka_section(protein, conformation, parameters)
def print_pka_section(protein, conformation, parameters):
def print_pka_section(protein: "MolecularContainer", conformation: str, parameters: Parameters):
"""Prints out pKa section of results.
Args:
@@ -212,7 +227,7 @@ def print_pka_section(protein, conformation, parameters):
_LOGGER.info("pKa summary:\n%s", str_)
def get_determinant_section(protein, conformation, parameters):
def get_determinant_section(protein: "MolecularContainer", conformation: str, parameters: Parameters):
"""Returns string with determinant section of results.
Args:
@@ -244,7 +259,8 @@ def get_determinant_section(protein, conformation, parameters):
return str_
def get_summary_section(protein, conformation, parameters):
def get_summary_section(protein: "MolecularContainer", conformation: str,
parameters: Parameters):
"""Returns string with summary section of the results.
Args:
@@ -266,7 +282,8 @@ def get_summary_section(protein, conformation, parameters):
def get_folding_profile_section(
protein, conformation='AVR', direction="folding", reference="neutral",
protein: "MolecularContainer",
conformation='AVR', direction="folding", reference="neutral",
window=[0., 14., 1.0], _=False, __=None):
"""Returns string with the folding profile section of the results.
@@ -373,6 +390,7 @@ def write_scwrl_sequence_file(sequence, filename="x-ray.seq", _=None):
TODO - figure out what this is
"""
warnings.warn("unused and untested by propka")
with open(filename, 'w') as file_:
start = 0
while len(sequence[start:]) > 60:
@@ -536,7 +554,7 @@ def make_interaction_map(name, list_, interaction):
return res
def write_pdb_for_atoms(atoms, filename, make_conect_section=False):
def write_pdb_for_atoms(atoms: List["Atom"], filename: _PathArg, make_conect_section=False):
"""Write out PDB file for atoms.
Args:

View File

@@ -12,10 +12,12 @@ function. If similar functionality is desired from a Python script
"""
import logging
import sys
from typing import IO, Iterable, Optional
from propka.lib import loadOptions
from propka.input import read_parameter_file, read_molecule_file
from propka.parameters import Parameters
from propka.molecular_container import MolecularContainer
from propka.output import _PathArg
_LOGGER = logging.getLogger(__name__)
@@ -44,7 +46,9 @@ def main(optargs=None):
my_molecule.write_pka()
def single(filename: str, optargs: tuple = (), stream=None,
def single(filename: _PathArg,
optargs: Iterable[str] = (),
stream: Optional[IO[str]] = None,
write_pka: bool = True):
"""Run a single PROPKA calculation using ``filename`` as input.
@@ -105,6 +109,7 @@ def single(filename: str, optargs: tuple = (), stream=None,
.. versionchanged:: 3.4.0
Removed ability to write out PROPKA input files.
"""
filename = str(filename)
# Deal with input optarg options
optargs = tuple(optargs)
optargs += (filename,)

27
tests/test_lib.py Normal file
View File

@@ -0,0 +1,27 @@
import propka.lib as m
import argparse
import pytest
def test_parse_res_string():
assert m.parse_res_string("C:123") == ("C", 123, " ")
assert m.parse_res_string("C:123B") == ("C", 123, "B")
assert m.parse_res_string("ABC:123x") == ("ABC", 123, "x")
with pytest.raises(ValueError):
m.parse_res_string("C:B123")
with pytest.raises(ValueError):
m.parse_res_string("123B")
with pytest.raises(ValueError):
m.parse_res_string("C:123:B")
def test_parse_res_list():
assert m.parse_res_list("C:123") == [("C", 123, " ")]
assert m.parse_res_list("ABC:123,D:4,F:56X") == [
("ABC", 123, " "),
("D", 4, " "),
("F", 56, "X"),
]
with pytest.raises(argparse.ArgumentTypeError):
m.parse_res_list("C:B123")