Skip to content

Commit

Permalink
Add type annotations and mypy CI job
Browse files Browse the repository at this point in the history
  • Loading branch information
speleo3 committed Oct 26, 2023
1 parent ee89e36 commit 57d7c67
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 149 deletions.
10 changes: 10 additions & 0 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,13 @@ jobs:
with:
name: coverage-html
path: htmlcov/*

static_type_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: "3.12"
- run: python -m pip install mypy types-setuptools
- run: mypy
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,5 @@
docs/build
docs/source/api/*.rst
build/
dist/
.coverage
47 changes: 19 additions & 28 deletions propka/atom.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@
"""

import string
from typing import cast, List, NoReturn, Optional, TYPE_CHECKING

from propka.lib import make_tidy_atom_label
from . import hybrid36

if TYPE_CHECKING:
from propka.group import Group
from propka.molecular_container import MolecularContainer
from propka.conformation_container import ConformationContainer

# Format strings that get used in multiple places (or are very complex)
PDB_LINE_FMT1 = (
Expand Down Expand Up @@ -37,37 +43,24 @@ class Atom:
removed as reading/writing PROPKA input is no longer supported.
"""

def __init__(self, line=None):
def __init__(self, line: Optional[str] = None):
"""Initialize Atom object.
Args:
line: Line from a PDB file to set properties of atom.
"""
self.occ = None
self.numb = None
self.res_name = None
self.type = None
self.chain_id = None
self.beta = None
self.icode = None
self.res_num = None
self.name = None
self.element = None
self.x = None
self.y = None
self.z = None
self.group = None
self.group_type = None
self.number_of_bonded_elements = {}
self.cysteine_bridge = False
self.bonded_atoms = []
self.number_of_bonded_elements: NoReturn = cast(NoReturn, {}) # FIXME unused?
self.group: Optional[Group] = None
self.group_type: Optional[str] = None
self.cysteine_bridge: bool = False
self.bonded_atoms: List[Atom] = []
self.residue = None
self.conformation_container = None
self.molecular_container = None
self.conformation_container: Optional[ConformationContainer] = None
self.molecular_container: Optional[MolecularContainer] = None
self.is_protonated = False
self.steric_num_lone_pairs_set = False
self.terminal = None
self.charge = 0
self.terminal: Optional[str] = None
self.charge = 0.0
self.charge_set = False
self.steric_number = 0
self.number_of_lone_pairs = 0
Expand All @@ -84,7 +77,7 @@ def __init__(self, line=None):
self.sybyl_assigned = False
self.marvin_pka = False

def set_properties(self, line):
def set_properties(self, line: Optional[str]):
"""Line from PDB file to set properties of atom.
Args:
Expand Down Expand Up @@ -112,10 +105,8 @@ def set_properties(self, line):
self.z = float(line[46:54].strip())
self.res_num = int(line[22:26].strip())
self.res_name = "{0:<3s}".format(line[17:20].strip())
self.chain_id = line[21]
# Set chain id to "_" if it is just white space.
if not self.chain_id.strip():
self.chain_id = '_'
self.chain_id = line[21].strip() or '_'
self.type = line[:6].strip().lower()

# TODO - define nucleic acid residue names elsewhere
Expand All @@ -134,7 +125,7 @@ def set_properties(self, line):
self.element = '{0:1s}{1:1s}'.format(
self.element[0], self.element[1].lower())

def set_group_type(self, type_):
def set_group_type(self, type_: str):
"""Set group type of atom.
Args:
Expand Down
6 changes: 5 additions & 1 deletion propka/bonds.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
import json
import pkg_resources
import propka.calculations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from propka.molecular_container import MolecularContainer


_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -329,7 +333,7 @@ def check_distance(self, atom1, atom2):
return True
return False

def find_bonds_for_molecules_using_boxes(self, molecules):
def find_bonds_for_molecules_using_boxes(self, molecules: "MolecularContainer"):
""" Finds all bonds for a molecular container.
Args:
Expand Down
15 changes: 11 additions & 4 deletions propka/calculations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@
"""

import math
from typing import Iterable, Optional, Tuple, TypeVar
from .vector_algebra import _XYZ

_BoundXYZ_1 = TypeVar("_BoundXYZ_1", bound=_XYZ)
_BoundXYZ_2 = TypeVar("_BoundXYZ_2", bound=_XYZ)

#: Maximum distance used to bound calculations of smallest distance
MAX_DISTANCE = 1e6


def squared_distance(atom1, atom2):
def squared_distance(atom1: _XYZ, atom2: _XYZ) -> float:
"""Calculate the squared distance between two atoms.
Args:
Expand All @@ -28,7 +32,7 @@ def squared_distance(atom1, atom2):
return res


def distance(atom1, atom2):
def distance(atom1: _XYZ, atom2: _XYZ) -> float:
"""Calculate the distance between two atoms.
Args:
Expand All @@ -40,7 +44,10 @@ def distance(atom1, atom2):
return math.sqrt(squared_distance(atom1, atom2))


def get_smallest_distance(atoms1, atoms2):
def get_smallest_distance(
atoms1: Iterable[_BoundXYZ_1],
atoms2: Iterable[_BoundXYZ_2],
) -> Tuple[Optional[_BoundXYZ_1], float, Optional[_BoundXYZ_2]]:
"""Calculate the smallest distance between two groups of atoms.
Args:
Expand All @@ -59,4 +66,4 @@ def get_smallest_distance(atoms1, atoms2):
res_dist = dist
res_atom1 = atom1
res_atom2 = atom2
return [res_atom1, math.sqrt(res_dist), res_atom2]
return (res_atom1, math.sqrt(res_dist), res_atom2)
46 changes: 29 additions & 17 deletions propka/conformation_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@
"""
import logging
import functools
from typing import Iterable, List, NoReturn, Optional, TYPE_CHECKING, Set

if TYPE_CHECKING:
from propka.atom import Atom
from propka.molecular_container import MolecularContainer

import propka.ligand
from propka.output import make_interaction_map
from propka.determinant import Determinant
from propka.coupled_groups import NCCG
from propka.determinants import set_backbone_determinants, set_ion_determinants
from propka.determinants import set_determinants
from propka.group import Group, is_group
from typing import Iterable


_LOGGER = logging.getLogger(__name__)
Expand All @@ -38,7 +43,10 @@ class ConformationContainer:
PROPKA inputs is no longer supported.
"""

def __init__(self, name='', parameters=None, molecular_container=None):
def __init__(self,
name: str = '',
parameters=None,
molecular_container: Optional["MolecularContainer"] = None):
"""Initialize conformation container.
Args:
Expand All @@ -49,9 +57,9 @@ def __init__(self, name='', parameters=None, molecular_container=None):
self.molecular_container = molecular_container
self.name = name
self.parameters = parameters
self.atoms = []
self.groups = []
self.chains = []
self.atoms: List["Atom"] = []
self.groups: List[Group] = []
self.chains: List[str] = []
self.current_iter_item = 0
self.marvin_pkas_calculated = False
self.non_covalently_coupled_groups = False
Expand Down Expand Up @@ -126,7 +134,8 @@ def find_non_covalently_coupled_groups(self, verbose=False):
self.get_titratable_groups()))) > 0:
self.non_covalently_coupled_groups = True

def find_bonded_titratable_groups(self, atom, num_bonds, original_atom):
def find_bonded_titratable_groups(self, atom: "Atom", num_bonds: int,
original_atom: "Atom"):
"""Find bonded titrable groups.
Args:
Expand All @@ -136,7 +145,7 @@ def find_bonded_titratable_groups(self, atom, num_bonds, original_atom):
Returns:
a set of bonded atom groups
"""
res = set()
res: Set[Group] = set()
for bond_atom in atom.bonded_atoms:
# skip the original atom
if bond_atom == original_atom:
Expand All @@ -152,7 +161,7 @@ def find_bonded_titratable_groups(self, atom, num_bonds, original_atom):
bond_atom, num_bonds+1, original_atom)
return res

def setup_and_add_group(self, group):
def setup_and_add_group(self, group: Optional[Group]):
"""Check if we want to include this group in the calculations.
Args:
Expand All @@ -166,7 +175,7 @@ def setup_and_add_group(self, group):
self.init_group(group)
self.groups.append(group)

def init_group(self, group):
def init_group(self, group: Group):
"""Initialize the given Group object.
Args:
Expand All @@ -178,10 +187,11 @@ def init_group(self, group):

# If --titrate_only option is set, make non-specified residues
# un-titratable:
assert self.molecular_container is not None
titrate_only = self.molecular_container.options.titrate_only
if titrate_only is not None:
atom = group.atom
if not (atom.chain_id, atom.res_num, atom.icode) in titrate_only:
if (atom.chain_id, atom.res_num, atom.icode) not in titrate_only:
group.titratable = False
if group.residue_type == 'CYS':
group.exclude_cys_from_results = True
Expand Down Expand Up @@ -475,25 +485,27 @@ def get_ions(self):
group for group in self.groups
if group.residue_type in self.parameters.ions.keys()]

def get_group_names(self, group_list):
def get_group_names(self, group_list: NoReturn) -> NoReturn: # FIXME unused?
"""Get names of groups in list.
Args:
group_list: list to check
Returns:
list of groups
"""
if TYPE_CHECKING:
assert False
return [group for group in self.groups if group.type in group_list]

def get_ligand_atoms(self):
def get_ligand_atoms(self) -> List["Atom"]:
"""Get atoms associated with ligands.
Returns:
list of atoms
"""
return [atom for atom in self.atoms if atom.type == 'hetatm']

def get_heavy_ligand_atoms(self):
def get_heavy_ligand_atoms(self) -> List["Atom"]:
"""Get heavy atoms associated with ligands.
Returns:
Expand All @@ -503,7 +515,7 @@ def get_heavy_ligand_atoms(self):
atom for atom in self.atoms
if atom.type == 'hetatm' and atom.element != 'H']

def get_chain(self, chain):
def get_chain(self, chain: str) -> List["Atom"]:
"""Get atoms associated with a specific chain.
Args:
Expand All @@ -513,7 +525,7 @@ def get_chain(self, chain):
"""
return [atom for atom in self.atoms if atom.chain_id != chain]

def add_atom(self, atom):
def add_atom(self, atom: "Atom"):
"""Add atom to container.
Args:
Expand Down Expand Up @@ -556,7 +568,7 @@ def top_up(self, other):
"""
self.top_up_from_atoms(other.atoms)

def top_up_from_atoms(self, other_atoms: Iterable["propka.atom.Atom"]):
def top_up_from_atoms(self, other_atoms: Iterable["Atom"]):
"""Adds atoms which are missing from this container.
Args:
Expand Down Expand Up @@ -613,7 +625,7 @@ def sort_atoms(self):
self.atoms[i].numb = i+1

@staticmethod
def sort_atoms_key(atom):
def sort_atoms_key(atom: "Atom") -> float:
"""Generate key for atom sorting.
Args:
Expand Down
Loading

0 comments on commit 57d7c67

Please sign in to comment.