MoleditPy-linux 3.0.6__tar.gz → 3.2.0__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.
Files changed (80) hide show
  1. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/PKG-INFO +1 -1
  2. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/pyproject.toml +1 -1
  3. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
  4. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -1
  5. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/__main__.py +2 -2
  6. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/core/mol_geometry.py +26 -17
  7. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/core/molecular_data.py +20 -13
  8. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/main.py +4 -3
  9. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/plugins/plugin_interface.py +31 -24
  10. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/plugins/plugin_manager.py +29 -17
  11. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/plugins/plugin_manager_window.py +15 -9
  12. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/__init__.py +1 -1
  13. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/about_dialog.py +15 -7
  14. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/align_plane_dialog.py +28 -12
  15. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/alignment_dialog.py +30 -24
  16. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/analysis_window.py +14 -4
  17. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/angle_dialog.py +37 -19
  18. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/app_state.py +34 -26
  19. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/atom_item.py +3 -1
  20. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/base_picking_dialog.py +33 -16
  21. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/bond_item.py +2 -2
  22. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/bond_length_dialog.py +30 -13
  23. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/calculation_worker.py +78 -46
  24. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/color_settings_dialog.py +13 -10
  25. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/compute_logic.py +17 -18
  26. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +74 -62
  27. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/custom_interactor_style.py +23 -14
  28. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/custom_qt_interactor.py +11 -5
  29. moleditpy_linux-3.2.0/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +250 -0
  30. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/dialog_logic.py +27 -27
  31. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/dihedral_dialog.py +49 -23
  32. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/edit_3d_logic.py +72 -55
  33. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/edit_actions_logic.py +39 -30
  34. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/export_logic.py +2 -2
  35. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/geometry_base_dialog.py +30 -8
  36. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/io_logic.py +30 -32
  37. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/main_window.py +27 -10
  38. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/main_window_init.py +138 -103
  39. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/mirror_dialog.py +11 -7
  40. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/molecular_scene_handler.py +31 -21
  41. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/molecule_scene.py +25 -21
  42. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/move_group_dialog.py +129 -74
  43. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/periodic_table_dialog.py +4 -2
  44. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/planarize_dialog.py +26 -9
  45. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/settings_dialog.py +9 -8
  46. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +15 -9
  47. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +27 -15
  48. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +20 -7
  49. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +15 -8
  50. moleditpy_linux-3.2.0/src/moleditpy_linux/ui/string_importers.py +212 -0
  51. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/template_preview_item.py +28 -12
  52. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/template_preview_view.py +12 -7
  53. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/translation_dialog.py +33 -23
  54. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/ui_manager.py +44 -30
  55. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/user_template_dialog.py +30 -30
  56. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/view_3d_logic.py +83 -42
  57. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/zoomable_view.py +24 -11
  58. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/utils/constants.py +1 -1
  59. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +2 -2
  60. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/utils/system_utils.py +6 -3
  61. moleditpy_linux-3.0.6/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -252
  62. moleditpy_linux-3.0.6/src/moleditpy_linux/ui/sip_isdeleted_safe.py +0 -35
  63. moleditpy_linux-3.0.6/src/moleditpy_linux/ui/string_importers.py +0 -257
  64. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/LICENSE +0 -0
  65. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/README.md +0 -0
  66. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/setup.cfg +0 -0
  67. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
  68. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
  69. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
  70. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
  71. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/__init__.py +0 -0
  72. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/assets/file_icon.ico +0 -0
  73. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/assets/icon.icns +0 -0
  74. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/assets/icon.ico +0 -0
  75. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/assets/icon.png +0 -0
  76. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/core/__init__.py +0 -0
  77. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/plugins/__init__.py +0 -0
  78. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
  79. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/utils/__init__.py +0 -0
  80. {moleditpy_linux-3.0.6 → moleditpy_linux-3.2.0}/src/moleditpy_linux/utils/default_settings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 3.0.6
3
+ Version: 3.2.0
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "MoleditPy-linux"
7
7
 
8
- version = "3.0.6"
8
+ version = "3.2.0"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy-linux
3
- Version: 3.0.6
3
+ Version: 3.2.0
4
4
  Summary: A cross-platform, simple, and intuitive molecular structure editor built in Python. It allows 2D molecular drawing and 3D structure visualization. It supports exporting structure files for input to DFT calculation software.
5
5
  Author-email: HiroYokoyama <titech.yoko.hiro@gmail.com>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -55,7 +55,6 @@ src/moleditpy_linux/ui/move_group_dialog.py
55
55
  src/moleditpy_linux/ui/periodic_table_dialog.py
56
56
  src/moleditpy_linux/ui/planarize_dialog.py
57
57
  src/moleditpy_linux/ui/settings_dialog.py
58
- src/moleditpy_linux/ui/sip_isdeleted_safe.py
59
58
  src/moleditpy_linux/ui/string_importers.py
60
59
  src/moleditpy_linux/ui/template_preview_item.py
61
60
  src/moleditpy_linux/ui/template_preview_view.py
@@ -15,9 +15,9 @@ print("MoleditPy - A Python-based molecular editing software")
15
15
  print("-----------------------------------------------------\n")
16
16
 
17
17
  try:
18
- from .main import main
18
+ from .main import main # type: ignore
19
19
  except ImportError:
20
- from main import main
20
+ from main import main # type: ignore
21
21
 
22
22
  # --- Application Execution ---
23
23
  if __name__ == "__main__":
@@ -152,7 +152,7 @@ def rodrigues_rotate(v: np.ndarray, axis: np.ndarray, angle: float) -> np.ndarra
152
152
  """
153
153
  cos_a = np.cos(angle)
154
154
  sin_a = np.sin(angle)
155
- return v * cos_a + np.cross(axis, v) * sin_a + axis * np.dot(axis, v) * (1 - cos_a)
155
+ return v * cos_a + np.cross(axis, v) * sin_a + axis * np.dot(axis, v) * (1 - cos_a) # type: ignore[no-any-return]
156
156
 
157
157
 
158
158
  def adjust_bond_angle(
@@ -421,7 +421,9 @@ def is_problematic_valence(
421
421
  # ------------------------------------------------------------------
422
422
 
423
423
 
424
- def inject_ez_stereo_to_mol_block(mol_block, rdkit_mol, bonds_data):
424
+ def inject_ez_stereo_to_mol_block(
425
+ mol_block: str, rdkit_mol: Any, bonds_data: Dict[Tuple[int, int], Any]
426
+ ) -> str:
425
427
  """Generate a modified MOL block with 'M CFG' lines for E/Z stereochemistry.
426
428
 
427
429
  Parameters
@@ -502,7 +504,7 @@ def identify_valence_problems(
502
504
  problem_atom_ids = []
503
505
 
504
506
  # Pre-calculate bond orders per atom
505
- bond_orders = {}
507
+ bond_orders: Dict[int, int] = {}
506
508
  for (id1, id2), bond in bonds_data.items():
507
509
  order = bond.get("order", 1)
508
510
  bond_orders[id1] = bond_orders.get(id1, 0) + order
@@ -534,15 +536,22 @@ def optimize_2d_coords(mol: Any) -> Dict[int, Tuple[float, float]]:
534
536
  return new_positions
535
537
 
536
538
 
537
- def calculate_best_fit_plane_projection(centered_positions, normal, centroid):
539
+ def calculate_best_fit_plane_projection(
540
+ centered_positions: np.ndarray, normal: np.ndarray, centroid: np.ndarray
541
+ ) -> np.ndarray:
538
542
  """Project centered points orthogonally onto the plane defined by normal and centroid."""
539
543
  projections = centered_positions - np.outer(
540
544
  np.dot(centered_positions, normal), normal
541
545
  )
542
- return projections + centroid
546
+ return projections + centroid # type: ignore[no-any-return]
543
547
 
544
548
 
545
- def rotate_2d_points(points_map, center_x, center_y, angle_degrees):
549
+ def rotate_2d_points(
550
+ points_map: Dict[int, Tuple[float, float]],
551
+ center_x: float,
552
+ center_y: float,
553
+ angle_degrees: float,
554
+ ) -> Dict[int, Tuple[float, float]]:
546
555
  """Rotate 2D points (atom_id -> (x, y)) around a center."""
547
556
  rad = math.radians(angle_degrees)
548
557
  cos_a = math.cos(rad)
@@ -558,13 +567,13 @@ def rotate_2d_points(points_map, center_x, center_y, angle_degrees):
558
567
 
559
568
 
560
569
  def resolve_2d_overlaps(
561
- atom_ids,
562
- positions_map,
563
- adjacency_list,
564
- overlap_threshold=0.5,
565
- move_distance=20,
566
- has_bond_check_func=None,
567
- ):
570
+ atom_ids: Iterable[int],
571
+ positions_map: Dict[int, Tuple[float, float]],
572
+ adjacency_list: Dict[int, List[int]],
573
+ overlap_threshold: float = 0.5,
574
+ move_distance: float = 20,
575
+ has_bond_check_func: Optional[Any] = None,
576
+ ) -> List[Tuple[Set[int], Tuple[float, float]]]:
568
577
  """Detect and resolve overlapping atom groups in 2D.
569
578
 
570
579
  Returns list of (atom_ids_set, translation_vector_tuple).
@@ -592,13 +601,13 @@ def resolve_2d_overlaps(
592
601
  # Union-Find for overlap groups
593
602
  parent = {aid: aid for aid in atom_ids}
594
603
 
595
- def find_set(aid):
604
+ def find_set(aid: Any) -> Any:
596
605
  if parent[aid] == aid:
597
606
  return aid
598
607
  parent[aid] = find_set(parent[aid])
599
608
  return parent[aid]
600
609
 
601
- def unite_sets(aid1, aid2):
610
+ def unite_sets(aid1: Any, aid2: Any) -> None:
602
611
  root1 = find_set(aid1)
603
612
  root2 = find_set(aid2)
604
613
  if root1 != root2:
@@ -607,7 +616,7 @@ def resolve_2d_overlaps(
607
616
  for id1, id2 in overlapping_pairs:
608
617
  unite_sets(id1, id2)
609
618
 
610
- groups_by_root = {}
619
+ groups_by_root: Dict[int, List[int]] = {}
611
620
  for aid in atom_ids:
612
621
  root = find_set(aid)
613
622
  groups_by_root.setdefault(root, []).append(aid)
@@ -649,7 +658,7 @@ def resolve_2d_overlaps(
649
658
  rep_id1, rep_id2 = i1, i2
650
659
  break
651
660
 
652
- if rep_id1 is None:
661
+ if rep_id1 is None or rep_id2 is None:
653
662
  continue
654
663
 
655
664
  frag1 = next((f for f in fragments if rep_id1 in f), None)
@@ -25,10 +25,10 @@ class PointTuple(tuple):
25
25
  """Backward-compatible tuple that allows .x() and .y() access like QPointF."""
26
26
 
27
27
  def x(self) -> float:
28
- return self[0]
28
+ return float(self[0])
29
29
 
30
30
  def y(self) -> float:
31
- return self[1]
31
+ return float(self[1])
32
32
 
33
33
 
34
34
  class MolecularData:
@@ -248,10 +248,10 @@ class MolecularData:
248
248
  if nbr.GetIdx() == exclude_idx:
249
249
  continue
250
250
  if nbr.GetAtomicNum() > 1:
251
- return nbr.GetIdx()
251
+ return int(nbr.GetIdx())
252
252
  for nbr in atom.GetNeighbors():
253
253
  if nbr.GetIdx() != exclude_idx:
254
- return nbr.GetIdx()
254
+ return int(nbr.GetIdx())
255
255
  return None
256
256
 
257
257
  # Overwrite based on labels (E/Z has highest priority) ---
@@ -275,7 +275,7 @@ class MolecularData:
275
275
  begin_atom_idx = bond.GetBeginAtomIdx()
276
276
  end_atom_idx = bond.GetEndAtomIdx()
277
277
 
278
- bond_data = info.get("bond_data", {}) or {}
278
+ bond_data: dict[str, Any] = info.get("bond_data") or {} # type: ignore[assignment]
279
279
  stereo_atoms_specified = bond_data.get("stereo_atoms")
280
280
 
281
281
  if stereo_atoms_specified:
@@ -365,7 +365,10 @@ class MolecularData:
365
365
  if key in self.bonds:
366
366
  rdkit_bond_idx_to_item[bidx] = self.bonds[key].get("item")
367
367
 
368
- # 5. Initialize/Reset all bond items
368
+ # 5. Initialize/Reset all bond items and track best ring size
369
+ bond_to_best_size: Dict[
370
+ int, int
371
+ ] = {} # bond_item_id -> smallest_ring_size_found
369
372
  for bond_data in self.bonds.values():
370
373
  bond_item = bond_data.get("item")
371
374
  if bond_item:
@@ -374,6 +377,7 @@ class MolecularData:
374
377
 
375
378
  # 6. Apply ring information
376
379
  for a_ring, b_ring in zip(atom_rings, bond_rings):
380
+ ring_size = len(a_ring)
377
381
  # Calculate ring center (geometric mean of atom positions)
378
382
  positions = []
379
383
  for aidx in a_ring:
@@ -395,18 +399,21 @@ class MolecularData:
395
399
  bond_item = rdkit_bond_idx_to_item.get(bidx)
396
400
  if bond_item:
397
401
  bond_item.is_in_ring = True
398
- # Note: Simplified; a bond might be part of multiple rings.
399
- # The inner-bond logic usually picks the smallest ring.
400
- # Since we iterate through all rings, the last one wins.
401
- # RDKit's AtomRings returns SSSR (Smallest Set of Smallest Rings),
402
- # so this is usually correct for 2D drawing.
403
- bond_item.ring_center = ring_center
402
+ # Explicitly prioritize smaller rings for double bond shift logic.
403
+ # This ensures the double bond is drawn inside the smaller ring in fused systems.
404
+ item_id = id(bond_item)
405
+ if (
406
+ item_id not in bond_to_best_size
407
+ or ring_size < bond_to_best_size[item_id]
408
+ ):
409
+ bond_item.ring_center = ring_center
410
+ bond_to_best_size[item_id] = ring_size
404
411
 
405
412
  def to_mol_block(self) -> Optional[str]:
406
413
  mol = self.to_rdkit_mol()
407
414
  if mol:
408
415
  try:
409
- return Chem.MolToMolBlock(mol, includeStereo=True)
416
+ return Chem.MolToMolBlock(mol, includeStereo=True) # type: ignore[no-any-return]
410
417
  except (RuntimeError, ValueError, TypeError) as e:
411
418
  logging.warning(
412
419
  f"RDKit MolBlock generation failed: {e}"
@@ -15,6 +15,7 @@ import sys
15
15
  import argparse
16
16
  import logging
17
17
  import os
18
+ from typing import Any
18
19
 
19
20
  try:
20
21
  from .utils.constants import VERSION
@@ -35,7 +36,7 @@ except ImportError:
35
36
  from moleditpy_linux.ui.main_window import MainWindow
36
37
 
37
38
 
38
- def setup_logging():
39
+ def setup_logging() -> None:
39
40
  logging.basicConfig(
40
41
  level=logging.INFO,
41
42
  format="%(asctime)s [%(levelname)s] %(name)s (%(pathname)s:%(lineno)d): %(message)s",
@@ -43,7 +44,7 @@ def setup_logging():
43
44
  force=True,
44
45
  )
45
46
 
46
- def handle_exception(exc_type, exc_value, exc_traceback):
47
+ def handle_exception(exc_type: Any, exc_value: Any, exc_traceback: Any) -> None:
47
48
  """Log unhandled exceptions using the configured logging system."""
48
49
  if issubclass(exc_type, KeyboardInterrupt):
49
50
  # Allow keyboard interrupt to exit normally
@@ -57,7 +58,7 @@ def setup_logging():
57
58
  sys.excepthook = handle_exception
58
59
 
59
60
 
60
- def main():
61
+ def main() -> None:
61
62
  # Setup logging as early as possible
62
63
  setup_logging()
63
64
 
@@ -19,7 +19,7 @@ class PluginContext:
19
19
  It is passed to the `initialize(context)` function of the plugin.
20
20
  """
21
21
 
22
- def __init__(self, manager, plugin_name: str):
22
+ def __init__(self, manager: Any, plugin_name: str) -> None:
23
23
  self._manager = manager
24
24
  self._plugin_name = plugin_name
25
25
 
@@ -30,7 +30,7 @@ class PluginContext:
30
30
  text: Optional[str] = None,
31
31
  icon: Optional[str] = None,
32
32
  shortcut: Optional[str] = None,
33
- ):
33
+ ) -> None:
34
34
  """
35
35
  Register a menu action.
36
36
 
@@ -52,7 +52,7 @@ class PluginContext:
52
52
  callback: Optional[Callable] = None,
53
53
  icon: Optional[str] = None,
54
54
  shortcut: Optional[str] = None,
55
- ):
55
+ ) -> None:
56
56
  """Backward-compatible alias for add_menu_action.
57
57
  Supports old 3-arg style: register_menu_action(path, text, callback).
58
58
  """
@@ -61,7 +61,8 @@ class PluginContext:
61
61
  self.add_menu_action(path, text_or_callback, None, icon, shortcut)
62
62
  else:
63
63
  # Old style: (path, text, callback)
64
- self.add_menu_action(path, callback, text_or_callback, icon, shortcut)
64
+ if callback is not None:
65
+ self.add_menu_action(path, callback, text_or_callback, icon, shortcut)
65
66
 
66
67
  def add_plugin_menu(
67
68
  self,
@@ -70,7 +71,7 @@ class PluginContext:
70
71
  text: Optional[str] = None,
71
72
  icon: Optional[str] = None,
72
73
  shortcut: Optional[str] = None,
73
- ):
74
+ ) -> None:
74
75
  """
75
76
  Register an action nested inside the Plugin menu.
76
77
 
@@ -95,7 +96,7 @@ class PluginContext:
95
96
  text: str,
96
97
  icon: Optional[str] = None,
97
98
  tooltip: Optional[str] = None,
98
- ):
99
+ ) -> None:
99
100
  """
100
101
  Register a toolbar action.
101
102
  """
@@ -103,7 +104,9 @@ class PluginContext:
103
104
  self._plugin_name, callback, text, icon, tooltip
104
105
  )
105
106
 
106
- def register_drop_handler(self, callback: Callable[[str], bool], priority: int = 0):
107
+ def register_drop_handler(
108
+ self, callback: Callable[[str], bool], priority: int = 0
109
+ ) -> None:
107
110
  """
108
111
  Register a handler for file drops.
109
112
 
@@ -132,7 +135,7 @@ class PluginContext:
132
135
  Returns a list of RDKit atom indices currently selected in the 2D or 3D view.
133
136
  Note: RDKit indices are returned, which map to the current_mol.
134
137
  """
135
- return self._manager.get_selected_atom_indices()
138
+ return self._manager.get_selected_atom_indices() # type: ignore[no-any-return]
136
139
 
137
140
  def register_window(self, window_id: str, window: Any) -> None:
138
141
  """
@@ -174,7 +177,7 @@ class PluginContext:
174
177
  )
175
178
 
176
179
  @current_mol.setter
177
- def current_mol(self, mol: Any):
180
+ def current_mol(self, mol: Any) -> None:
178
181
  mw = self.get_main_window()
179
182
  if mw and hasattr(mw, "view_3d_manager"):
180
183
  mw.view_3d_manager.current_mol = mol
@@ -186,7 +189,7 @@ class PluginContext:
186
189
  return self.current_mol
187
190
 
188
191
  @current_molecule.setter
189
- def current_molecule(self, mol: Any):
192
+ def current_molecule(self, mol: Any) -> None:
190
193
  self.current_mol = mol
191
194
 
192
195
  @property
@@ -236,7 +239,7 @@ class PluginContext:
236
239
  if mw and hasattr(mw, "view_3d_manager") and mw.view_3d_manager.plotter:
237
240
  mw.view_3d_manager.plotter.reset_camera()
238
241
 
239
- def add_export_action(self, label: str, callback: Callable):
242
+ def add_export_action(self, label: str, callback: Callable) -> None:
240
243
  """
241
244
  Register a custom export action.
242
245
 
@@ -248,7 +251,7 @@ class PluginContext:
248
251
 
249
252
  def register_optimization_method(
250
253
  self, method_name: str, callback: Callable[[Any], bool]
251
- ):
254
+ ) -> None:
252
255
  """
253
256
  Register a custom 3D optimization method.
254
257
 
@@ -263,7 +266,7 @@ class PluginContext:
263
266
 
264
267
  def register_file_opener(
265
268
  self, extension: str, callback: Callable[[str], None], priority: int = 0
266
- ):
269
+ ) -> None:
267
270
  """
268
271
  Register a handler for opening a specific file extension.
269
272
 
@@ -277,7 +280,7 @@ class PluginContext:
277
280
  self._plugin_name, extension, callback, priority
278
281
  )
279
282
 
280
- def add_analysis_tool(self, label: str, callback: Callable):
283
+ def add_analysis_tool(self, label: str, callback: Callable) -> None:
281
284
  """
282
285
  Register a tool in the Analysis menu.
283
286
 
@@ -287,7 +290,7 @@ class PluginContext:
287
290
  """
288
291
  self._manager.register_analysis_tool(self._plugin_name, label, callback)
289
292
 
290
- def register_save_handler(self, callback: Callable[[], dict]):
293
+ def register_save_handler(self, callback: Callable[[], dict]) -> None:
291
294
  """
292
295
  Register a callback to save state into the project file.
293
296
 
@@ -296,7 +299,7 @@ class PluginContext:
296
299
  """
297
300
  self._manager.register_save_handler(self._plugin_name, callback)
298
301
 
299
- def register_load_handler(self, callback: Callable[[dict], None]):
302
+ def register_load_handler(self, callback: Callable[[dict], None]) -> None:
300
303
  """
301
304
  Register a callback to restore state from the project file.
302
305
 
@@ -305,13 +308,15 @@ class PluginContext:
305
308
  """
306
309
  self._manager.register_load_handler(self._plugin_name, callback)
307
310
 
308
- def register_3d_context_menu(self, callback: Callable, label: str):
311
+ def register_3d_context_menu(self, callback: Callable, label: str) -> None:
309
312
  """Deprecated: This method does nothing. Kept for backward compatibility."""
310
313
  print(
311
314
  f"Warning: Plugin '{self._plugin_name}' uses deprecated 'register_3d_context_menu'. This API has been removed."
312
315
  )
313
316
 
314
- def register_3d_style(self, style_name: str, callback: Callable[[Any, Any], None]):
317
+ def register_3d_style(
318
+ self, style_name: str, callback: Callable[[Any, Any], None]
319
+ ) -> None:
315
320
  """
316
321
  Register a custom 3D rendering style.
317
322
 
@@ -322,7 +327,7 @@ class PluginContext:
322
327
  """
323
328
  self._manager.register_3d_style(self._plugin_name, style_name, callback)
324
329
 
325
- def register_document_reset_handler(self, callback: Callable[[], None]):
330
+ def register_document_reset_handler(self, callback: Callable[[], None]) -> None:
326
331
  """
327
332
  Register a callback to be called when a new document is created (File→New).
328
333
 
@@ -370,14 +375,14 @@ class PluginContext:
370
375
  class Plugin3DController:
371
376
  """Helper to manipulate the 3D scene."""
372
377
 
373
- def __init__(self, main_window):
378
+ def __init__(self, main_window: Any) -> None:
374
379
  self._mw = main_window
375
380
 
376
- def _get_v3d(self):
381
+ def _get_v3d(self) -> Optional[Any]:
377
382
  """Helper to get the 3D manager."""
378
383
  return getattr(self._mw, "view_3d_manager", None)
379
384
 
380
- def set_atom_color(self, atom_index: int, color_hex: str):
385
+ def set_atom_color(self, atom_index: int, color_hex: str) -> None:
381
386
  """
382
387
  Set the color of a specific atom in the 3D view.
383
388
  Args:
@@ -390,7 +395,7 @@ class Plugin3DController:
390
395
  if hasattr(self._mw, "plotter") and self._mw.plotter:
391
396
  self._mw.plotter.render()
392
397
 
393
- def set_bond_color(self, bond_index: int, color_hex: str):
398
+ def set_bond_color(self, bond_index: int, color_hex: str) -> None:
394
399
  """
395
400
  Set the color of a specific bond in the 3D view.
396
401
 
@@ -404,7 +409,9 @@ class Plugin3DController:
404
409
  if hasattr(self._mw, "plotter") and self._mw.plotter:
405
410
  self._mw.plotter.render()
406
411
 
407
- def set_bond_color_by_atoms(self, atom_idx1: int, atom_idx2: int, color_hex: str):
412
+ def set_bond_color_by_atoms(
413
+ self, atom_idx1: int, atom_idx2: int, color_hex: str
414
+ ) -> None:
408
415
  """
409
416
  Set the color of the bond between two atoms.
410
417
 
@@ -315,7 +315,7 @@ class PluginManager:
315
315
  spec = importlib.util.spec_from_file_location(
316
316
  unique_module_name,
317
317
  filepath,
318
- submodule_search_locations=([pkg_dir] if is_package else None),
318
+ submodule_search_locations=([pkg_dir] if is_package else None), # type: ignore[list-item]
319
319
  )
320
320
  if spec and spec.loader:
321
321
  module = importlib.util.module_from_spec(spec)
@@ -482,12 +482,16 @@ class PluginManager:
482
482
  # Sort by priority desc
483
483
  self.drop_handlers.sort(key=lambda x: x["priority"], reverse=True)
484
484
 
485
- def register_export_action(self, plugin_name, label, callback):
485
+ def register_export_action(
486
+ self, plugin_name: str, label: str, callback: Callable
487
+ ) -> None:
486
488
  self.export_actions.append(
487
489
  {"plugin": plugin_name, "label": label, "callback": callback}
488
490
  )
489
491
 
490
- def register_optimization_method(self, plugin_name, method_name, callback):
492
+ def register_optimization_method(
493
+ self, plugin_name: str, method_name: str, callback: Callable
494
+ ) -> None:
491
495
  # Key by upper-case method name for consistency
492
496
  self.optimization_methods[method_name.upper()] = {
493
497
  "plugin": plugin_name,
@@ -495,7 +499,9 @@ class PluginManager:
495
499
  "label": method_name,
496
500
  }
497
501
 
498
- def register_file_opener(self, plugin_name, extension, callback, priority=0):
502
+ def register_file_opener(
503
+ self, plugin_name: str, extension: str, callback: Callable, priority: int = 0
504
+ ) -> None:
499
505
  # Normalize extension to lowercase
500
506
  ext = extension.lower()
501
507
  if not ext.startswith("."):
@@ -512,25 +518,31 @@ class PluginManager:
512
518
  self.file_openers[ext].sort(key=lambda x: x["priority"], reverse=True)
513
519
 
514
520
  # Analysis Tools registration
515
- def register_analysis_tool(self, plugin_name, label, callback):
521
+ def register_analysis_tool(
522
+ self, plugin_name: str, label: str, callback: Callable
523
+ ) -> None:
516
524
  self.analysis_tools.append(
517
525
  {"plugin": plugin_name, "label": label, "callback": callback}
518
526
  )
519
527
 
520
528
  # State Persistence registration
521
- def register_save_handler(self, plugin_name, callback):
529
+ def register_save_handler(self, plugin_name: str, callback: Callable) -> None:
522
530
  self.save_handlers[plugin_name] = callback
523
531
 
524
- def register_load_handler(self, plugin_name, callback):
532
+ def register_load_handler(self, plugin_name: str, callback: Callable) -> None:
525
533
  self.load_handlers[plugin_name] = callback
526
534
 
527
- def register_3d_style(self, plugin_name, style_name, callback):
535
+ def register_3d_style(
536
+ self, plugin_name: str, style_name: str, callback: Callable
537
+ ) -> None:
528
538
  self.custom_3d_styles[style_name] = {
529
539
  "plugin": plugin_name,
530
540
  "callback": callback,
531
541
  }
532
542
 
533
- def register_document_reset_handler(self, plugin_name, callback):
543
+ def register_document_reset_handler(
544
+ self, plugin_name: str, callback: Callable
545
+ ) -> None:
534
546
  """Register callback to be invoked when a new document is created."""
535
547
  self.document_reset_handlers.append(
536
548
  {"plugin": plugin_name, "callback": callback}
@@ -637,7 +649,7 @@ class PluginManager:
637
649
  f"Error in document reset handler for {handler['plugin']}: {e}"
638
650
  )
639
651
 
640
- def get_plugin_info_safe(self, file_path):
652
+ def get_plugin_info_safe(self, file_path: str) -> Dict[str, str]:
641
653
  """Extracts plugin metadata using AST parsing (safe, no execution)."""
642
654
  info = {
643
655
  "name": os.path.basename(file_path),
@@ -691,25 +703,25 @@ class PluginManager:
691
703
 
692
704
  if val is not None:
693
705
  if target.id == "PLUGIN_NAME":
694
- info["name"] = val
706
+ info["name"] = str(val)
695
707
  elif target.id == "PLUGIN_VERSION":
696
- info["version"] = val
708
+ info["version"] = str(val)
697
709
  elif target.id == "PLUGIN_AUTHOR":
698
- info["author"] = val
710
+ info["author"] = str(val)
699
711
  elif target.id == "PLUGIN_DESCRIPTION":
700
- info["description"] = val
712
+ info["description"] = str(val)
701
713
  elif target.id == "PLUGIN_CATEGORY":
702
- info["category"] = val
714
+ info["category"] = str(val)
703
715
  elif (
704
716
  target.id == "__version__"
705
717
  and info["version"] == "Unknown"
706
718
  ):
707
- info["version"] = val
719
+ info["version"] = str(val)
708
720
  elif (
709
721
  target.id == "__author__"
710
722
  and info["author"] == "Unknown"
711
723
  ):
712
- info["author"] = val
724
+ info["author"] = str(val)
713
725
 
714
726
  # Docstring extraction
715
727
  if isinstance(node, ast.Expr):