Static typing for Bio.PDB (#4641)

mypy type checking had been disabled for Bio.PDB
This commit is contained in:
Thomas Holder
2024-02-29 23:59:50 +01:00
committed by GitHub
parent dd216dda76
commit f103682c22
12 changed files with 101 additions and 37 deletions

View File

@ -17,8 +17,32 @@ warn_redundant_casts = True
warn_unused_configs = True warn_unused_configs = True
[mypy-Bio/PDB/*] [mypy-Bio/PDB/*]
disallow_incomplete_defs = False
[mypy-Bio.PDB.ccealign]
ignore_missing_imports = True
[mypy-Bio.PDB.ic_rebuild]
ignore_errors = True ignore_errors = True
[mypy-Bio.PDB.internal_coords]
ignore_errors = True
[mypy-Bio.PDB.kdtrees]
ignore_missing_imports = True
[mypy-Bio.PDB.PDBMLParser]
ignore_errors = True
[mypy-Bio.PDB.PICIO]
ignore_errors = True
[mypy-igraph.*]
ignore_missing_imports = True
[mypy-mmtf.*]
ignore_missing_imports = True
[mypy-numpy.*] [mypy-numpy.*]
ignore_missing_imports = True ignore_missing_imports = True

View File

@ -9,6 +9,7 @@
import copy import copy
import sys import sys
from typing import Optional, TYPE_CHECKING
import warnings import warnings
import numpy as np import numpy as np
@ -18,6 +19,9 @@ from Bio.PDB.PDBExceptions import PDBConstructionWarning
from Bio.PDB.vectors import Vector from Bio.PDB.vectors import Vector
from Bio.Data import IUPACData from Bio.Data import IUPACData
if TYPE_CHECKING:
from Bio.PDB.Residue import Residue
class Atom: class Atom:
"""Define Atom class. """Define Atom class.
@ -33,16 +37,16 @@ class Atom:
def __init__( def __init__(
self, self,
name, name: str,
coord, coord: np.ndarray,
bfactor, bfactor: Optional[float],
occupancy, occupancy: Optional[float],
altloc, altloc: str,
fullname, fullname: str,
serial_number, serial_number,
element=None, element: Optional[str] = None,
pqr_charge=None, pqr_charge: Optional[float] = None,
radius=None, radius: Optional[float] = None,
): ):
"""Initialize Atom object. """Initialize Atom object.
@ -76,7 +80,7 @@ class Atom:
""" """
self.level = "A" self.level = "A"
# Reference to the residue # Reference to the residue
self.parent = None self.parent: Optional["Residue"] = None
# the atomic data # the atomic data
self.name = name # eg. CA, spaces are removed from atom name self.name = name # eg. CA, spaces are removed from atom name
self.fullname = fullname # e.g. " CA ", spaces included self.fullname = fullname # e.g. " CA ", spaces included
@ -92,7 +96,7 @@ class Atom:
self.sigatm_array = None self.sigatm_array = None
self.serial_number = serial_number self.serial_number = serial_number
# Dictionary that keeps additional properties # Dictionary that keeps additional properties
self.xtra = {} self.xtra: dict = {}
assert not element or element == element.upper(), element assert not element or element == element.upper(), element
self.element = self._assign_element(element) self.element = self._assign_element(element)
self.mass = self._assign_atom_mass() self.mass = self._assign_atom_mass()

View File

@ -10,10 +10,14 @@
from Bio.PDB.Entity import Entity from Bio.PDB.Entity import Entity
from Bio.PDB.internal_coords import IC_Chain from Bio.PDB.internal_coords import IC_Chain
from typing import Optional from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from Bio.PDB.Model import Model
from Bio.PDB.Residue import Residue
class Chain(Entity): class Chain(Entity["Model", "Residue"]):
"""Define Chain class. """Define Chain class.
Chain is an object of type Entity, stores residues and includes a method to Chain is an object of type Entity, stores residues and includes a method to
@ -208,7 +212,8 @@ class Chain(Entity):
verbose=verbose, start=start, fin=fin verbose=verbose, start=start, fin=fin
) )
else: else:
structure = None if self.parent is None else self.parent.parent
raise Exception( raise Exception(
"Structure %s Chain %s does not have internal coordinates set" "Structure %s Chain %s does not have internal coordinates set"
% (self.parent.parent, self) % (structure, self)
) )

View File

@ -13,20 +13,32 @@ import warnings
from collections import deque from collections import deque
from copy import copy from copy import copy
from typing import TYPE_CHECKING, Any, Dict, Generic, List, Optional, TypeVar, Union
import numpy as np import numpy as np
from Bio import BiopythonWarning from Bio import BiopythonWarning
from Bio.PDB.PDBExceptions import PDBConstructionException from Bio.PDB.PDBExceptions import PDBConstructionException
if TYPE_CHECKING:
from Bio.PDB.Atom import Atom
class Entity: _Child = TypeVar("_Child", bound=Union["Entity", "Atom"])
_Parent = TypeVar("_Parent", bound=Optional["Entity"])
class Entity(Generic[_Parent, _Child]):
"""Basic container object for PDB hierarchy. """Basic container object for PDB hierarchy.
Structure, Model, Chain and Residue are subclasses of Entity. Structure, Model, Chain and Residue are subclasses of Entity.
It deals with storage and lookup. It deals with storage and lookup.
""" """
parent: Optional[_Parent]
child_list: List[_Child]
child_dict: Dict[Any, _Child]
level: str
def __init__(self, id): def __init__(self, id):
"""Initialize the class.""" """Initialize the class."""
self._id = id self._id = id
@ -174,7 +186,7 @@ class Entity:
""" """
if value == self._id: if value == self._id:
return return
if self.parent: if self.parent is not None:
if value in self.parent.child_dict: if value in self.parent.child_dict:
# See issue 1551 for details on the downgrade. # See issue 1551 for details on the downgrade.
warnings.warn( warnings.warn(
@ -200,7 +212,7 @@ class Entity:
""" """
return self.level return self.level
def set_parent(self, entity): def set_parent(self, entity: _Parent):
"""Set the parent Entity object.""" """Set the parent Entity object."""
self.parent = entity self.parent = entity
self._reset_full_id() self._reset_full_id()
@ -216,7 +228,7 @@ class Entity:
del self.child_dict[id] del self.child_dict[id]
self.child_list.remove(child) self.child_list.remove(child)
def add(self, entity): def add(self, entity: _Child):
"""Add a child to the Entity.""" """Add a child to the Entity."""
entity_id = entity.get_id() entity_id = entity.get_id()
if self.has_id(entity_id): if self.has_id(entity_id):
@ -225,7 +237,7 @@ class Entity:
self.child_list.append(entity) self.child_list.append(entity)
self.child_dict[entity_id] = entity self.child_dict[entity_id] = entity
def insert(self, pos, entity): def insert(self, pos: int, entity: _Child):
"""Add a child to the Entity at a specified position.""" """Add a child to the Entity at a specified position."""
entity_id = entity.get_id() entity_id = entity.get_id()
if self.has_id(entity_id): if self.has_id(entity_id):

View File

@ -8,8 +8,14 @@
from Bio.PDB.Entity import Entity from Bio.PDB.Entity import Entity
from Bio.PDB.internal_coords import IC_Chain from Bio.PDB.internal_coords import IC_Chain
from typing import TYPE_CHECKING
class Model(Entity): if TYPE_CHECKING:
from Bio.PDB.Chain import Chain
from Bio.PDB.Structure import Structure
class Model(Entity["Structure", "Chain"]):
"""The object representing a model in a structure. """The object representing a model in a structure.
In a structure derived from an X-ray crystallography experiment, In a structure derived from an X-ray crystallography experiment,

View File

@ -200,7 +200,7 @@ class NACCESS_atomic(AbstractAtomPropertyMap):
if __name__ == "__main__": if __name__ == "__main__":
import sys import sys
from Bio.PDB import PDBParser from Bio.PDB.PDBParser import PDBParser
p = PDBParser() p = PDBParser()
s = p.get_structure("X", sys.argv[1]) s = p.get_structure("X", sys.argv[1])

View File

@ -484,14 +484,14 @@ class PDBList:
data = json.dumps(body).encode("utf-8") data = json.dumps(body).encode("utf-8")
headers = { headers = {
"Content-Type": "application/json; charset=utf-8", "Content-Type": "application/json; charset=utf-8",
"Content-Length": len(data), "Content-Length": str(len(data)),
} }
request = Request("https://search.rcsb.org/rcsbsearch/v2/query", data, headers) request = Request("https://search.rcsb.org/rcsbsearch/v2/query", data, headers)
with urlopen(request) as response: with urlopen(request) as response:
assemblies = json.loads(response.read().decode("utf-8"))["result_set"] assemblies = json.loads(response.read().decode("utf-8"))["result_set"]
# We transform the assemblies to match the format that they have historically been returned in. # We transform the assemblies to match the format that they have historically been returned in.
def transform(assembly: dict) -> tuple[str, str]: def transform(assembly: dict) -> Tuple[str, str]:
split = assembly["identifier"].split("-") split = assembly["identifier"].split("-")
return split[0].lower(), split[-1] return split[0].lower(), split[-1]

View File

@ -10,6 +10,12 @@
from Bio.PDB.PDBExceptions import PDBConstructionException from Bio.PDB.PDBExceptions import PDBConstructionException
from Bio.PDB.Entity import Entity, DisorderedEntityWrapper from Bio.PDB.Entity import Entity, DisorderedEntityWrapper
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from Bio.PDB.Atom import Atom
from Bio.PDB.Chain import Chain
_atom_name_dict = {} _atom_name_dict = {}
_atom_name_dict["N"] = 1 _atom_name_dict["N"] = 1
@ -18,7 +24,7 @@ _atom_name_dict["C"] = 3
_atom_name_dict["O"] = 4 _atom_name_dict["O"] = 4
class Residue(Entity): class Residue(Entity["Chain", "Atom"]):
"""Represents a residue. A Residue object stores atoms.""" """Represents a residue. A Residue object stores atoms."""
def __init__(self, id, resname, segid): def __init__(self, id, resname, segid):

View File

@ -18,6 +18,7 @@ Reference:
import collections import collections
import math import math
from typing import MutableMapping
import numpy as np import numpy as np
@ -41,7 +42,7 @@ _ENTITY_HIERARCHY = {
# References: # References:
# A. Bondi (1964). "van der Waals Volumes and Radii". # A. Bondi (1964). "van der Waals Volumes and Radii".
# M. Mantina, A.C. et al., J. Phys. Chem. 2009, 113, 5806. # M. Mantina, A.C. et al., J. Phys. Chem. 2009, 113, 5806.
ATOMIC_RADII = collections.defaultdict(lambda: 2.0) ATOMIC_RADII: MutableMapping[str, float] = collections.defaultdict(lambda: 2.0)
ATOMIC_RADII.update( ATOMIC_RADII.update(
{ {
"H": 1.200, "H": 1.200,

View File

@ -9,8 +9,13 @@
from Bio.PDB.Entity import Entity from Bio.PDB.Entity import Entity
from typing import TYPE_CHECKING
class Structure(Entity): if TYPE_CHECKING:
from Bio.PDB.Model import Model
class Structure(Entity[None, "Model"]):
"""The Structure class contains a collection of Model instances.""" """The Structure class contains a collection of Model instances."""
def __init__(self, id): def __init__(self, id):

View File

@ -8,6 +8,7 @@
This is used by the PDBParser and MMCIFparser classes. This is used by the PDBParser and MMCIFparser classes.
""" """
from typing import Optional
import numpy as np import numpy as np
import warnings import warnings
@ -75,7 +76,7 @@ class StructureBuilder:
""" """
self.structure = Structure(structure_id) self.structure = Structure(structure_id)
def init_model(self, model_id: int, serial_num: int = None): def init_model(self, model_id: int, serial_num: Optional[int] = None):
"""Create a new Model object with given id. """Create a new Model object with given id.
Arguments: Arguments:
@ -185,15 +186,15 @@ class StructureBuilder:
def init_atom( def init_atom(
self, self,
name: str, name: str,
coord: np.array, coord: np.ndarray,
b_factor: float, b_factor: float,
occupancy: float, occupancy: float,
altloc: str, altloc: str,
fullname: str, fullname: str,
serial_number=None, serial_number=None,
element: str = None, element: Optional[str] = None,
pqr_charge: float = None, pqr_charge: Optional[float] = None,
radius: float = None, radius: Optional[float] = None,
is_pqr: bool = False, is_pqr: bool = False,
): ):
"""Create a new Atom object. """Create a new Atom object.

View File

@ -388,7 +388,7 @@ Robert T. Miller 2019
""" """
def homog_rot_mtx(angle_rads: float, axis: str) -> np.array: def homog_rot_mtx(angle_rads: float, axis: str) -> np.ndarray:
"""Generate a 4x4 single-axis NumPy rotation matrix. """Generate a 4x4 single-axis NumPy rotation matrix.
:param float angle_rads: the desired rotation angle in radians :param float angle_rads: the desired rotation angle in radians
@ -459,7 +459,7 @@ def set_X_homog_rot_mtx(angle_rads: float, mtx: np.ndarray):
mtx[1][2] = -sinang mtx[1][2] = -sinang
def homog_trans_mtx(x: float, y: float, z: float) -> np.array: def homog_trans_mtx(x: float, y: float, z: float) -> np.ndarray:
"""Generate a 4x4 NumPy translation matrix. """Generate a 4x4 NumPy translation matrix.
:param x, y, z: translation in each axis :param x, y, z: translation in each axis
@ -477,7 +477,7 @@ def set_homog_trans_mtx(x: float, y: float, z: float, mtx: np.ndarray):
mtx[2][3] = z mtx[2][3] = z
def homog_scale_mtx(scale: float) -> np.array: def homog_scale_mtx(scale: float) -> np.ndarray:
"""Generate a 4x4 NumPy scaling matrix. """Generate a 4x4 NumPy scaling matrix.
:param float scale: scale multiplier :param float scale: scale multiplier
@ -502,17 +502,17 @@ def _get_azimuth(x: float, y: float) -> float:
) )
def get_spherical_coordinates(xyz: np.array) -> Tuple[float, float, float]: def get_spherical_coordinates(xyz: np.ndarray) -> Tuple[float, float, float]:
"""Compute spherical coordinates (r, azimuth, polar_angle) for X,Y,Z point. """Compute spherical coordinates (r, azimuth, polar_angle) for X,Y,Z point.
:param array xyz: column vector (3 row x 1 column NumPy array) :param array xyz: column vector (3 row x 1 column NumPy array)
:return: tuple of r, azimuth, polar_angle for input coordinate :return: tuple of r, azimuth, polar_angle for input coordinate
""" """
r = np.linalg.norm(xyz) r = float(np.linalg.norm(xyz))
if 0 == r: if 0 == r:
return (0, 0, 0) return (0, 0, 0)
azimuth = _get_azimuth(xyz[0], xyz[1]) azimuth = _get_azimuth(xyz[0], xyz[1])
polar_angle = np.arccos(xyz[2] / r) polar_angle: float = np.arccos(xyz[2] / r)
return (r, azimuth, polar_angle) return (r, azimuth, polar_angle)