MoleditPy 4.1.4__tar.gz → 4.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.
- {moleditpy-4.1.4 → moleditpy-4.2.0}/PKG-INFO +6 -6
- {moleditpy-4.1.4 → moleditpy-4.2.0}/README.md +2 -2
- {moleditpy-4.1.4 → moleditpy-4.2.0}/pyproject.toml +4 -4
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/MoleditPy.egg-info/PKG-INFO +6 -6
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/MoleditPy.egg-info/SOURCES.txt +1 -0
- moleditpy-4.2.0/src/MoleditPy.egg-info/requires.txt +11 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/core/mol_geometry.py +61 -4
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/core/molecular_data.py +35 -13
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/main.py +7 -4
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/plugins/plugin_interface.py +2 -2
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/plugins/plugin_manager.py +20 -17
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/plugins/plugin_manager_window.py +4 -4
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/about_dialog.py +1 -1
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/align_plane_dialog.py +3 -3
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/alignment_dialog.py +3 -3
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/analysis_window.py +2 -2
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/angle_dialog.py +16 -15
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/app_state.py +19 -9
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/atom_item.py +9 -6
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/atom_picking.py +3 -3
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/base_picking_dialog.py +5 -4
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/bond_item.py +22 -17
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/bond_length_dialog.py +15 -14
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/calculation_worker.py +29 -34
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/color_settings_dialog.py +11 -11
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/compute_logic.py +97 -58
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/constrained_optimization_dialog.py +1 -1
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/custom_interactor_style.py +27 -16
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/dialog_3d_picking_mixin.py +9 -5
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/dihedral_dialog.py +14 -13
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/edit_3d_logic.py +11 -10
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/edit_actions_logic.py +62 -59
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/export_logic.py +13 -7
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/geometry_base_dialog.py +2 -1
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/io_logic.py +65 -38
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/main_window.py +5 -5
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/main_window_init.py +52 -61
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/mirror_dialog.py +1 -1
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/molecular_scene_handler.py +62 -11
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/molecule_scene.py +12 -14
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/move_group_dialog.py +14 -14
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/move_selected_atoms_dialog.py +13 -13
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/periodic_table_dialog.py +1 -1
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/planarize_dialog.py +7 -5
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/plugin_menu_manager.py +7 -7
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/settings_dialog.py +8 -8
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +9 -9
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +7 -7
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +8 -8
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +15 -14
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/translation_dialog.py +11 -11
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/ui_manager.py +6 -6
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/user_template_dialog.py +5 -5
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/view_3d_logic.py +69 -65
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/zoomable_view.py +14 -13
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/utils/constants.py +11 -4
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/utils/sip_isdeleted_safe.py +1 -1
- moleditpy-4.2.0/src/moleditpy/utils/suppress_log.py +31 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/utils/system_utils.py +10 -4
- moleditpy-4.1.4/src/MoleditPy.egg-info/requires.txt +0 -11
- {moleditpy-4.1.4 → moleditpy-4.2.0}/LICENSE +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/setup.cfg +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/__init__.py +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/__main__.py +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/assets/file_icon.ico +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/assets/icon.icns +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/assets/icon.ico +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/assets/icon.png +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/core/__init__.py +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/plugins/__init__.py +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/__init__.py +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/custom_qt_interactor.py +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/dialog_logic.py +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/string_importers.py +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/template_preview_item.py +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/ui/template_preview_view.py +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/utils/__init__.py +0 -0
- {moleditpy-4.1.4 → moleditpy-4.2.0}/src/moleditpy/utils/default_settings.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.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
|
|
@@ -694,9 +694,9 @@ Requires-Python: <3.15,>=3.9
|
|
|
694
694
|
Description-Content-Type: text/markdown
|
|
695
695
|
License-File: LICENSE
|
|
696
696
|
Requires-Dist: numpy
|
|
697
|
-
Requires-Dist: pyqt6<6.10;
|
|
698
|
-
Requires-Dist: pyqt6<6.
|
|
699
|
-
Requires-Dist: pyvista<0.
|
|
697
|
+
Requires-Dist: pyqt6<6.10; python_version == "3.9"
|
|
698
|
+
Requires-Dist: pyqt6<6.12; python_version != "3.9"
|
|
699
|
+
Requires-Dist: pyvista<0.49
|
|
700
700
|
Requires-Dist: pyvistaqt<0.12
|
|
701
701
|
Requires-Dist: rdkit<2026.4
|
|
702
702
|
Requires-Dist: openbabel-wheel<3.2
|
|
@@ -774,7 +774,7 @@ This application combines a modern GUI built with **PyQt6**, powerful cheminform
|
|
|
774
774
|
* Import structures from **MOL/SDF** files or **SMILES** strings.
|
|
775
775
|
* Export 3D structures to **MOL** or **XYZ** formats, which are compatible with most DFT calculation software.
|
|
776
776
|
* Export 2D and 3D views as high-resolution PNG images.
|
|
777
|
-
*
|
|
777
|
+
* **Security note on `.pmeraw`:** the legacy `.pmeraw` project format uses Python pickle, which can execute arbitrary code when loaded. Only open `.pmeraw` files that you created yourself; prefer the JSON-based `.pmeprj` format for sharing.
|
|
778
778
|
|
|
779
779
|
### 4. Programmable & Extensible
|
|
780
780
|
|
|
@@ -909,7 +909,7 @@ Additionally, please cite the plugins you used.
|
|
|
909
909
|
* **MOL/SDF**ファイルや**SMILES**文字列から構造をインポートできます。
|
|
910
910
|
* 3D構造を**MOL**または**XYZ**形式でエクスポートでき、これらは多くのDFT計算ソフトウェアと互換性があります。
|
|
911
911
|
* 2Dおよび3Dビューを高解像度のPNG画像としてエクスポートできます。
|
|
912
|
-
*
|
|
912
|
+
* **`.pmeraw` に関するセキュリティ上の注意:** 旧形式の `.pmeraw` プロジェクトは Python の pickle を使用しており、読み込み時に任意のコードが実行される可能性があります。自分で作成した `.pmeraw` ファイルのみを開き、共有には JSON ベースの `.pmeprj` 形式を使用してください。
|
|
913
913
|
|
|
914
914
|
### 4. プログラマブルで拡張可能
|
|
915
915
|
|
|
@@ -70,7 +70,7 @@ This application combines a modern GUI built with **PyQt6**, powerful cheminform
|
|
|
70
70
|
* Import structures from **MOL/SDF** files or **SMILES** strings.
|
|
71
71
|
* Export 3D structures to **MOL** or **XYZ** formats, which are compatible with most DFT calculation software.
|
|
72
72
|
* Export 2D and 3D views as high-resolution PNG images.
|
|
73
|
-
*
|
|
73
|
+
* **Security note on `.pmeraw`:** the legacy `.pmeraw` project format uses Python pickle, which can execute arbitrary code when loaded. Only open `.pmeraw` files that you created yourself; prefer the JSON-based `.pmeprj` format for sharing.
|
|
74
74
|
|
|
75
75
|
### 4. Programmable & Extensible
|
|
76
76
|
|
|
@@ -205,7 +205,7 @@ Additionally, please cite the plugins you used.
|
|
|
205
205
|
* **MOL/SDF**ファイルや**SMILES**文字列から構造をインポートできます。
|
|
206
206
|
* 3D構造を**MOL**または**XYZ**形式でエクスポートでき、これらは多くのDFT計算ソフトウェアと互換性があります。
|
|
207
207
|
* 2Dおよび3Dビューを高解像度のPNG画像としてエクスポートできます。
|
|
208
|
-
*
|
|
208
|
+
* **`.pmeraw` に関するセキュリティ上の注意:** 旧形式の `.pmeraw` プロジェクトは Python の pickle を使用しており、読み込み時に任意のコードが実行される可能性があります。自分で作成した `.pmeraw` ファイルのみを開き、共有には JSON ベースの `.pmeprj` 形式を使用してください。
|
|
209
209
|
|
|
210
210
|
### 4. プログラマブルで拡張可能
|
|
211
211
|
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "MoleditPy"
|
|
7
7
|
|
|
8
|
-
version = "4.
|
|
8
|
+
version = "4.2.0"
|
|
9
9
|
|
|
10
10
|
license = {file = "LICENSE"}
|
|
11
11
|
|
|
@@ -34,9 +34,9 @@ classifiers = [
|
|
|
34
34
|
|
|
35
35
|
dependencies = [
|
|
36
36
|
"numpy",
|
|
37
|
-
"pyqt6 < 6.10;
|
|
38
|
-
"pyqt6 < 6.
|
|
39
|
-
"pyvista < 0.
|
|
37
|
+
"pyqt6 < 6.10; python_version == '3.9'",
|
|
38
|
+
"pyqt6 < 6.12; python_version != '3.9'",
|
|
39
|
+
"pyvista < 0.49",
|
|
40
40
|
"pyvistaqt < 0.12",
|
|
41
41
|
"rdkit < 2026.4",
|
|
42
42
|
"openbabel-wheel < 3.2"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.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
|
|
@@ -694,9 +694,9 @@ Requires-Python: <3.15,>=3.9
|
|
|
694
694
|
Description-Content-Type: text/markdown
|
|
695
695
|
License-File: LICENSE
|
|
696
696
|
Requires-Dist: numpy
|
|
697
|
-
Requires-Dist: pyqt6<6.10;
|
|
698
|
-
Requires-Dist: pyqt6<6.
|
|
699
|
-
Requires-Dist: pyvista<0.
|
|
697
|
+
Requires-Dist: pyqt6<6.10; python_version == "3.9"
|
|
698
|
+
Requires-Dist: pyqt6<6.12; python_version != "3.9"
|
|
699
|
+
Requires-Dist: pyvista<0.49
|
|
700
700
|
Requires-Dist: pyvistaqt<0.12
|
|
701
701
|
Requires-Dist: rdkit<2026.4
|
|
702
702
|
Requires-Dist: openbabel-wheel<3.2
|
|
@@ -774,7 +774,7 @@ This application combines a modern GUI built with **PyQt6**, powerful cheminform
|
|
|
774
774
|
* Import structures from **MOL/SDF** files or **SMILES** strings.
|
|
775
775
|
* Export 3D structures to **MOL** or **XYZ** formats, which are compatible with most DFT calculation software.
|
|
776
776
|
* Export 2D and 3D views as high-resolution PNG images.
|
|
777
|
-
*
|
|
777
|
+
* **Security note on `.pmeraw`:** the legacy `.pmeraw` project format uses Python pickle, which can execute arbitrary code when loaded. Only open `.pmeraw` files that you created yourself; prefer the JSON-based `.pmeprj` format for sharing.
|
|
778
778
|
|
|
779
779
|
### 4. Programmable & Extensible
|
|
780
780
|
|
|
@@ -909,7 +909,7 @@ Additionally, please cite the plugins you used.
|
|
|
909
909
|
* **MOL/SDF**ファイルや**SMILES**文字列から構造をインポートできます。
|
|
910
910
|
* 3D構造を**MOL**または**XYZ**形式でエクスポートでき、これらは多くのDFT計算ソフトウェアと互換性があります。
|
|
911
911
|
* 2Dおよび3Dビューを高解像度のPNG画像としてエクスポートできます。
|
|
912
|
-
*
|
|
912
|
+
* **`.pmeraw` に関するセキュリティ上の注意:** 旧形式の `.pmeraw` プロジェクトは Python の pickle を使用しており、読み込み時に任意のコードが実行される可能性があります。自分で作成した `.pmeraw` ファイルのみを開き、共有には JSON ベースの `.pmeprj` 形式を使用してください。
|
|
913
913
|
|
|
914
914
|
### 4. プログラマブルで拡張可能
|
|
915
915
|
|
|
@@ -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
|
|
|
@@ -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.{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
|
|
|
@@ -608,10 +613,6 @@ class PluginManager:
|
|
|
608
613
|
selected_indices.append(i)
|
|
609
614
|
except (RuntimeError, ValueError, TypeError):
|
|
610
615
|
continue
|
|
611
|
-
except (
|
|
612
|
-
ImportError
|
|
613
|
-
): # [OPTIONAL DEP] importlib.metadata unavailable (<3.8); silently skip.
|
|
614
|
-
pass
|
|
615
616
|
except (RuntimeError, AttributeError) as e:
|
|
616
617
|
logging.error(f"Error retrieving selected atom indices: {e}")
|
|
617
618
|
|
|
@@ -660,15 +661,15 @@ class PluginManager:
|
|
|
660
661
|
if isinstance(target, ast.Name):
|
|
661
662
|
# Helper to extract value
|
|
662
663
|
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):
|
|
664
|
+
if node.value: # type: ignore[attr-defined] # AnnAssign might presumably not have value? (though usually does for module globals)
|
|
665
|
+
if isinstance(node.value, ast.Constant): # type: ignore[attr-defined] # Py3.8+
|
|
666
|
+
val = node.value.value # type: ignore[attr-defined]
|
|
667
|
+
elif isinstance(node.value, ast.Tuple): # type: ignore[attr-defined]
|
|
667
668
|
# Handle version tuples e.g. (1, 0, 0)
|
|
668
669
|
try:
|
|
669
670
|
# Extract simple constants from tuple
|
|
670
671
|
elts = []
|
|
671
|
-
for elt in node.value.elts:
|
|
672
|
+
for elt in node.value.elts: # type: ignore[attr-defined]
|
|
672
673
|
if isinstance(elt, ast.Constant):
|
|
673
674
|
elts.append(elt.value)
|
|
674
675
|
val = ".".join(map(str, elts))
|
|
@@ -678,7 +679,9 @@ class PluginManager:
|
|
|
678
679
|
ValueError,
|
|
679
680
|
TypeError,
|
|
680
681
|
): # [AST PARSE] Complex/unexpected AST node shapes during metadata extraction; skip gracefully.
|
|
681
|
-
|
|
682
|
+
logging.debug(
|
|
683
|
+
"Suppressed non-critical error", exc_info=True
|
|
684
|
+
)
|
|
682
685
|
|
|
683
686
|
if val is not None:
|
|
684
687
|
if target.id == "PLUGIN_NAME":
|
|
@@ -712,7 +715,7 @@ class PluginManager:
|
|
|
712
715
|
elif hasattr(ast, "Str") and isinstance(
|
|
713
716
|
node.value, getattr(ast, "Str", type(None))
|
|
714
717
|
):
|
|
715
|
-
val = node.value.s
|
|
718
|
+
val = node.value.s # type: ignore[attr-defined]
|
|
716
719
|
|
|
717
720
|
if val and not info["description"]:
|
|
718
721
|
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")
|