helmkit 0.7.6__tar.gz → 0.7.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: helmkit
3
- Version: 0.7.6
3
+ Version: 0.7.7
4
4
  Summary: Parse HELM strings into RDKit molecules
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "helmkit"
3
- version = "0.7.6"
3
+ version = "0.7.7"
4
4
  description = "Parse HELM strings into RDKit molecules"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -47,6 +47,11 @@ ignore = ["I001", "N999"]
47
47
  [tool.ruff.format]
48
48
  skip-magic-trailing-comma = true
49
49
 
50
+ [tool.ty.rules]
51
+ all = "error"
52
+ unsound-return-statement = "ignore"
53
+ unsound-assignment = "ignore"
54
+
50
55
  [tool.ty.src]
51
56
  include = ["src"]
52
57
  exclude = ["tests"]
@@ -4,9 +4,13 @@ import re
4
4
  import warnings
5
5
  from collections import defaultdict
6
6
  from collections.abc import Callable
7
+ from collections.abc import Iterable
7
8
  from collections.abc import Sequence
8
9
  from functools import lru_cache
9
10
  from importlib.resources import files
11
+ from typing import assert_never
12
+ from typing import cast
13
+ from typing import Literal
10
14
  from typing import overload
11
15
  from typing import TypedDict
12
16
  from typing import TypeVar
@@ -21,9 +25,7 @@ MAX_RGROUPS = 4
21
25
  def get_molecule_property(
22
26
  molecule: Chem.Mol, property_name: str, default: str | None = None
23
27
  ) -> str | None:
24
- return (
25
- molecule.GetProp(property_name) if molecule.HasProp(property_name) else default
26
- )
28
+ return molecule.GetProp(property_name, default=default)
27
29
 
28
30
 
29
31
  T = TypeVar("T")
@@ -68,14 +70,14 @@ def infer_attachment_points(
68
70
  continue
69
71
 
70
72
  atom = molecule.GetAtomWithIdx(r_idx)
73
+ bonds: tuple[Chem.Bond, ...] = atom.GetBonds()
71
74
 
72
- for bond in atom.GetBonds():
75
+ for bond in bonds:
73
76
  other_idx = bond.GetOtherAtomIdx(r_idx)
74
77
  attachment_points.append(other_idx)
75
78
  break
76
79
  else:
77
- attachment_points.append(None)
78
- warnings.warn(
80
+ raise ValueError(
79
81
  f"R-group atom {r_idx} has no bonds to determine attachment point"
80
82
  )
81
83
 
@@ -102,12 +104,13 @@ def load_monomer_library(library_path: str | None = None) -> MonomerLibrary:
102
104
  monomers_dict: MonomerLibrary = defaultdict(dict)
103
105
  supplier = Chem.SDMolSupplier(library_path, removeHs=False)
104
106
 
105
- for mol in supplier:
107
+ for mol in cast(Iterable[Chem.Mol | None], supplier):
106
108
  if mol is None:
107
109
  continue
108
110
 
109
111
  symbol = get_molecule_property(mol, "symbol")
110
112
  if not symbol:
113
+ warnings.warn("Monomer without a symbol property will be skipped")
111
114
  continue
112
115
 
113
116
  m_type = get_molecule_property(mol, "m_type", "")
@@ -121,9 +124,9 @@ def load_monomer_library(library_path: str | None = None) -> MonomerLibrary:
121
124
  rgroup_idx = parse_comma_separated_property(mol, "m_RgroupIdx", int)
122
125
  attachment_point_idx = infer_attachment_points(mol, rgroup_idx)
123
126
 
124
- abbr = get_molecule_property(mol, "m_abbr", "")
125
- if not abbr:
126
- continue
127
+ # m_abbr is only used for display, so fall back to the symbol when a
128
+ # library does not provide it instead of dropping the monomer.
129
+ abbr = get_molecule_property(mol, "m_abbr") or symbol
127
130
 
128
131
  monomers_dict[m_type][symbol] = {
129
132
  "m_romol": mol,
@@ -294,10 +297,10 @@ class Molecule:
294
297
  def _split_helm_sections(
295
298
  self, helm: str
296
299
  ) -> tuple[list[str], list[str], list[str], str, str]:
297
- parts = self._dollar_outside_brackets.split(helm, 4)
300
+ parts: list[str] = self._dollar_outside_brackets.split(helm, 4)
298
301
  parts.extend([""] * (5 - len(parts)))
299
302
 
300
- polymers = (
303
+ polymers: list[str] = (
301
304
  self._pipe_outside_brackets.split(parts[0])
302
305
  if "|" in parts[0]
303
306
  else [parts[0]]
@@ -332,17 +335,18 @@ class Molecule:
332
335
 
333
336
  return result
334
337
 
335
- def _extract_chain_id(self, chain_str: str) -> tuple[str, str]:
338
+ @staticmethod
339
+ def _extract_polymer_type(chain_str: str) -> Literal["PEPTIDE", "RNA", "CHEM"]:
336
340
  """Extract chain ID and return (chain_id, polymer_type)."""
337
341
  match = re.fullmatch(r"([A-Z]+)(\d+)", chain_str)
338
342
  if not match:
339
343
  raise ValueError(f"Invalid chain format: {chain_str}")
340
344
 
341
- polymer_type = match.group(1)
345
+ polymer_type: str = match.group(1)
342
346
  if polymer_type not in {"PEPTIDE", "RNA", "CHEM"}:
343
347
  raise ValueError(f"Unsupported polymer type: {polymer_type}")
344
348
 
345
- return chain_str, polymer_type
349
+ return polymer_type
346
350
 
347
351
  def _process_monomer(
348
352
  self, monomer_name: str, chain_id: str, residue_idx: int, polymer_type: str
@@ -353,7 +357,7 @@ class Molecule:
353
357
  if monomer_name.startswith("[") and monomer_name.endswith("]")
354
358
  else monomer_name
355
359
  )
356
- if monomer_name == "":
360
+ if not monomer_name:
357
361
  raise ValueError(f"Monomer {residue_idx + 1} has no name. Check HELM.")
358
362
 
359
363
  # Check for (a,[b]) pattern
@@ -426,8 +430,8 @@ class Molecule:
426
430
  warnings.warn(f"No sequence in polymer: {chain}")
427
431
  continue
428
432
 
429
- id_chain = chain[: match.start()]
430
- chain_id, polymer_type = self._extract_chain_id(id_chain)
433
+ chain_id = chain[: match.start()]
434
+ polymer_type = self._extract_polymer_type(chain_id)
431
435
 
432
436
  if chain_id in self.chain_offset:
433
437
  raise ValueError(f"Duplicate chain ID: {chain_id}")
@@ -445,8 +449,6 @@ class Molecule:
445
449
  monomer = self._process_monomer(
446
450
  monomer_name, chain_id, residue_idx, polymer_type
447
451
  )
448
- if not monomer:
449
- continue
450
452
 
451
453
  self.monomers.append(monomer)
452
454
  self.residue_reps[chain_id].append(monomer_idx)
@@ -550,9 +552,12 @@ class Molecule:
550
552
  self.monomers.append(monomer)
551
553
  self.residue_reps[chain_id].append(monomer_idx)
552
554
  monomer_idx += 1
555
+ else:
556
+ assert_never(polymer_type)
553
557
 
558
+ @staticmethod
554
559
  def _parse_connection(
555
- self, connection_str: str
560
+ connection_str: str,
556
561
  ) -> tuple[str, int, int, str, int, int] | None:
557
562
  """Parse a single connection string."""
558
563
  parts = connection_str.split(",")
@@ -661,7 +666,7 @@ class Molecule:
661
666
  rgroup_idx = monomer["m_RgroupIdx"]
662
667
  for i in range(min(len(rgroups), MAX_RGROUPS)):
663
668
  if rgroups[i] is not None:
664
- self._replace_rgroup(self._mol, 0, rgroup_idx[i], rgroups[i])
669
+ self._replace_rgroup(0, rgroup_idx[i], rgroups[i])
665
670
 
666
671
  current_offset = self._mol.GetNumAtoms()
667
672
  self.offset = [0, current_offset]
@@ -673,9 +678,7 @@ class Molecule:
673
678
  rgroup_idx = monomer["m_RgroupIdx"]
674
679
  for i in range(min(len(rgroups), MAX_RGROUPS)):
675
680
  if rgroups[i] is not None:
676
- self._replace_rgroup(
677
- self._mol, current_offset, rgroup_idx[i], rgroups[i]
678
- )
681
+ self._replace_rgroup(current_offset, rgroup_idx[i], rgroups[i])
679
682
 
680
683
  atom_count = monomer["m_romol"].GetNumAtoms()
681
684
  current_offset += atom_count
@@ -694,10 +697,9 @@ class Molecule:
694
697
  absolute_atom1_idx, absolute_atom2_idx, Chem.BondType.SINGLE
695
698
  )
696
699
 
697
- def _replace_rgroup(
698
- self, rdkit_mol: Chem.RWMol, atom_offset: int, atom_idx: int, atom_type: str
699
- ) -> None:
700
+ def _replace_rgroup(self, atom_offset: int, atom_idx: int, atom_type: str) -> None:
700
701
  """Replace an R-group with the appropriate atom type."""
702
+ rdkit_mol = self.mol
701
703
  absolute_idx = atom_offset + atom_idx
702
704
 
703
705
  if atom_type == "OH":