MoleditPy-linux 4.1.4__py3-none-any.whl → 4.2.1__py3-none-any.whl
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.
- moleditpy_linux/core/mol_geometry.py +61 -4
- moleditpy_linux/core/molecular_data.py +35 -13
- moleditpy_linux/main.py +7 -4
- moleditpy_linux/plugins/plugin_interface.py +2 -2
- moleditpy_linux/plugins/plugin_manager.py +28 -19
- moleditpy_linux/plugins/plugin_manager_window.py +4 -4
- moleditpy_linux/ui/about_dialog.py +1 -1
- moleditpy_linux/ui/align_plane_dialog.py +3 -3
- moleditpy_linux/ui/alignment_dialog.py +3 -3
- moleditpy_linux/ui/analysis_window.py +2 -2
- moleditpy_linux/ui/angle_dialog.py +16 -15
- moleditpy_linux/ui/app_state.py +19 -9
- moleditpy_linux/ui/atom_item.py +9 -6
- moleditpy_linux/ui/atom_picking.py +3 -3
- moleditpy_linux/ui/base_picking_dialog.py +5 -4
- moleditpy_linux/ui/bond_item.py +22 -17
- moleditpy_linux/ui/bond_length_dialog.py +15 -14
- moleditpy_linux/ui/calculation_worker.py +29 -34
- moleditpy_linux/ui/color_settings_dialog.py +11 -11
- moleditpy_linux/ui/compute_logic.py +100 -71
- moleditpy_linux/ui/constrained_optimization_dialog.py +1 -1
- moleditpy_linux/ui/custom_interactor_style.py +27 -16
- moleditpy_linux/ui/dialog_3d_picking_mixin.py +9 -5
- moleditpy_linux/ui/dihedral_dialog.py +14 -13
- moleditpy_linux/ui/edit_3d_logic.py +11 -10
- moleditpy_linux/ui/edit_actions_logic.py +62 -59
- moleditpy_linux/ui/export_logic.py +13 -7
- moleditpy_linux/ui/geometry_base_dialog.py +2 -1
- moleditpy_linux/ui/io_logic.py +65 -38
- moleditpy_linux/ui/main_window.py +5 -5
- moleditpy_linux/ui/main_window_init.py +77 -68
- moleditpy_linux/ui/mirror_dialog.py +1 -1
- moleditpy_linux/ui/molecular_scene_handler.py +62 -11
- moleditpy_linux/ui/molecule_scene.py +12 -14
- moleditpy_linux/ui/move_group_dialog.py +14 -14
- moleditpy_linux/ui/move_selected_atoms_dialog.py +13 -13
- moleditpy_linux/ui/periodic_table_dialog.py +1 -1
- moleditpy_linux/ui/planarize_dialog.py +7 -5
- moleditpy_linux/ui/plugin_menu_manager.py +19 -7
- moleditpy_linux/ui/settings_dialog.py +8 -8
- moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +9 -9
- moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +7 -7
- moleditpy_linux/ui/settings_tabs/settings_other_tab.py +8 -8
- moleditpy_linux/ui/settings_tabs/settings_tab_base.py +15 -14
- moleditpy_linux/ui/translation_dialog.py +11 -11
- moleditpy_linux/ui/ui_manager.py +6 -6
- moleditpy_linux/ui/user_template_dialog.py +5 -5
- moleditpy_linux/ui/view_3d_logic.py +69 -65
- moleditpy_linux/ui/zoomable_view.py +14 -13
- moleditpy_linux/utils/constants.py +11 -4
- moleditpy_linux/utils/sip_isdeleted_safe.py +1 -1
- moleditpy_linux/utils/suppress_log.py +31 -0
- moleditpy_linux/utils/system_utils.py +10 -4
- {moleditpy_linux-4.1.4.dist-info → moleditpy_linux-4.2.1.dist-info}/METADATA +6 -6
- moleditpy_linux-4.2.1.dist-info/RECORD +76 -0
- moleditpy_linux-4.1.4.dist-info/RECORD +0 -75
- {moleditpy_linux-4.1.4.dist-info → moleditpy_linux-4.2.1.dist-info}/WHEEL +0 -0
- {moleditpy_linux-4.1.4.dist-info → moleditpy_linux-4.2.1.dist-info}/entry_points.txt +0 -0
- {moleditpy_linux-4.1.4.dist-info → moleditpy_linux-4.2.1.dist-info}/licenses/LICENSE +0 -0
- {moleditpy_linux-4.1.4.dist-info → moleditpy_linux-4.2.1.dist-info}/top_level.txt +0 -0
|
@@ -517,6 +517,14 @@ def identify_valence_problems(
|
|
|
517
517
|
list
|
|
518
518
|
IDs of atoms with problematic valence.
|
|
519
519
|
"""
|
|
520
|
+
# Preferred: RDKit's own detection on an unsanitized mol. Unlike the
|
|
521
|
+
# heuristic table below, it knows every element and accounts for formal
|
|
522
|
+
# charges and radical electrons (e.g. hypervalent N+, S, B, radicals).
|
|
523
|
+
rdkit_ids = _identify_problems_rdkit(atoms_data, bonds_data)
|
|
524
|
+
if rdkit_ids is not None:
|
|
525
|
+
return rdkit_ids
|
|
526
|
+
|
|
527
|
+
# Fallback heuristic when RDKit cannot even construct the atoms
|
|
520
528
|
problem_atom_ids = []
|
|
521
529
|
|
|
522
530
|
# Pre-calculate bond orders per atom
|
|
@@ -537,6 +545,52 @@ def identify_valence_problems(
|
|
|
537
545
|
return problem_atom_ids
|
|
538
546
|
|
|
539
547
|
|
|
548
|
+
def _identify_problems_rdkit(
|
|
549
|
+
atoms_data: Dict[int, Any], bonds_data: Dict[Tuple[int, int], Any]
|
|
550
|
+
) -> Optional[List[int]]:
|
|
551
|
+
"""Find problem atoms via Chem.DetectChemistryProblems (no sanitization).
|
|
552
|
+
|
|
553
|
+
Returns None when the molecule cannot be constructed at all (unknown
|
|
554
|
+
symbols etc.), signalling the caller to use the heuristic fallback.
|
|
555
|
+
"""
|
|
556
|
+
try:
|
|
557
|
+
from rdkit import Chem as _Chem
|
|
558
|
+
|
|
559
|
+
mol = _Chem.RWMol()
|
|
560
|
+
id_to_idx = {}
|
|
561
|
+
for atom_id, data in atoms_data.items():
|
|
562
|
+
atom = _Chem.Atom(data["symbol"])
|
|
563
|
+
atom.SetFormalCharge(data.get("charge", 0))
|
|
564
|
+
atom.SetNumRadicalElectrons(data.get("radical", 0))
|
|
565
|
+
atom.SetNoImplicit(False)
|
|
566
|
+
id_to_idx[atom_id] = mol.AddAtom(atom)
|
|
567
|
+
|
|
568
|
+
bond_type_map = {
|
|
569
|
+
1.0: _Chem.BondType.SINGLE,
|
|
570
|
+
1.5: _Chem.BondType.AROMATIC,
|
|
571
|
+
2.0: _Chem.BondType.DOUBLE,
|
|
572
|
+
3.0: _Chem.BondType.TRIPLE,
|
|
573
|
+
}
|
|
574
|
+
for (id1, id2), bond in bonds_data.items():
|
|
575
|
+
if id1 not in id_to_idx or id2 not in id_to_idx:
|
|
576
|
+
continue
|
|
577
|
+
order = bond_type_map.get(
|
|
578
|
+
float(bond.get("order", 1)), _Chem.BondType.SINGLE
|
|
579
|
+
)
|
|
580
|
+
mol.AddBond(id_to_idx[id1], id_to_idx[id2], order)
|
|
581
|
+
|
|
582
|
+
idx_to_id = {v: k for k, v in id_to_idx.items()}
|
|
583
|
+
problem_ids = set()
|
|
584
|
+
for prob in _Chem.DetectChemistryProblems(mol.GetMol()):
|
|
585
|
+
if hasattr(prob, "GetAtomIdx"):
|
|
586
|
+
problem_ids.add(idx_to_id[prob.GetAtomIdx()])
|
|
587
|
+
elif hasattr(prob, "GetAtomIndices"):
|
|
588
|
+
problem_ids.update(idx_to_id[i] for i in prob.GetAtomIndices())
|
|
589
|
+
return sorted(problem_ids)
|
|
590
|
+
except (RuntimeError, ValueError, TypeError, KeyError):
|
|
591
|
+
return None
|
|
592
|
+
|
|
593
|
+
|
|
540
594
|
def optimize_2d_coords(mol: Chem.Mol) -> Dict[int, Tuple[float, float]]:
|
|
541
595
|
"""Generate 2D coordinates using RDKit and return a map of (x, y) tuples."""
|
|
542
596
|
from rdkit.Chem import AllChem
|
|
@@ -618,10 +672,13 @@ def resolve_2d_overlaps(
|
|
|
618
672
|
parent = {aid: aid for aid in atom_ids}
|
|
619
673
|
|
|
620
674
|
def find_set(aid: int) -> int:
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
parent[
|
|
624
|
-
|
|
675
|
+
# Iterative path compression avoids recursion limits
|
|
676
|
+
root = aid
|
|
677
|
+
while parent[root] != root:
|
|
678
|
+
root = parent[root]
|
|
679
|
+
while parent[aid] != root:
|
|
680
|
+
parent[aid], aid = root, parent[aid]
|
|
681
|
+
return root
|
|
625
682
|
|
|
626
683
|
def unite_sets(aid1: int, aid2: int) -> None:
|
|
627
684
|
root1 = find_set(aid1)
|
|
@@ -101,9 +101,14 @@ class MolecularData:
|
|
|
101
101
|
if (id1, id2) in self.bonds:
|
|
102
102
|
self.bonds[(id1, id2)].update(bond_data)
|
|
103
103
|
return (id1, id2), "updated"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
if (id2, id1) in self.bonds:
|
|
105
|
+
# Re-key reversed entry (stereo direction changed) to avoid duplicates
|
|
106
|
+
existing = self.bonds.pop((id2, id1))
|
|
107
|
+
existing.update(bond_data)
|
|
108
|
+
self.bonds[(id1, id2)] = existing
|
|
109
|
+
return (id1, id2), "updated"
|
|
110
|
+
self.bonds[(id1, id2)] = bond_data
|
|
111
|
+
return (id1, id2), "created"
|
|
107
112
|
|
|
108
113
|
def remove_atom(self, atom_id: int) -> None:
|
|
109
114
|
"""Remove an atom and all bonds involving it from the data model."""
|
|
@@ -171,7 +176,9 @@ class MolecularData:
|
|
|
171
176
|
atom_id_to_idx_map[atom_id] = idx
|
|
172
177
|
|
|
173
178
|
# save bonds & stereo info (label info is kept here)
|
|
174
|
-
bond_stereo_info
|
|
179
|
+
bond_stereo_info: Dict[
|
|
180
|
+
int, Dict[str, Any]
|
|
181
|
+
] = {} # bond_idx -> {'type', 'atom_ids', 'bond_data'}
|
|
175
182
|
for (id1, id2), bond_data in self.bonds.items():
|
|
176
183
|
if id1 not in atom_id_to_idx_map or id2 not in atom_id_to_idx_map:
|
|
177
184
|
continue
|
|
@@ -279,8 +286,8 @@ class MolecularData:
|
|
|
279
286
|
begin_atom_idx = bond.GetBeginAtomIdx()
|
|
280
287
|
end_atom_idx = bond.GetEndAtomIdx()
|
|
281
288
|
|
|
282
|
-
|
|
283
|
-
stereo_atoms_specified =
|
|
289
|
+
label_bond_data = info.get("bond_data") or {}
|
|
290
|
+
stereo_atoms_specified = label_bond_data.get("stereo_atoms")
|
|
284
291
|
|
|
285
292
|
if stereo_atoms_specified:
|
|
286
293
|
try:
|
|
@@ -338,10 +345,10 @@ class MolecularData:
|
|
|
338
345
|
|
|
339
346
|
if not self.atoms:
|
|
340
347
|
return None
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
348
|
+
|
|
349
|
+
# Counts line and bond indices must only reflect atoms actually written
|
|
350
|
+
atom_map: Dict[int, int] = {}
|
|
351
|
+
atom_lines: List[str] = []
|
|
345
352
|
for old_id, atom in self.atoms.items():
|
|
346
353
|
# Convert scene pixel coordinates to angstroms when emitting MOL block
|
|
347
354
|
pos = atom.get("pos")
|
|
@@ -373,10 +380,19 @@ class MolecularData:
|
|
|
373
380
|
elif charge == -3:
|
|
374
381
|
chg_code = 7
|
|
375
382
|
|
|
376
|
-
|
|
383
|
+
atom_map[old_id] = len(atom_lines)
|
|
384
|
+
atom_lines.append(
|
|
385
|
+
f"{x:10.4f}{y:10.4f}{z:10.4f} {symbol:<3} 0 0 0{chg_code:3d} 0 0 0 0 0 0 0\n"
|
|
386
|
+
)
|
|
377
387
|
|
|
388
|
+
bond_lines = []
|
|
378
389
|
for (id1, id2), bond in self.bonds.items():
|
|
379
|
-
|
|
390
|
+
if id1 not in atom_map or id2 not in atom_map:
|
|
391
|
+
continue
|
|
392
|
+
idx1, idx2 = atom_map[id1] + 1, atom_map[id2] + 1
|
|
393
|
+
# Bond order may be a float (1.5 = aromatic); V2000 uses code 4.
|
|
394
|
+
order_val = float(bond["order"])
|
|
395
|
+
order = 4 if order_val == 1.5 else int(order_val)
|
|
380
396
|
stereo_code = 0
|
|
381
397
|
bond_stereo = bond.get("stereo", 0)
|
|
382
398
|
if bond_stereo == 1:
|
|
@@ -384,8 +400,14 @@ class MolecularData:
|
|
|
384
400
|
elif bond_stereo == 2:
|
|
385
401
|
stereo_code = 6
|
|
386
402
|
|
|
387
|
-
|
|
403
|
+
bond_lines.append(
|
|
404
|
+
f"{idx1:3d}{idx2:3d}{order:3d}{stereo_code:3d} 0 0 0\n"
|
|
405
|
+
)
|
|
388
406
|
|
|
407
|
+
mol_block = "\n MoleditPy\n\n"
|
|
408
|
+
mol_block += f"{len(atom_lines):3d}{len(bond_lines):3d} 0 0 0 0 0 0 0 0999 V2000\n"
|
|
409
|
+
mol_block += "".join(atom_lines)
|
|
410
|
+
mol_block += "".join(bond_lines)
|
|
389
411
|
mol_block += "M END\n"
|
|
390
412
|
return mol_block
|
|
391
413
|
|
moleditpy_linux/main.py
CHANGED
|
@@ -17,7 +17,7 @@ import logging.handlers
|
|
|
17
17
|
import os
|
|
18
18
|
import sys
|
|
19
19
|
import argparse
|
|
20
|
-
from typing import Any
|
|
20
|
+
from typing import Any, Optional
|
|
21
21
|
|
|
22
22
|
from .utils.constants import VERSION
|
|
23
23
|
|
|
@@ -39,8 +39,9 @@ _QT_LOG_LEVEL = {
|
|
|
39
39
|
_DOWNGRADED_QT_PATTERNS = ("Retrying to obtain clipboard",)
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def _qt_message_handler(mode: QtMsgType, _context: Any, message: str) -> None:
|
|
42
|
+
def _qt_message_handler(mode: QtMsgType, _context: Any, message: Optional[str]) -> None:
|
|
43
43
|
"""Route Qt log messages to Python logging, downgrading known noisy warnings."""
|
|
44
|
+
message = message or ""
|
|
44
45
|
for pattern in _DOWNGRADED_QT_PATTERNS:
|
|
45
46
|
if pattern in message:
|
|
46
47
|
logging.debug("Qt: %s", message)
|
|
@@ -109,7 +110,9 @@ def main() -> None:
|
|
|
109
110
|
setup_logging()
|
|
110
111
|
|
|
111
112
|
if sys.platform == "win32":
|
|
112
|
-
|
|
113
|
+
# Taskbar grouping ID follows the major version
|
|
114
|
+
major = VERSION.split(".")[0] if VERSION and VERSION != "Unknown" else "0"
|
|
115
|
+
myappid = f"hyoko.moleditpy_linux.{major}"
|
|
113
116
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
|
114
117
|
|
|
115
118
|
parser = argparse.ArgumentParser(
|
|
@@ -201,6 +204,6 @@ def main() -> None:
|
|
|
201
204
|
except (
|
|
202
205
|
Exception
|
|
203
206
|
): # [COSMETIC] Icon refresh is best-effort; Qt timing errors are non-fatal.
|
|
204
|
-
|
|
207
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
205
208
|
|
|
206
209
|
sys.exit(app.exec())
|
|
@@ -133,7 +133,7 @@ class PluginContext:
|
|
|
133
133
|
|
|
134
134
|
def get_selected_atom_indices(self) -> List[int]:
|
|
135
135
|
"""
|
|
136
|
-
Returns a list of RDKit atom indices currently selected in the 2D
|
|
136
|
+
Returns a list of RDKit atom indices currently selected in the 2D view.
|
|
137
137
|
Note: RDKit indices are returned, which map to the current_mol.
|
|
138
138
|
"""
|
|
139
139
|
return self._manager.get_selected_atom_indices() # type: ignore[no-any-return]
|
|
@@ -397,7 +397,7 @@ class PluginContext:
|
|
|
397
397
|
if fn:
|
|
398
398
|
fn()
|
|
399
399
|
except Exception:
|
|
400
|
-
|
|
400
|
+
logging.debug("mark_project_modified failed", exc_info=True)
|
|
401
401
|
|
|
402
402
|
def refresh_ui(self) -> None:
|
|
403
403
|
"""Refresh all UI state after modifying the molecule.
|
|
@@ -151,21 +151,24 @@ class PluginManager:
|
|
|
151
151
|
# Smart Extraction: Check if ZIP has a single top-level folder
|
|
152
152
|
# Fix for paths with backslashes on Windows if zip was created on Windows
|
|
153
153
|
roots = set()
|
|
154
|
+
root_is_dir = False
|
|
154
155
|
for name in zf.namelist():
|
|
155
156
|
# Normalize path separators to forward slash for consistent check
|
|
156
157
|
name = name.replace("\\", "/")
|
|
157
158
|
parts = name.split("/")
|
|
158
159
|
if parts[0]:
|
|
159
160
|
roots.add(parts[0])
|
|
161
|
+
# Sub-entries prove the root is a folder
|
|
162
|
+
if len(parts) > 1:
|
|
163
|
+
root_is_dir = True
|
|
160
164
|
|
|
161
|
-
is_nested = len(roots) == 1
|
|
165
|
+
is_nested = len(roots) == 1 and root_is_dir
|
|
162
166
|
|
|
163
167
|
if is_nested:
|
|
164
168
|
# Case A: ZIP contains a single folder (e.g. MyPlugin/init.py)
|
|
165
169
|
top_folder = list(roots)[0]
|
|
166
170
|
|
|
167
|
-
#
|
|
168
|
-
# otherwise we pollute the plugin_dir root.
|
|
171
|
+
# A root named __init__.py always needs a wrapper folder
|
|
169
172
|
if top_folder == "__init__.py":
|
|
170
173
|
is_nested = False
|
|
171
174
|
|
|
@@ -249,8 +252,10 @@ class PluginManager:
|
|
|
249
252
|
return []
|
|
250
253
|
|
|
251
254
|
for root, dirs, files in os.walk(self.plugin_dir):
|
|
252
|
-
# Exclude hidden directories
|
|
253
|
-
dirs[:] = [
|
|
255
|
+
# Exclude hidden and dunder directories (e.g. .git, __pycache__)
|
|
256
|
+
dirs[:] = [
|
|
257
|
+
d for d in dirs if not d.startswith("__") and not d.startswith(".")
|
|
258
|
+
]
|
|
254
259
|
|
|
255
260
|
# [Check] Is current dir a package (plugin body)?
|
|
256
261
|
if "__init__.py" in files:
|
|
@@ -333,7 +338,7 @@ class PluginManager:
|
|
|
333
338
|
sys.modules[spec.name] = module
|
|
334
339
|
|
|
335
340
|
# Inject category info
|
|
336
|
-
module.PLUGIN_CATEGORY = category
|
|
341
|
+
module.PLUGIN_CATEGORY = category # type: ignore[attr-defined]
|
|
337
342
|
|
|
338
343
|
spec.loader.exec_module(module)
|
|
339
344
|
|
|
@@ -480,12 +485,18 @@ class PluginManager:
|
|
|
480
485
|
self, plugin_name: str, method_name: str, callback: Callable
|
|
481
486
|
) -> None:
|
|
482
487
|
"""Register a named 3D optimization method provided by a plugin."""
|
|
483
|
-
|
|
484
|
-
self.optimization_methods[
|
|
488
|
+
method_key = method_name.upper()
|
|
489
|
+
self.optimization_methods[method_key] = {
|
|
485
490
|
"plugin": plugin_name,
|
|
486
491
|
"callback": callback,
|
|
487
492
|
"label": method_name,
|
|
488
493
|
}
|
|
494
|
+
if hasattr(self.main_window, "init_manager") and hasattr(
|
|
495
|
+
self.main_window.init_manager, "add_optimization_method"
|
|
496
|
+
):
|
|
497
|
+
self.main_window.init_manager.add_optimization_method(
|
|
498
|
+
method_name, method_key
|
|
499
|
+
)
|
|
489
500
|
|
|
490
501
|
def register_file_opener(
|
|
491
502
|
self, plugin_name: str, extension: str, callback: Callable, priority: int = 0
|
|
@@ -608,10 +619,6 @@ class PluginManager:
|
|
|
608
619
|
selected_indices.append(i)
|
|
609
620
|
except (RuntimeError, ValueError, TypeError):
|
|
610
621
|
continue
|
|
611
|
-
except (
|
|
612
|
-
ImportError
|
|
613
|
-
): # [OPTIONAL DEP] importlib.metadata unavailable (<3.8); silently skip.
|
|
614
|
-
pass
|
|
615
622
|
except (RuntimeError, AttributeError) as e:
|
|
616
623
|
logging.error(f"Error retrieving selected atom indices: {e}")
|
|
617
624
|
|
|
@@ -660,15 +667,15 @@ class PluginManager:
|
|
|
660
667
|
if isinstance(target, ast.Name):
|
|
661
668
|
# Helper to extract value
|
|
662
669
|
val = None
|
|
663
|
-
if node.value: # AnnAssign might presumably not have value? (though usually does for module globals)
|
|
664
|
-
if isinstance(node.value, ast.Constant): # Py3.8+
|
|
665
|
-
val = node.value.value
|
|
666
|
-
elif isinstance(node.value, ast.Tuple):
|
|
670
|
+
if node.value: # type: ignore[attr-defined] # AnnAssign might presumably not have value? (though usually does for module globals)
|
|
671
|
+
if isinstance(node.value, ast.Constant): # type: ignore[attr-defined] # Py3.8+
|
|
672
|
+
val = node.value.value # type: ignore[attr-defined]
|
|
673
|
+
elif isinstance(node.value, ast.Tuple): # type: ignore[attr-defined]
|
|
667
674
|
# Handle version tuples e.g. (1, 0, 0)
|
|
668
675
|
try:
|
|
669
676
|
# Extract simple constants from tuple
|
|
670
677
|
elts = []
|
|
671
|
-
for elt in node.value.elts:
|
|
678
|
+
for elt in node.value.elts: # type: ignore[attr-defined]
|
|
672
679
|
if isinstance(elt, ast.Constant):
|
|
673
680
|
elts.append(elt.value)
|
|
674
681
|
val = ".".join(map(str, elts))
|
|
@@ -678,7 +685,9 @@ class PluginManager:
|
|
|
678
685
|
ValueError,
|
|
679
686
|
TypeError,
|
|
680
687
|
): # [AST PARSE] Complex/unexpected AST node shapes during metadata extraction; skip gracefully.
|
|
681
|
-
|
|
688
|
+
logging.debug(
|
|
689
|
+
"Suppressed non-critical error", exc_info=True
|
|
690
|
+
)
|
|
682
691
|
|
|
683
692
|
if val is not None:
|
|
684
693
|
if target.id == "PLUGIN_NAME":
|
|
@@ -712,7 +721,7 @@ class PluginManager:
|
|
|
712
721
|
elif hasattr(ast, "Str") and isinstance(
|
|
713
722
|
node.value, getattr(ast, "Str", type(None))
|
|
714
723
|
):
|
|
715
|
-
val = node.value.s
|
|
724
|
+
val = node.value.s # type: ignore[attr-defined]
|
|
716
725
|
|
|
717
726
|
if val and not info["description"]:
|
|
718
727
|
info["description"] = val.strip().split("\n")[0]
|
|
@@ -37,8 +37,8 @@ class PluginManagerWindow(QDialog):
|
|
|
37
37
|
|
|
38
38
|
def __init__(self, plugin_manager: Any, parent: Optional[QWidget] = None) -> None:
|
|
39
39
|
super().__init__(parent)
|
|
40
|
-
self.btn_remove = None
|
|
41
|
-
self.table = None
|
|
40
|
+
self.btn_remove: Any = None
|
|
41
|
+
self.table: Any = None
|
|
42
42
|
self.plugin_manager = plugin_manager
|
|
43
43
|
self.setWindowTitle("Plugin Manager")
|
|
44
44
|
self.resize(800, 500)
|
|
@@ -241,7 +241,7 @@ class PluginManagerWindow(QDialog):
|
|
|
241
241
|
"""Accept drag events carrying file URLs."""
|
|
242
242
|
if event is None:
|
|
243
243
|
return
|
|
244
|
-
if event.mimeData().hasUrls():
|
|
244
|
+
if event.mimeData().hasUrls(): # type: ignore[union-attr]
|
|
245
245
|
event.accept()
|
|
246
246
|
else:
|
|
247
247
|
event.ignore()
|
|
@@ -252,7 +252,7 @@ class PluginManagerWindow(QDialog):
|
|
|
252
252
|
return
|
|
253
253
|
files_installed = []
|
|
254
254
|
errors = []
|
|
255
|
-
for url in event.mimeData().urls():
|
|
255
|
+
for url in event.mimeData().urls(): # type: ignore[union-attr]
|
|
256
256
|
file_path = url.toLocalFile()
|
|
257
257
|
|
|
258
258
|
is_valid = False
|
|
@@ -34,7 +34,7 @@ class AboutDialog(QDialog):
|
|
|
34
34
|
|
|
35
35
|
def __init__(self, main_window: Any, parent: Optional[QWidget] = None) -> None:
|
|
36
36
|
super().__init__(parent)
|
|
37
|
-
self.image_label = None
|
|
37
|
+
self.image_label: Any = None
|
|
38
38
|
self.main_window = main_window
|
|
39
39
|
self.setWindowTitle("About MoleditPy")
|
|
40
40
|
self.setFixedSize(250, 300)
|
|
@@ -11,7 +11,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
|
-
from typing import TYPE_CHECKING, Literal, Optional, Sequence
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, Sequence
|
|
15
15
|
|
|
16
16
|
import numpy as np
|
|
17
17
|
|
|
@@ -66,9 +66,9 @@ class AlignPlaneDialog(BasePickingDialog):
|
|
|
66
66
|
return self._selected_atoms
|
|
67
67
|
|
|
68
68
|
@selected_atoms.setter
|
|
69
|
-
def selected_atoms(self, val:
|
|
69
|
+
def selected_atoms(self, val: Any) -> None:
|
|
70
70
|
"""Replace the selection with a new SelectionList built from val."""
|
|
71
|
-
self._selected_atoms = SelectionList(val)
|
|
71
|
+
self._selected_atoms = SelectionList(val)
|
|
72
72
|
|
|
73
73
|
def init_ui(self) -> None:
|
|
74
74
|
"""Build and lay out all widgets for the plane-alignment dialog."""
|
|
@@ -11,7 +11,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
|
-
from typing import TYPE_CHECKING, Literal, Optional, Sequence
|
|
14
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, Sequence
|
|
15
15
|
|
|
16
16
|
import numpy as np
|
|
17
17
|
|
|
@@ -74,9 +74,9 @@ class AlignmentDialog(Dialog3DPickingMixin, QDialog):
|
|
|
74
74
|
return self._selected_atoms
|
|
75
75
|
|
|
76
76
|
@selected_atoms.setter
|
|
77
|
-
def selected_atoms(self, val:
|
|
77
|
+
def selected_atoms(self, val: Any) -> None:
|
|
78
78
|
"""Replace the selection with a new SelectionList built from val."""
|
|
79
|
-
self._selected_atoms = SelectionList(val)
|
|
79
|
+
self._selected_atoms = SelectionList(val)
|
|
80
80
|
|
|
81
81
|
def init_ui(self) -> None:
|
|
82
82
|
"""Build and lay out all widgets for the alignment dialog."""
|
|
@@ -235,8 +235,8 @@ class AnalysisWindow(QDialog):
|
|
|
235
235
|
def copy_to_clipboard(self, text: str) -> None:
|
|
236
236
|
"""Copy text to the system clipboard and show a status bar message."""
|
|
237
237
|
clipboard = QApplication.clipboard()
|
|
238
|
-
clipboard.setText(text)
|
|
238
|
+
clipboard.setText(text) # type: ignore[union-attr]
|
|
239
239
|
if self.parent() and hasattr(self.parent(), "statusBar"):
|
|
240
|
-
self.parent().statusBar().showMessage(
|
|
240
|
+
self.parent().statusBar().showMessage( # type: ignore[union-attr]
|
|
241
241
|
f"Copied '{text}' to clipboard.", 2000
|
|
242
242
|
)
|
|
@@ -11,8 +11,9 @@ DOI: 10.5281/zenodo.17268532
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
|
+
import logging
|
|
14
15
|
|
|
15
|
-
from typing import TYPE_CHECKING, Optional, Sequence
|
|
16
|
+
from typing import Any, TYPE_CHECKING, Optional, Sequence
|
|
16
17
|
|
|
17
18
|
from PyQt6.QtCore import Qt
|
|
18
19
|
from PyQt6.QtWidgets import (
|
|
@@ -50,18 +51,18 @@ class AngleDialog(GeometryBaseDialog):
|
|
|
50
51
|
parent: Optional[QWidget] = None,
|
|
51
52
|
) -> None:
|
|
52
53
|
super().__init__(mol, main_window, parent)
|
|
53
|
-
self._baseline_positions = None
|
|
54
|
+
self._baseline_positions: Any = None
|
|
54
55
|
self._snapshot_positions = None
|
|
55
|
-
self.angle_input = None
|
|
56
|
-
self.angle_label = None
|
|
57
|
-
self.angle_slider = None
|
|
58
|
-
self.apply_button = None
|
|
59
|
-
self.both_groups_radio = None
|
|
60
|
-
self.clear_button = None
|
|
56
|
+
self.angle_input: Any = None
|
|
57
|
+
self.angle_label: Any = None
|
|
58
|
+
self.angle_slider: Any = None
|
|
59
|
+
self.apply_button: Any = None
|
|
60
|
+
self.both_groups_radio: Any = None
|
|
61
|
+
self.clear_button: Any = None
|
|
61
62
|
self.picker_connection = None
|
|
62
|
-
self.rotate_atom_radio = None
|
|
63
|
-
self.rotate_group_radio = None
|
|
64
|
-
self.selection_label = None
|
|
63
|
+
self.rotate_atom_radio: Any = None
|
|
64
|
+
self.rotate_group_radio: Any = None
|
|
65
|
+
self.selection_label: Any = None
|
|
65
66
|
self.atom1_idx: Optional[int] = None
|
|
66
67
|
self.atom2_idx: Optional[int] = None # vertex atom
|
|
67
68
|
self.atom3_idx: Optional[int] = None
|
|
@@ -249,7 +250,7 @@ class AngleDialog(GeometryBaseDialog):
|
|
|
249
250
|
self.angle_slider.blockSignals(False)
|
|
250
251
|
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
251
252
|
# Safe defensive fallback catching AttributeError, RuntimeError, ValueError, TypeError
|
|
252
|
-
|
|
253
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
253
254
|
elif self.atom2_idx is None:
|
|
254
255
|
symbol1 = self.mol.GetAtomWithIdx(self.atom1_idx).GetSymbol()
|
|
255
256
|
self.selection_label.setText(
|
|
@@ -271,7 +272,7 @@ class AngleDialog(GeometryBaseDialog):
|
|
|
271
272
|
self.angle_slider.blockSignals(False)
|
|
272
273
|
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
273
274
|
# Safe defensive fallback catching AttributeError, RuntimeError, ValueError, TypeError
|
|
274
|
-
|
|
275
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
275
276
|
elif self.atom3_idx is None:
|
|
276
277
|
symbol1 = self.mol.GetAtomWithIdx(self.atom1_idx).GetSymbol()
|
|
277
278
|
symbol2 = self.mol.GetAtomWithIdx(self.atom2_idx).GetSymbol()
|
|
@@ -295,7 +296,7 @@ class AngleDialog(GeometryBaseDialog):
|
|
|
295
296
|
self.angle_slider.blockSignals(False)
|
|
296
297
|
except (AttributeError, RuntimeError, ValueError, TypeError):
|
|
297
298
|
# Safe defensive fallback catching AttributeError, RuntimeError, ValueError, TypeError
|
|
298
|
-
|
|
299
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
299
300
|
else:
|
|
300
301
|
symbol1 = self.mol.GetAtomWithIdx(self.atom1_idx).GetSymbol()
|
|
301
302
|
symbol2 = self.mol.GetAtomWithIdx(self.atom2_idx).GetSymbol()
|
|
@@ -333,7 +334,7 @@ class AngleDialog(GeometryBaseDialog):
|
|
|
333
334
|
self.angle_slider.setEnabled(True)
|
|
334
335
|
except (AttributeError, RuntimeError, TypeError):
|
|
335
336
|
# Safe defensive fallback catching AttributeError, RuntimeError, TypeError
|
|
336
|
-
|
|
337
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
337
338
|
|
|
338
339
|
# Add labels
|
|
339
340
|
self.add_selection_label(self.atom1_idx, "1")
|
moleditpy_linux/ui/app_state.py
CHANGED
|
@@ -18,7 +18,7 @@ import binascii
|
|
|
18
18
|
import copy
|
|
19
19
|
import logging
|
|
20
20
|
import os
|
|
21
|
-
import
|
|
21
|
+
from ..utils.suppress_log import suppress_log
|
|
22
22
|
from typing import Any, Dict, Optional, Tuple
|
|
23
23
|
|
|
24
24
|
|
|
@@ -203,7 +203,10 @@ class StateManager:
|
|
|
203
203
|
)
|
|
204
204
|
except (RuntimeError, ValueError, TypeError):
|
|
205
205
|
# Safe defensive fallback catching RuntimeError, ValueError, TypeError
|
|
206
|
-
|
|
206
|
+
logging.debug(
|
|
207
|
+
"Suppressed non-critical error",
|
|
208
|
+
exc_info=True,
|
|
209
|
+
)
|
|
207
210
|
|
|
208
211
|
# Sync 2D atoms with 3D actors
|
|
209
212
|
if self.host.compute_manager:
|
|
@@ -237,7 +240,7 @@ class StateManager:
|
|
|
237
240
|
else:
|
|
238
241
|
self.host.clear_3d_view()
|
|
239
242
|
self.host.init_manager.analysis_action.setEnabled(False)
|
|
240
|
-
self.host.init_manager.optimize_3d_button.setEnabled(False)
|
|
243
|
+
self.host.init_manager.optimize_3d_button.setEnabled(False) # type: ignore[union-attr]
|
|
241
244
|
# Disable 3D features
|
|
242
245
|
self.host.ui_manager.enable_3d_features(False)
|
|
243
246
|
|
|
@@ -246,7 +249,7 @@ class StateManager:
|
|
|
246
249
|
|
|
247
250
|
if loaded_data.get("is_3d_viewer_mode", False):
|
|
248
251
|
self.host.ui_manager.enter_3d_viewer_mode()
|
|
249
|
-
self.host.statusBar().showMessage("Project loaded in 3D Viewer Mode.")
|
|
252
|
+
self.host.statusBar().showMessage("Project loaded in 3D Viewer Mode.") # type: ignore[union-attr]
|
|
250
253
|
else:
|
|
251
254
|
self.host.ui_manager.restore_ui_for_editing()
|
|
252
255
|
# Enable 3D edit features even in 2D editor mode if 3D molecule exists
|
|
@@ -489,7 +492,7 @@ class StateManager:
|
|
|
489
492
|
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
490
493
|
# Suppress InChI generation errors during project save if RDKit lacks InChI support
|
|
491
494
|
# Safe defensive fallback catching AttributeError, RuntimeError, TypeError, ValueError
|
|
492
|
-
|
|
495
|
+
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
493
496
|
|
|
494
497
|
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
495
498
|
logging.warning("Could not generate molecular identifiers: %s", e)
|
|
@@ -543,7 +546,7 @@ class StateManager:
|
|
|
543
546
|
is_3d_mode = json_data.get("is_3d_viewer_mode", False)
|
|
544
547
|
# Restore last successful optimization method if present in file
|
|
545
548
|
method = json_data.get("last_successful_optimization_method", None)
|
|
546
|
-
with
|
|
549
|
+
with suppress_log(AttributeError, RuntimeError, TypeError):
|
|
547
550
|
self.host.set_last_successful_optimization_method(method)
|
|
548
551
|
|
|
549
552
|
# Plugin State Restoration (Phase 3)
|
|
@@ -627,7 +630,10 @@ class StateManager:
|
|
|
627
630
|
TypeError,
|
|
628
631
|
IndexError,
|
|
629
632
|
): # [RDKIT GUARD] RDKit atom property assignment may fail on invalid ids; skip silently.
|
|
630
|
-
|
|
633
|
+
logging.debug(
|
|
634
|
+
"Suppressed non-critical error",
|
|
635
|
+
exc_info=True,
|
|
636
|
+
)
|
|
631
637
|
self.host.set_3d_atom_positions(positions_3d)
|
|
632
638
|
|
|
633
639
|
# Build mapping
|
|
@@ -637,7 +643,9 @@ class StateManager:
|
|
|
637
643
|
self.host.view_3d_manager.update_atom_id_menu_state()
|
|
638
644
|
except (RuntimeError, TypeError, AttributeError):
|
|
639
645
|
# Safe defensive fallback catching RuntimeError, TypeError, AttributeError
|
|
640
|
-
|
|
646
|
+
logging.debug(
|
|
647
|
+
"Suppressed non-critical error", exc_info=True
|
|
648
|
+
)
|
|
641
649
|
|
|
642
650
|
# Always show 3D if 3D molecule exists
|
|
643
651
|
self.host.view_3d_manager.draw_molecule_3d(
|
|
@@ -659,7 +667,9 @@ class StateManager:
|
|
|
659
667
|
self.host.ui_manager.enable_3d_features(True)
|
|
660
668
|
except (RuntimeError, TypeError, AttributeError):
|
|
661
669
|
# Safe defensive fallback catching RuntimeError, TypeError, AttributeError
|
|
662
|
-
|
|
670
|
+
logging.debug(
|
|
671
|
+
"Suppressed non-critical error", exc_info=True
|
|
672
|
+
)
|
|
663
673
|
except (RuntimeError, ValueError, TypeError, binascii.Error) as e:
|
|
664
674
|
logging.error(f"Could not restore 3D molecular data: {e}")
|
|
665
675
|
self.host.set_current_molecule(None)
|