MoleditPy 3.0.0a3__tar.gz → 3.0.0a4__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 (82) hide show
  1. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/PKG-INFO +1 -1
  2. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/pyproject.toml +1 -1
  3. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/MoleditPy.egg-info/PKG-INFO +1 -1
  4. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/MoleditPy.egg-info/SOURCES.txt +0 -2
  5. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/core/mol_geometry.py +26 -21
  6. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/core/molecular_data.py +20 -13
  7. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/modules/__init__.py +2 -2
  8. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/plugins/plugin_manager.py +32 -31
  9. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/atom_item.py +33 -26
  10. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/bond_item.py +36 -28
  11. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/compute_logic.py +25 -22
  12. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/edit_actions_logic.py +21 -19
  13. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/export_logic.py +27 -24
  14. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/molecular_parsers.py +14 -9
  15. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/molecular_scene_handler.py +20 -9
  16. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/molecule_scene.py +21 -14
  17. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/project_io.py +8 -7
  18. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/string_importers.py +9 -4
  19. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/view_3d_logic.py +56 -36
  20. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/utils/constants.py +1 -1
  21. moleditpy-3.0.0a3/src/moleditpy/ui/edit_3d.py +0 -444
  22. moleditpy-3.0.0a3/src/moleditpy/ui/edit_actions.py +0 -1432
  23. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/LICENSE +0 -0
  24. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/README.md +0 -0
  25. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/setup.cfg +0 -0
  26. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
  27. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/MoleditPy.egg-info/entry_points.txt +0 -0
  28. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/MoleditPy.egg-info/requires.txt +0 -0
  29. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/MoleditPy.egg-info/top_level.txt +0 -0
  30. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/__init__.py +0 -0
  31. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/__main__.py +0 -0
  32. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/assets/file_icon.ico +0 -0
  33. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/assets/icon.icns +0 -0
  34. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/assets/icon.ico +0 -0
  35. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/assets/icon.png +0 -0
  36. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/core/__init__.py +0 -0
  37. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/main.py +0 -0
  38. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/plugins/__init__.py +0 -0
  39. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/plugins/plugin_interface.py +0 -0
  40. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/plugins/plugin_manager_window.py +0 -0
  41. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/__init__.py +0 -0
  42. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/about_dialog.py +0 -0
  43. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/align_plane_dialog.py +0 -0
  44. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/alignment_dialog.py +0 -0
  45. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/analysis_window.py +0 -0
  46. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/angle_dialog.py +0 -0
  47. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/app_state.py +0 -0
  48. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/bond_length_dialog.py +0 -0
  49. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/calculation_worker.py +0 -0
  50. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/color_settings_dialog.py +0 -0
  51. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/compute_engine.py +0 -0
  52. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/constrained_optimization_dialog.py +0 -0
  53. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/custom_interactor_style.py +0 -0
  54. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/custom_qt_interactor.py +0 -0
  55. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/dialog_3d_picking_mixin.py +0 -0
  56. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/dialog_logic.py +0 -0
  57. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/dialog_manager.py +0 -0
  58. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/dihedral_dialog.py +0 -0
  59. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/edit_3d_logic.py +0 -0
  60. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/main_window.py +0 -0
  61. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/main_window_init.py +0 -0
  62. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/mirror_dialog.py +0 -0
  63. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/move_group_dialog.py +0 -0
  64. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
  65. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/planarize_dialog.py +0 -0
  66. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/settings_dialog.py +0 -0
  67. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
  68. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +0 -0
  69. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +0 -0
  70. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +0 -0
  71. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -0
  72. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/sip_isdeleted_safe.py +0 -0
  73. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/template_preview_item.py +0 -0
  74. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/template_preview_view.py +0 -0
  75. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/translation_dialog.py +0 -0
  76. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/ui_manager.py +0 -0
  77. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/user_template_dialog.py +0 -0
  78. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/view_loaders.py +0 -0
  79. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/ui/zoomable_view.py +0 -0
  80. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/utils/__init__.py +0 -0
  81. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/utils/sip_isdeleted_safe.py +0 -0
  82. {moleditpy-3.0.0a3 → moleditpy-3.0.0a4}/src/moleditpy/utils/system_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 3.0.0a3
3
+ Version: 3.0.0a4
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"
7
7
 
8
- version = "3.0.0a3"
8
+ version = "3.0.0a4"
9
9
 
10
10
  license = {file = "LICENSE"}
11
11
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MoleditPy
3
- Version: 3.0.0a3
3
+ Version: 3.0.0a4
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
@@ -49,9 +49,7 @@ src/moleditpy/ui/dialog_3d_picking_mixin.py
49
49
  src/moleditpy/ui/dialog_logic.py
50
50
  src/moleditpy/ui/dialog_manager.py
51
51
  src/moleditpy/ui/dihedral_dialog.py
52
- src/moleditpy/ui/edit_3d.py
53
52
  src/moleditpy/ui/edit_3d_logic.py
54
- src/moleditpy/ui/edit_actions.py
55
53
  src/moleditpy/ui/edit_actions_logic.py
56
54
  src/moleditpy/ui/export_logic.py
57
55
  src/moleditpy/ui/main_window.py
@@ -10,23 +10,19 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
- """Pure-logic molecular-geometry helpers.
14
-
15
- This module is intentionally free of any GUI imports so that it can
16
- be unit-tested in isolation and shared across dialogs and main-window
17
- submodules without circular dependencies.
18
- """
19
-
20
- import numpy as np
13
+ from __future__ import annotations
21
14
  import math
15
+ import logging
22
16
  from collections import deque
17
+ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
18
+
19
+ import numpy as np
23
20
 
24
21
  # ------------------------------------------------------------------
25
22
  # Primitive geometry helpers
26
23
  # ------------------------------------------------------------------
27
24
 
28
-
29
- def calc_distance(pos1, pos2) -> float:
25
+ def calc_distance(pos1: Union[np.ndarray, Tuple[float, float, float], List[float]], pos2: Union[np.ndarray, Tuple[float, float, float], List[float]]) -> float:
30
26
  """Return the Euclidean distance between two 3-D positions.
31
27
 
32
28
  Parameters
@@ -44,7 +40,11 @@ def calc_distance(pos1, pos2) -> float:
44
40
  )
45
41
 
46
42
 
47
- def calc_angle_deg(pos1, pos2_vertex, pos3) -> float:
43
+ def calc_angle_deg(
44
+ pos1: Union[np.ndarray, Tuple[float, float, float], List[float]],
45
+ pos2_vertex: Union[np.ndarray, Tuple[float, float, float], List[float]],
46
+ pos3: Union[np.ndarray, Tuple[float, float, float], List[float]],
47
+ ) -> float:
48
48
  """Return the angle pos1–pos2_vertex–pos3 in degrees.
49
49
 
50
50
  The angle is measured at *pos2_vertex* and is always in [0, 180].
@@ -78,7 +78,7 @@ def calc_angle_deg(pos1, pos2_vertex, pos3) -> float:
78
78
  # ------------------------------------------------------------------
79
79
 
80
80
 
81
- def get_connected_group(mol, start_atom, exclude=None):
81
+ def get_connected_group(mol: Any, start_atom: int, exclude: Optional[int] = None) -> Set[int]:
82
82
  """Return the set of atom indices reachable from *start_atom*
83
83
  without passing through *exclude*.
84
84
 
@@ -124,7 +124,7 @@ def get_connected_group(mol, start_atom, exclude=None):
124
124
  # ------------------------------------------------------------------
125
125
 
126
126
 
127
- def rodrigues_rotate(v, axis, angle):
127
+ def rodrigues_rotate(v: np.ndarray, axis: np.ndarray, angle: float) -> np.ndarray:
128
128
  """Rotate vector *v* around a unit *axis* by *angle* radians.
129
129
 
130
130
  Implements Rodrigues' rotation formula:
@@ -151,8 +151,13 @@ def rodrigues_rotate(v, axis, angle):
151
151
 
152
152
 
153
153
  def adjust_bond_angle(
154
- positions, idx_a, idx_b, idx_c, target_angle_deg, atom_indices_to_move
155
- ):
154
+ positions: np.ndarray,
155
+ idx_a: int,
156
+ idx_b: int,
157
+ idx_c: int,
158
+ target_angle_deg: float,
159
+ atom_indices_to_move: Iterable[int],
160
+ ) -> float:
156
161
  """Adjust the A–B–C bond angle to *target_angle_deg* using a
157
162
  difference-based rotation.
158
163
 
@@ -244,7 +249,7 @@ def adjust_bond_angle(
244
249
  # ------------------------------------------------------------------
245
250
 
246
251
 
247
- def calculate_dihedral(positions, i1, i2, i3, i4):
252
+ def calculate_dihedral(positions: Any, i1: int, i2: int, i3: int, i4: int) -> float:
248
253
  """Compute the dihedral angle defined by four atom indices.
249
254
 
250
255
  Parameters
@@ -314,7 +319,7 @@ _VALENCE_LIMITS = {
314
319
  }
315
320
 
316
321
 
317
- def is_problematic_valence(symbol, bond_count, charge=0):
322
+ def is_problematic_valence(symbol: str, bond_count: Union[int, float], charge: int = 0) -> bool:
318
323
  """Return ``True`` if the atom's total bond order exceeds its
319
324
  typical maximum valence.
320
325
 
@@ -406,7 +411,7 @@ def inject_ez_stereo_to_mol_block(mol_block, rdkit_mol, bonds_data):
406
411
  return "\n".join(mol_lines)
407
412
 
408
413
 
409
- def identify_valence_problems(atoms_data, bonds_data):
414
+ def identify_valence_problems(atoms_data: Dict[int, Any], bonds_data: Dict[Tuple[int, int], Any]) -> List[int]:
410
415
  """Identify atoms with problematic valence.
411
416
 
412
417
  Parameters
@@ -441,7 +446,7 @@ def identify_valence_problems(atoms_data, bonds_data):
441
446
  return problem_atom_ids
442
447
 
443
448
 
444
- def optimize_2d_coords(mol):
449
+ def optimize_2d_coords(mol: Any) -> Dict[int, Tuple[float, float]]:
445
450
  """Generate 2D coordinates using RDKit and return a map of (x, y) tuples."""
446
451
  from rdkit.Chem import AllChem
447
452
 
@@ -571,12 +576,12 @@ def resolve_2d_overlaps(
571
576
  rep_id1, rep_id2 = i1, i2
572
577
  break
573
578
 
574
- if not rep_id1:
579
+ if rep_id1 is None:
575
580
  continue
576
581
 
577
582
  frag1 = next((f for f in fragments if rep_id1 in f), None)
578
583
  frag2 = next((f for f in fragments if rep_id2 in f), None)
579
- if not frag1 or not frag2 or frag1 == frag2:
584
+ if frag1 is None or frag2 is None or frag1 == frag2:
580
585
  continue
581
586
 
582
587
  ids_to_move = frag1 if rep_id1 > rep_id2 else frag2
@@ -10,7 +10,9 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
+ from __future__ import annotations
13
14
  import logging
15
+ from typing import Any, Dict, List, Optional, Set, Tuple, Union
14
16
  from rdkit import Chem
15
17
 
16
18
  try:
@@ -22,21 +24,26 @@ except ImportError:
22
24
  class PointTuple(tuple):
23
25
  """Backward-compatible tuple that allows .x() and .y() access like QPointF."""
24
26
 
25
- def x(self):
27
+ def x(self) -> float:
26
28
  return self[0]
27
29
 
28
- def y(self):
30
+ def y(self) -> float:
29
31
  return self[1]
30
32
 
31
33
 
32
34
  class MolecularData:
33
- def __init__(self):
35
+ atoms: Dict[int, Dict[str, Any]]
36
+ bonds: Dict[Tuple[int, int], Dict[str, Any]]
37
+ adjacency_list: Dict[int, List[int]]
38
+ _next_atom_id: int
39
+
40
+ def __init__(self) -> None:
34
41
  self.atoms = {}
35
42
  self.bonds = {}
36
43
  self._next_atom_id = 0
37
44
  self.adjacency_list = {}
38
45
 
39
- def add_atom(self, symbol, pos, charge=0, radical=0):
46
+ def add_atom(self, symbol: str, pos: Union[Any, Tuple[float, float]], charge: int = 0, radical: int = 0) -> int:
40
47
  atom_id = self._next_atom_id
41
48
  # Internalize position as raw floats to decouple from UI types (QPointF)
42
49
  if hasattr(pos, "x") and hasattr(pos, "y"):
@@ -55,7 +62,7 @@ class MolecularData:
55
62
  self._next_atom_id += 1
56
63
  return atom_id
57
64
 
58
- def set_atom_pos(self, atom_id, pos):
65
+ def set_atom_pos(self, atom_id: int, pos: Union[Any, Tuple[float, float]]) -> None:
59
66
  """Update atom position using raw floats or QPointF."""
60
67
  if atom_id in self.atoms:
61
68
  if hasattr(pos, "x") and hasattr(pos, "y"):
@@ -65,7 +72,7 @@ class MolecularData:
65
72
  else:
66
73
  self.atoms[atom_id]["pos"] = PointTuple((float(pos[0]), float(pos[1])))
67
74
 
68
- def add_bond(self, id1, id2, order=1, stereo=0):
75
+ def add_bond(self, id1: int, id2: int, order: Union[int, float] = 1, stereo: int = 0) -> Tuple[Tuple[int, int], str]:
69
76
  # For stereo bonds, do not sort because ID order determines direction.
70
77
  # For non-stereo bonds, sort to normalize the key.
71
78
  if stereo == 0:
@@ -88,7 +95,7 @@ class MolecularData:
88
95
  self.bonds[(id1, id2)] = bond_data
89
96
  return (id1, id2), "created"
90
97
 
91
- def remove_atom(self, atom_id):
98
+ def remove_atom(self, atom_id: int) -> None:
92
99
  if atom_id in self.atoms:
93
100
  # Safely get neighbors before deleting the atom's own entry
94
101
  neighbors = self.adjacency_list.get(atom_id, [])
@@ -111,7 +118,7 @@ class MolecularData:
111
118
  for key in bonds_to_remove:
112
119
  self.bonds.pop(key, None)
113
120
 
114
- def remove_bond(self, id1, id2):
121
+ def remove_bond(self, id1: int, id2: int) -> None:
115
122
  # Look for directional stereo bonds (forward/reverse) and normalized non-stereo bond keys.
116
123
  key_to_remove = None
117
124
  if (id1, id2) in self.bonds:
@@ -126,7 +133,7 @@ class MolecularData:
126
133
  self.adjacency_list[id2].remove(id1)
127
134
  self.bonds.pop(key_to_remove, None)
128
135
 
129
- def to_rdkit_mol(self, use_2d_stereo=True):
136
+ def to_rdkit_mol(self, use_2d_stereo: bool = True) -> Optional[Chem.Mol]:
130
137
  """
131
138
  use_2d_stereo: True estimates E/Z from 2D coordinates (as before). False prioritizes E/Z labels.
132
139
  Call with use_2d_stereo=False for 3D conversion.
@@ -227,7 +234,7 @@ class MolecularData:
227
234
  )
228
235
 
229
236
  # Helper: Pick neighbors prioritizing heavy atoms
230
- def pick_preferred_neighbor(atom, exclude_idx):
237
+ def pick_preferred_neighbor(atom: Chem.Atom, exclude_idx: int) -> Optional[int]:
231
238
  for nbr in atom.GetNeighbors():
232
239
  if nbr.GetIdx() == exclude_idx:
233
240
  continue
@@ -305,7 +312,7 @@ class MolecularData:
305
312
  Chem.AssignStereochemistry(final_mol, cleanIt=False, force=False)
306
313
  return final_mol
307
314
 
308
- def update_ring_info_2d(self):
315
+ def update_ring_info_2d(self) -> None:
309
316
  """Update is_in_ring and ring_center for all BondItems based on 2D topology."""
310
317
  if not self.atoms or not self.bonds:
311
318
  return
@@ -386,7 +393,7 @@ class MolecularData:
386
393
  # so this is usually correct for 2D drawing.
387
394
  bond_item.ring_center = ring_center
388
395
 
389
- def to_mol_block(self):
396
+ def to_mol_block(self) -> Optional[str]:
390
397
  mol = self.to_rdkit_mol()
391
398
  if mol:
392
399
  try:
@@ -449,7 +456,7 @@ class MolecularData:
449
456
  mol_block += "M END\n"
450
457
  return mol_block
451
458
 
452
- def to_template_dict(self, name, version="1.0", application_version=""):
459
+ def to_template_dict(self, name: str, version: str = "1.0", application_version: str = "") -> Dict[str, Any]:
453
460
  """Convert current structure to a dictionary for template storage."""
454
461
  import datetime
455
462
 
@@ -54,8 +54,8 @@ _MAPPINGS = {
54
54
  "view_2d": "moleditpy.ui.zoomable_view", # Alias for ZoomableView
55
55
  # Renamed UI Mixins
56
56
  "main_window_dialog_manager": "moleditpy.ui.dialog_manager",
57
- "main_window_edit_3d": "moleditpy.ui.edit_3d",
58
- "main_window_edit_actions": "moleditpy.ui.edit_actions",
57
+ "main_window_edit_3d_logic": "moleditpy.ui.edit_3d_logic",
58
+ "main_window_edit_actions_logic": "moleditpy.ui.edit_actions_logic",
59
59
  "main_window_export": "moleditpy.ui.export_logic",
60
60
  "main_window_main_init": "moleditpy.ui.main_window_init",
61
61
  "main_window_ui_manager": "moleditpy.ui.ui_manager",
@@ -10,6 +10,7 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
+ from __future__ import annotations
13
14
  import ast
14
15
  import importlib.util
15
16
  import logging
@@ -17,6 +18,7 @@ import os
17
18
  import shutil
18
19
  import sys
19
20
  import zipfile
21
+ from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
20
22
 
21
23
  from PyQt6.QtCore import QUrl
22
24
  from PyQt6.QtGui import QDesktopServices
@@ -28,35 +30,34 @@ except ImportError:
28
30
  # Fallback if running as script
29
31
  from moleditpy.plugins.plugin_interface import PluginContext
30
32
 
31
-
32
33
  class PluginManager:
33
- def __init__(self, main_window=None):
34
- self.plugin_dir = os.path.join(os.path.expanduser("~"), ".moleditpy", "plugins")
35
- self.plugins = [] # List of dicts
36
- self.main_window = main_window
34
+ def __init__(self, main_window: Any = None) -> None:
35
+ self.plugin_dir: str = os.path.join(os.path.expanduser("~"), ".moleditpy", "plugins")
36
+ self.plugins: List[Dict[str, Any]] = [] # List of dicts
37
+ self.main_window: Any = main_window
37
38
 
38
39
  # Registries for actions
39
- self.menu_actions = [] # List of (plugin_name, path, callback, text, icon, shortcut)
40
- self.toolbar_actions = []
41
- self.drop_handlers = [] # List of (priority, plugin_name, callback)
42
-
43
- # Extended Registries (Added to prevent lazy initialization "monkey patching")
44
- self.export_actions = []
45
- self.optimization_methods = {}
46
- self.file_openers = {} # ext -> list of {'plugin':..., 'callback':..., 'priority':...}
47
- self.analysis_tools = []
48
- self.save_handlers = {}
49
- self.load_handlers = {}
50
- self.custom_3d_styles = {} # style_name -> {'plugin': name, 'callback': func}
51
- self.document_reset_handlers = [] # List of callbacks to call on new document
52
-
53
- def get_main_window(self):
40
+ self.menu_actions: List[Dict[str, Any]] = []
41
+ self.toolbar_actions: List[Dict[str, Any]] = []
42
+ self.drop_handlers: List[Dict[str, Any]] = []
43
+
44
+ # Extended Registries
45
+ self.export_actions: List[Dict[str, Any]] = []
46
+ self.optimization_methods: Dict[str, Dict[str, Any]] = {}
47
+ self.file_openers: Dict[str, List[Dict[str, Any]]] = {}
48
+ self.analysis_tools: List[Dict[str, Any]] = []
49
+ self.save_handlers: Dict[str, Callable] = {}
50
+ self.load_handlers: Dict[str, Callable] = {}
51
+ self.custom_3d_styles: Dict[str, Dict[str, Any]] = {}
52
+ self.document_reset_handlers: List[Dict[str, Any]] = []
53
+
54
+ def get_main_window(self) -> Any:
54
55
  return self.main_window
55
56
 
56
- def set_main_window(self, mw):
57
+ def set_main_window(self, mw: Any) -> None:
57
58
  self.main_window = mw
58
59
 
59
- def ensure_plugin_dir(self):
60
+ def ensure_plugin_dir(self) -> None:
60
61
  """Creates the plugin directory if it doesn't exist."""
61
62
  if not os.path.exists(self.plugin_dir):
62
63
  try:
@@ -64,12 +65,12 @@ class PluginManager:
64
65
  except OSError as e:
65
66
  logging.error(f"Error creating plugin directory: {e}")
66
67
 
67
- def open_plugin_folder(self):
68
+ def open_plugin_folder(self) -> None:
68
69
  """Opens the plugin directory in the OS file explorer."""
69
70
  self.ensure_plugin_dir()
70
71
  QDesktopServices.openUrl(QUrl.fromLocalFile(self.plugin_dir))
71
72
 
72
- def install_plugin(self, file_path):
73
+ def install_plugin(self, file_path: str) -> Tuple[bool, str]:
73
74
  """Copies a plugin file to the plugin directory. Supports .py and .zip."""
74
75
  self.ensure_plugin_dir()
75
76
  try:
@@ -169,7 +170,7 @@ class PluginManager:
169
170
  ) as e:
170
171
  return False, str(e)
171
172
 
172
- def discover_plugins(self, parent=None):
173
+ def discover_plugins(self, parent: Any = None) -> List[Dict[str, Any]]:
173
174
  """
174
175
  Hybrid discovery:
175
176
  - Folders with '__init__.py' -> Treated as single package plugin.
@@ -233,7 +234,7 @@ class PluginManager:
233
234
 
234
235
  return self.plugins
235
236
 
236
- def _load_single_plugin(self, filepath, module_name, category):
237
+ def _load_single_plugin(self, filepath: str, module_name: str, category: str) -> None:
237
238
  """Common loading logic for both single-file and package plugins."""
238
239
  try:
239
240
  # Ensure unique module name by including category path
@@ -350,7 +351,7 @@ class PluginManager:
350
351
  # crashing the entire discovery process.
351
352
  logging.error(f"Failed to load plugin {module_name}: {e}")
352
353
 
353
- def run_plugin(self, module, main_window):
354
+ def run_plugin(self, module: Any, main_window: Any) -> None:
354
355
  """Executes the plugin's run method (Legacy manual trigger)."""
355
356
  try:
356
357
  module.run(main_window)
@@ -369,7 +370,7 @@ class PluginManager:
369
370
  )
370
371
 
371
372
  # --- Registration Callbacks ---
372
- def register_menu_action(self, plugin_name, path, callback, text, icon, shortcut):
373
+ def register_menu_action(self, plugin_name: str, path: str, callback: Callable, text: str, icon: str, shortcut: str) -> None:
373
374
  self.menu_actions.append(
374
375
  {
375
376
  "plugin": plugin_name,
@@ -381,7 +382,7 @@ class PluginManager:
381
382
  }
382
383
  )
383
384
 
384
- def register_toolbar_action(self, plugin_name, callback, text, icon, tooltip):
385
+ def register_toolbar_action(self, plugin_name: str, callback: Callable, text: str, icon: str, tooltip: str) -> None:
385
386
  self.toolbar_actions.append(
386
387
  {
387
388
  "plugin": plugin_name,
@@ -392,7 +393,7 @@ class PluginManager:
392
393
  }
393
394
  )
394
395
 
395
- def register_drop_handler(self, plugin_name, callback, priority):
396
+ def register_drop_handler(self, plugin_name: str, callback: Callable, priority: int) -> None:
396
397
  self.drop_handlers.append(
397
398
  {"priority": priority, "plugin": plugin_name, "callback": callback}
398
399
  )
@@ -453,7 +454,7 @@ class PluginManager:
453
454
  {"plugin": plugin_name, "callback": callback}
454
455
  )
455
456
 
456
- def invoke_document_reset_handlers(self):
457
+ def invoke_document_reset_handlers(self) -> None:
457
458
  """Call all registered document reset handlers."""
458
459
  for handler in self.document_reset_handlers:
459
460
  try:
@@ -10,6 +10,9 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
10
10
  DOI: 10.5281/zenodo.17268532
11
11
  """
12
12
 
13
+ from __future__ import annotations
14
+ from typing import Any, Dict, List, Optional, Set, Tuple, Union
15
+
13
16
  from PyQt6.QtCore import QPointF, QRectF, Qt
14
17
  from PyQt6.QtGui import (
15
18
  QBrush,
@@ -38,32 +41,29 @@ except ImportError:
38
41
  FONT_FAMILY,
39
42
  FONT_WEIGHT_BOLD,
40
43
  )
41
-
42
44
  from PyQt6 import sip
43
45
 
44
-
45
- def sip_isdeleted_safe(obj):
46
+ def sip_isdeleted_safe(obj: Any) -> bool:
46
47
  try:
47
48
  return sip.isdeleted(obj)
48
- except (AttributeError, TypeError, RuntimeError):
49
+ except (AttributeError, TypeError, ImportError, RuntimeError):
49
50
  # If the object does not support sip.isdeleted or is already in a state
50
51
  # where the check fails, we assume it's unsafe or "deleted" for our purposes.
51
52
  return True
52
53
 
53
54
 
54
55
  class AtomItem(QGraphicsItem):
55
- def __init__(self, atom_id, symbol, pos, charge=0, radical=0):
56
+ def __init__(self, atom_id: int, symbol: str, pos: QPointF, charge: int = 0, radical: int = 0) -> None:
56
57
  super().__init__()
57
- (
58
- self.atom_id,
59
- self.symbol,
60
- self.charge,
61
- self.radical,
62
- self.bonds,
63
- self.chiral_label,
64
- ) = atom_id, symbol, charge, radical, [], None
58
+ self.atom_id: int = atom_id
59
+ self.symbol: str = symbol
60
+ self.charge: int = charge
61
+ self.radical: int = radical
62
+ self.bonds: List[Any] = []
63
+ self.chiral_label: Optional[str] = None
64
+
65
65
  self.setPos(pos)
66
- self.implicit_h_count = 0
66
+ self.implicit_h_count: int = 0
67
67
  self.setFlags(
68
68
  QGraphicsItem.GraphicsItemFlag.ItemIsMovable
69
69
  | QGraphicsItem.GraphicsItemFlag.ItemIsSelectable
@@ -71,13 +71,12 @@ class AtomItem(QGraphicsItem):
71
71
  self.setZValue(1)
72
72
  self.update_style()
73
73
  self.setAcceptHoverEvents(True)
74
- self.hovered = False
75
- self.hovered = False
76
- self.has_problem = False
77
- self.is_visible = True
78
- self.font = QFont(FONT_FAMILY, 20, FONT_WEIGHT_BOLD)
74
+ self.hovered: bool = False
75
+ self.has_problem: bool = False
76
+ self.is_visible: bool = True
77
+ self.font: QFont = QFont(FONT_FAMILY, 20, FONT_WEIGHT_BOLD)
79
78
 
80
- def update_style(self):
79
+ def update_style(self) -> None:
81
80
  if sip_isdeleted_safe(self):
82
81
  return
83
82
  # Allow updating font preference dynamically
@@ -100,7 +99,8 @@ class AtomItem(QGraphicsItem):
100
99
  )
101
100
  self.update()
102
101
 
103
- def boundingRect(self):
102
+ def boundingRect(self) -> QRectF:
103
+ """Calculate the bounding rectangle for the atom item."""
104
104
  # --- Calculate text position and size using logic matching paint() ---
105
105
  # Get dynamic font size and family
106
106
  font_size = 20
@@ -215,7 +215,8 @@ class AtomItem(QGraphicsItem):
215
215
  # 3. Add final margins for selection highlights, etc.
216
216
  return full_visual_rect.adjusted(-3, -3, 3, 3)
217
217
 
218
- def shape(self):
218
+ def shape(self) -> QPainterPath:
219
+ """Define the shape of the atom item for collision detection."""
219
220
  scene = self.scene()
220
221
  if not scene or not scene.views():
221
222
  path = QPainterPath()
@@ -232,7 +233,13 @@ class AtomItem(QGraphicsItem):
232
233
  path.addEllipse(QPointF(0, 0), scene_radius, scene_radius)
233
234
  return path
234
235
 
235
- def paint(self, painter, option, widget):
236
+ def paint(
237
+ self,
238
+ painter: QPainter,
239
+ option: Any,
240
+ widget: Optional[QWidget] = None,
241
+ ) -> None:
242
+ """Paint the atom symbol and its associated labels (charge, radical)."""
236
243
  # Color logic: check if we should use bond color (uniform) or CPK (element-specific)
237
244
  color = CPK_COLORS.get(self.symbol, CPK_COLORS["DEFAULT"])
238
245
  # Use bond color if specified in settings
@@ -428,7 +435,7 @@ class AtomItem(QGraphicsItem):
428
435
  painter.setPen(pen)
429
436
  painter.drawRect(self.boundingRect())
430
437
 
431
- def itemChange(self, change, value):
438
+ def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value: Any) -> Any:
432
439
  res = super().itemChange(change, value)
433
440
  if change == QGraphicsItem.GraphicsItemChange.ItemPositionHasChanged:
434
441
  if self.flags() & QGraphicsItem.GraphicsItemFlag.ItemIsMovable:
@@ -438,13 +445,13 @@ class AtomItem(QGraphicsItem):
438
445
 
439
446
  return res
440
447
 
441
- def hoverEnterEvent(self, event):
448
+ def hoverEnterEvent(self, event: Any) -> None:
442
449
  # Enable highlight on hover regardless of scene mode
443
450
  self.hovered = True
444
451
  self.update()
445
452
  super().hoverEnterEvent(event)
446
453
 
447
- def hoverLeaveEvent(self, event):
454
+ def hoverLeaveEvent(self, event: Any) -> None:
448
455
  if self.hovered:
449
456
  self.hovered = False
450
457
  self.update()