MoleditPy-linux 3.6.6__tar.gz → 4.0.1__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_linux-3.6.6 → moleditpy_linux-4.0.1}/PKG-INFO +6 -3
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/README.md +5 -2
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/pyproject.toml +1 -1
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/MoleditPy_linux.egg-info/PKG-INFO +6 -3
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/MoleditPy_linux.egg-info/SOURCES.txt +1 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/__init__.py +1 -1
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/core/molecular_data.py +6 -98
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/main.py +6 -14
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/plugins/plugin_interface.py +151 -2
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/plugins/plugin_manager.py +26 -70
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/plugins/plugin_manager_window.py +3 -1
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/about_dialog.py +2 -4
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/align_plane_dialog.py +3 -9
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/alignment_dialog.py +5 -14
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/analysis_window.py +6 -4
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/angle_dialog.py +22 -14
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/app_state.py +132 -340
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/atom_item.py +16 -44
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/atom_picking.py +6 -8
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/base_picking_dialog.py +5 -18
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/bond_item.py +58 -88
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/bond_length_dialog.py +17 -6
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/calculation_worker.py +35 -69
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/color_settings_dialog.py +25 -89
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/compute_logic.py +94 -139
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +12 -8
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/custom_interactor_style.py +100 -98
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +19 -16
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/dialog_logic.py +30 -67
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/dihedral_dialog.py +20 -14
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/edit_3d_logic.py +46 -83
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/edit_actions_logic.py +147 -244
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/export_logic.py +16 -40
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/geometry_base_dialog.py +2 -13
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/io_logic.py +91 -155
- moleditpy_linux-4.0.1/src/moleditpy_linux/ui/main_window.py +318 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/main_window_init.py +214 -631
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/mirror_dialog.py +3 -2
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/molecular_scene_handler.py +67 -99
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/molecule_scene.py +218 -114
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/move_group_dialog.py +25 -22
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +7 -12
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/periodic_table_dialog.py +1 -4
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/planarize_dialog.py +6 -4
- moleditpy_linux-4.0.1/src/moleditpy_linux/ui/plugin_menu_manager.py +463 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/settings_dialog.py +16 -32
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +6 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +7 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +8 -2
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +4 -4
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/string_importers.py +2 -14
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/template_preview_item.py +1 -4
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/template_preview_view.py +5 -4
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/translation_dialog.py +1 -4
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/ui_manager.py +75 -115
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/user_template_dialog.py +8 -20
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/view_3d_logic.py +133 -213
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/utils/constants.py +5 -3
- moleditpy_linux-3.6.6/src/moleditpy_linux/ui/main_window.py +0 -122
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/LICENSE +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/setup.cfg +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/__main__.py +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/assets/file_icon.ico +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/assets/icon.icns +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/assets/icon.ico +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/assets/icon.png +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/core/__init__.py +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/core/mol_geometry.py +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/plugins/__init__.py +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/__init__.py +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/utils/__init__.py +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/utils/default_settings.py +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/utils/system_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.1
|
|
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
|
|
@@ -709,8 +709,7 @@ Dynamic: license-file
|
|
|
709
709
|
[](https://pypi.org/project/MoleditPy/)
|
|
710
710
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
711
711
|
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
712
|
-

|
|
712
|
+

|
|
714
713
|

|
|
715
714
|

|
|
716
715
|
[](https://pepy.tech/projects/moleditpy)
|
|
@@ -850,6 +849,8 @@ If you use this software in your work, please cite it as follows:
|
|
|
850
849
|
Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
|
|
851
850
|
```
|
|
852
851
|
|
|
852
|
+
Additionally, please cite the plugins you used.
|
|
853
|
+
|
|
853
854
|
-----
|
|
854
855
|
|
|
855
856
|
<div id="japanese"></div>
|
|
@@ -981,3 +982,5 @@ moleditpy
|
|
|
981
982
|
```
|
|
982
983
|
Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
|
|
983
984
|
```
|
|
985
|
+
|
|
986
|
+
また、使用したプラグインの引用もお願いいたします。
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
[](https://pypi.org/project/MoleditPy/)
|
|
7
7
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
8
8
|
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
9
|
-

|
|
9
|
+

|
|
11
10
|

|
|
12
11
|

|
|
13
12
|
[](https://pepy.tech/projects/moleditpy)
|
|
@@ -147,6 +146,8 @@ If you use this software in your work, please cite it as follows:
|
|
|
147
146
|
Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
|
|
148
147
|
```
|
|
149
148
|
|
|
149
|
+
Additionally, please cite the plugins you used.
|
|
150
|
+
|
|
150
151
|
-----
|
|
151
152
|
|
|
152
153
|
<div id="japanese"></div>
|
|
@@ -278,3 +279,5 @@ moleditpy
|
|
|
278
279
|
```
|
|
279
280
|
Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
|
|
280
281
|
```
|
|
282
|
+
|
|
283
|
+
また、使用したプラグインの引用もお願いいたします。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.1
|
|
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
|
|
@@ -709,8 +709,7 @@ Dynamic: license-file
|
|
|
709
709
|
[](https://pypi.org/project/MoleditPy/)
|
|
710
710
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
|
711
711
|
[](https://github.com/HiroYokoyama/python_molecular_editor/actions)
|
|
712
|
-

|
|
712
|
+

|
|
714
713
|

|
|
715
714
|

|
|
716
715
|
[](https://pepy.tech/projects/moleditpy)
|
|
@@ -850,6 +849,8 @@ If you use this software in your work, please cite it as follows:
|
|
|
850
849
|
Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
|
|
851
850
|
```
|
|
852
851
|
|
|
852
|
+
Additionally, please cite the plugins you used.
|
|
853
|
+
|
|
853
854
|
-----
|
|
854
855
|
|
|
855
856
|
<div id="japanese"></div>
|
|
@@ -981,3 +982,5 @@ moleditpy
|
|
|
981
982
|
```
|
|
982
983
|
Yokoyama, H. (2026). MoleditPy — A Python-based molecular editing software. Zenodo. https://doi.org/10.5281/zenodo.17268532
|
|
983
984
|
```
|
|
985
|
+
|
|
986
|
+
また、使用したプラグインの引用もお願いいたします。
|
|
@@ -56,6 +56,7 @@ src/moleditpy_linux/ui/move_group_dialog.py
|
|
|
56
56
|
src/moleditpy_linux/ui/move_selected_atoms_dialog.py
|
|
57
57
|
src/moleditpy_linux/ui/periodic_table_dialog.py
|
|
58
58
|
src/moleditpy_linux/ui/planarize_dialog.py
|
|
59
|
+
src/moleditpy_linux/ui/plugin_menu_manager.py
|
|
59
60
|
src/moleditpy_linux/ui/settings_dialog.py
|
|
60
61
|
src/moleditpy_linux/ui/string_importers.py
|
|
61
62
|
src/moleditpy_linux/ui/template_preview_item.py
|
|
@@ -15,10 +15,7 @@ import logging
|
|
|
15
15
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
16
16
|
from rdkit import Chem
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
from ..utils.constants import ANGSTROM_PER_PIXEL
|
|
20
|
-
except ImportError:
|
|
21
|
-
from moleditpy_linux.utils.constants import ANGSTROM_PER_PIXEL
|
|
18
|
+
from ..utils.constants import ANGSTROM_PER_PIXEL
|
|
22
19
|
|
|
23
20
|
|
|
24
21
|
class PointTuple(tuple):
|
|
@@ -35,12 +32,12 @@ class MolecularData:
|
|
|
35
32
|
atoms: Dict[int, Dict[str, Any]]
|
|
36
33
|
bonds: Dict[Tuple[int, int], Dict[str, Any]]
|
|
37
34
|
adjacency_list: Dict[int, List[int]]
|
|
38
|
-
|
|
35
|
+
next_atom_id: int
|
|
39
36
|
|
|
40
37
|
def __init__(self) -> None:
|
|
41
38
|
self.atoms = {}
|
|
42
39
|
self.bonds = {}
|
|
43
|
-
self.
|
|
40
|
+
self.next_atom_id = 0
|
|
44
41
|
self.adjacency_list = {}
|
|
45
42
|
|
|
46
43
|
def add_atom(
|
|
@@ -50,7 +47,7 @@ class MolecularData:
|
|
|
50
47
|
charge: int = 0,
|
|
51
48
|
radical: int = 0,
|
|
52
49
|
) -> int:
|
|
53
|
-
atom_id = self.
|
|
50
|
+
atom_id = self.next_atom_id
|
|
54
51
|
# Internalize position as raw floats to decouple from UI types (QPointF)
|
|
55
52
|
if hasattr(pos, "x") and hasattr(pos, "y"):
|
|
56
53
|
raw_pos = PointTuple((float(pos.x()), float(pos.y())))
|
|
@@ -60,12 +57,11 @@ class MolecularData:
|
|
|
60
57
|
self.atoms[atom_id] = {
|
|
61
58
|
"symbol": symbol,
|
|
62
59
|
"pos": raw_pos,
|
|
63
|
-
"item": None,
|
|
64
60
|
"charge": charge,
|
|
65
61
|
"radical": radical,
|
|
66
62
|
}
|
|
67
63
|
self.adjacency_list[atom_id] = []
|
|
68
|
-
self.
|
|
64
|
+
self.next_atom_id += 1
|
|
69
65
|
return atom_id
|
|
70
66
|
|
|
71
67
|
def set_atom_pos(self, atom_id: int, pos: Union[Any, Tuple[float, float]]) -> None:
|
|
@@ -87,7 +83,7 @@ class MolecularData:
|
|
|
87
83
|
if id1 > id2:
|
|
88
84
|
id1, id2 = id2, id1
|
|
89
85
|
|
|
90
|
-
bond_data = {"order": order, "stereo": stereo
|
|
86
|
+
bond_data = {"order": order, "stereo": stereo}
|
|
91
87
|
|
|
92
88
|
# Check if it's a new bond, considering reverse direction keys.
|
|
93
89
|
is_new_bond = (id1, id2) not in self.bonds and (id2, id1) not in self.bonds
|
|
@@ -321,94 +317,6 @@ class MolecularData:
|
|
|
321
317
|
Chem.AssignStereochemistry(final_mol, cleanIt=False, force=False)
|
|
322
318
|
return final_mol
|
|
323
319
|
|
|
324
|
-
def update_ring_info_2d(self) -> None:
|
|
325
|
-
"""Update is_in_ring and ring_center for all BondItems based on 2D topology."""
|
|
326
|
-
if not self.atoms or not self.bonds:
|
|
327
|
-
return
|
|
328
|
-
|
|
329
|
-
# 1. Generate RDKit molecule for topology analysis
|
|
330
|
-
mol = self.to_rdkit_mol(use_2d_stereo=False)
|
|
331
|
-
if not mol:
|
|
332
|
-
# Fallback: reset all ring info if molecule generation fails
|
|
333
|
-
for bond_data in self.bonds.values():
|
|
334
|
-
bond_item = bond_data.get("item")
|
|
335
|
-
if bond_item:
|
|
336
|
-
bond_item.is_in_ring = False
|
|
337
|
-
bond_item.ring_center = None
|
|
338
|
-
return
|
|
339
|
-
|
|
340
|
-
# 2. Extract ring information
|
|
341
|
-
ring_info = mol.GetRingInfo()
|
|
342
|
-
atom_rings = ring_info.AtomRings()
|
|
343
|
-
bond_rings = ring_info.BondRings()
|
|
344
|
-
|
|
345
|
-
# 3. Create mapping from RDKit atom index to editor atom item
|
|
346
|
-
rdkit_idx_to_item = {}
|
|
347
|
-
for atom in mol.GetAtoms():
|
|
348
|
-
if atom.HasProp("_original_atom_id"):
|
|
349
|
-
orig_id = atom.GetIntProp("_original_atom_id")
|
|
350
|
-
if orig_id in self.atoms:
|
|
351
|
-
rdkit_idx_to_item[atom.GetIdx()] = self.atoms[orig_id]["item"]
|
|
352
|
-
|
|
353
|
-
# 4. Map RDKit bond index to editor bond item
|
|
354
|
-
rdkit_bond_idx_to_item = {}
|
|
355
|
-
for bidx, rdkit_bond in enumerate(mol.GetBonds()):
|
|
356
|
-
a1_idx = rdkit_bond.GetBeginAtomIdx()
|
|
357
|
-
a2_idx = rdkit_bond.GetEndAtomIdx()
|
|
358
|
-
if a1_idx in rdkit_idx_to_item and a2_idx in rdkit_idx_to_item:
|
|
359
|
-
# Find corresponding editor bond item
|
|
360
|
-
# This is slightly expensive but done once per update
|
|
361
|
-
item1 = rdkit_idx_to_item[a1_idx]
|
|
362
|
-
item2 = rdkit_idx_to_item[a2_idx]
|
|
363
|
-
id1, id2 = item1.atom_id, item2.atom_id
|
|
364
|
-
key = (id1, id2) if (id1, id2) in self.bonds else (id2, id1)
|
|
365
|
-
if key in self.bonds:
|
|
366
|
-
rdkit_bond_idx_to_item[bidx] = self.bonds[key].get("item")
|
|
367
|
-
|
|
368
|
-
# 5. Initialize/Reset all bond items and track best ring size
|
|
369
|
-
bond_to_best_size: Dict[
|
|
370
|
-
int, int
|
|
371
|
-
] = {} # bond_item_id -> smallest_ring_size_found
|
|
372
|
-
for bond_data in self.bonds.values():
|
|
373
|
-
bond_item = bond_data.get("item")
|
|
374
|
-
if bond_item:
|
|
375
|
-
bond_item.is_in_ring = False
|
|
376
|
-
bond_item.ring_center = None
|
|
377
|
-
|
|
378
|
-
# 6. Apply ring information
|
|
379
|
-
for a_ring, b_ring in zip(atom_rings, bond_rings):
|
|
380
|
-
ring_size = len(a_ring)
|
|
381
|
-
# Calculate ring center (geometric mean of atom positions)
|
|
382
|
-
positions = []
|
|
383
|
-
for aidx in a_ring:
|
|
384
|
-
item = rdkit_idx_to_item.get(aidx)
|
|
385
|
-
if item and hasattr(item, "pos"):
|
|
386
|
-
pos = item.pos()
|
|
387
|
-
if pos is not None:
|
|
388
|
-
positions.append(pos)
|
|
389
|
-
|
|
390
|
-
if not positions:
|
|
391
|
-
continue
|
|
392
|
-
|
|
393
|
-
center_x = sum(p.x() for p in positions) / len(positions)
|
|
394
|
-
center_y = sum(p.y() for p in positions) / len(positions)
|
|
395
|
-
ring_center = (center_x, center_y) # Use tuple (x, y) instead of QPointF
|
|
396
|
-
|
|
397
|
-
# Update all bonds in this ring
|
|
398
|
-
for bidx in b_ring:
|
|
399
|
-
bond_item = rdkit_bond_idx_to_item.get(bidx)
|
|
400
|
-
if bond_item:
|
|
401
|
-
bond_item.is_in_ring = True
|
|
402
|
-
# Explicitly prioritize smaller rings for double bond shift logic.
|
|
403
|
-
# This ensures the double bond is drawn inside the smaller ring in fused systems.
|
|
404
|
-
item_id = id(bond_item)
|
|
405
|
-
if (
|
|
406
|
-
item_id not in bond_to_best_size
|
|
407
|
-
or ring_size < bond_to_best_size[item_id]
|
|
408
|
-
):
|
|
409
|
-
bond_item.ring_center = ring_center
|
|
410
|
-
bond_to_best_size[item_id] = ring_size
|
|
411
|
-
|
|
412
320
|
def to_mol_block(self) -> Optional[str]:
|
|
413
321
|
mol = self.to_rdkit_mol()
|
|
414
322
|
if mol:
|
|
@@ -17,23 +17,13 @@ import logging
|
|
|
17
17
|
import os
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
from .utils.constants import VERSION
|
|
22
|
-
except ImportError:
|
|
23
|
-
# Add the parent directory (src) to sys.path so 'moleditpy_linux.*' imports work
|
|
24
|
-
src_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
25
|
-
if src_dir not in sys.path:
|
|
26
|
-
sys.path.insert(0, src_dir)
|
|
27
|
-
from moleditpy_linux.utils.constants import VERSION
|
|
20
|
+
from .utils.constants import VERSION
|
|
28
21
|
|
|
29
22
|
# VERSION is resolved above (before Qt) so --version works without launching the app.
|
|
30
23
|
|
|
31
24
|
from PyQt6.QtWidgets import QApplication
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
from .ui.main_window import MainWindow
|
|
35
|
-
except ImportError:
|
|
36
|
-
from moleditpy_linux.ui.main_window import MainWindow
|
|
26
|
+
from .ui.main_window import MainWindow
|
|
37
27
|
|
|
38
28
|
|
|
39
29
|
def setup_logging() -> None:
|
|
@@ -103,7 +93,7 @@ def main() -> None:
|
|
|
103
93
|
from .plugins.plugin_manager import PluginManager
|
|
104
94
|
|
|
105
95
|
pm = PluginManager()
|
|
106
|
-
sha256 = pm.
|
|
96
|
+
sha256 = pm.compute_sha256(plugin_path)
|
|
107
97
|
|
|
108
98
|
# Extract metadata
|
|
109
99
|
metadata_file = plugin_path
|
|
@@ -155,7 +145,9 @@ def main() -> None:
|
|
|
155
145
|
from PyQt6.QtCore import QTimer
|
|
156
146
|
|
|
157
147
|
QTimer.singleShot(100, lambda: window.setWindowIcon(window.windowIcon()))
|
|
158
|
-
except
|
|
148
|
+
except (
|
|
149
|
+
Exception
|
|
150
|
+
): # [COSMETIC] Icon refresh is best-effort; Qt timing errors are non-fatal.
|
|
159
151
|
pass
|
|
160
152
|
|
|
161
153
|
sys.exit(app.exec())
|
{moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/plugins/plugin_interface.py
RENAMED
|
@@ -164,6 +164,24 @@ class PluginContext:
|
|
|
164
164
|
"""
|
|
165
165
|
self._manager.show_status_message(message, timeout)
|
|
166
166
|
|
|
167
|
+
def rebuild_menus(self) -> None:
|
|
168
|
+
"""Rebuild plugin-managed menus and toolbars to apply changes immediately (Public API)."""
|
|
169
|
+
if hasattr(self._manager, "rebuild_plugin_menus"):
|
|
170
|
+
self._manager.rebuild_plugin_menus()
|
|
171
|
+
|
|
172
|
+
def enter_3d_viewer_mode(self) -> None:
|
|
173
|
+
"""Switch the application UI layout to 3D viewer mode (Public API)."""
|
|
174
|
+
mw = self.get_main_window()
|
|
175
|
+
if mw is not None and hasattr(mw, "ui_manager"):
|
|
176
|
+
if hasattr(mw.ui_manager, "enter_3d_viewer_mode"):
|
|
177
|
+
mw.ui_manager.enter_3d_viewer_mode()
|
|
178
|
+
elif hasattr(mw.ui_manager, "enter_3d_viewer_ui_mode"):
|
|
179
|
+
mw.ui_manager.enter_3d_viewer_ui_mode()
|
|
180
|
+
|
|
181
|
+
def enter_3d_mode(self) -> None:
|
|
182
|
+
"""Switch UI layout to 3D viewer mode. Alias for enter_3d_viewer_mode."""
|
|
183
|
+
self.enter_3d_viewer_mode()
|
|
184
|
+
|
|
167
185
|
@property
|
|
168
186
|
def current_mol(self) -> Any:
|
|
169
187
|
"""
|
|
@@ -310,8 +328,12 @@ class PluginContext:
|
|
|
310
328
|
|
|
311
329
|
def register_3d_context_menu(self, callback: Callable, label: str) -> None:
|
|
312
330
|
"""Deprecated: This method does nothing. Kept for backward compatibility."""
|
|
313
|
-
|
|
314
|
-
|
|
331
|
+
import warnings
|
|
332
|
+
|
|
333
|
+
warnings.warn(
|
|
334
|
+
f"Plugin '{self._plugin_name}' uses deprecated 'register_3d_context_menu'. This API has been removed.",
|
|
335
|
+
category=DeprecationWarning,
|
|
336
|
+
stacklevel=2,
|
|
315
337
|
)
|
|
316
338
|
|
|
317
339
|
def register_3d_style(
|
|
@@ -371,6 +393,133 @@ class PluginContext:
|
|
|
371
393
|
if hasattr(mw.init_manager, "settings_dirty"):
|
|
372
394
|
mw.init_manager.settings_dirty = True
|
|
373
395
|
|
|
396
|
+
def mark_project_modified(self) -> None:
|
|
397
|
+
"""Mark the current project as having unsaved changes and update the window title."""
|
|
398
|
+
mw = self.get_main_window()
|
|
399
|
+
if mw and hasattr(mw, "state_manager"):
|
|
400
|
+
try:
|
|
401
|
+
mw.state_manager.has_unsaved_changes = True
|
|
402
|
+
if hasattr(mw.state_manager, "update_window_title"):
|
|
403
|
+
mw.state_manager.update_window_title()
|
|
404
|
+
except Exception:
|
|
405
|
+
pass
|
|
406
|
+
|
|
407
|
+
def refresh_ui(self) -> None:
|
|
408
|
+
"""Refresh all UI state after modifying the molecule.
|
|
409
|
+
|
|
410
|
+
Calls update_realtime_info, update_undo_redo_actions, and update_window_title
|
|
411
|
+
in one shot. Call this at the end of any edit that changes atom/bond data.
|
|
412
|
+
"""
|
|
413
|
+
mw = self.get_main_window()
|
|
414
|
+
if mw is None:
|
|
415
|
+
return
|
|
416
|
+
if hasattr(mw, "state_manager"):
|
|
417
|
+
if hasattr(mw.state_manager, "update_realtime_info"):
|
|
418
|
+
mw.state_manager.update_realtime_info()
|
|
419
|
+
if hasattr(mw.state_manager, "update_window_title"):
|
|
420
|
+
mw.state_manager.update_window_title()
|
|
421
|
+
if hasattr(mw, "edit_actions_manager") and hasattr(
|
|
422
|
+
mw.edit_actions_manager, "update_undo_redo_actions"
|
|
423
|
+
):
|
|
424
|
+
mw.edit_actions_manager.update_undo_redo_actions()
|
|
425
|
+
|
|
426
|
+
def fit_3d_view(self) -> None:
|
|
427
|
+
"""Zoom and re-center the 3D viewport to fit the current molecule."""
|
|
428
|
+
mw = self.get_main_window()
|
|
429
|
+
if mw and hasattr(mw, "view_3d_manager"):
|
|
430
|
+
fit = getattr(mw.view_3d_manager, "fit_to_view", None)
|
|
431
|
+
if fit is not None:
|
|
432
|
+
fit()
|
|
433
|
+
|
|
434
|
+
def clear_canvas(self, push_to_undo: bool = True) -> None:
|
|
435
|
+
"""Clear the 2D editor canvas.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
push_to_undo: Whether to push the cleared state onto the undo stack
|
|
439
|
+
before clearing (default True).
|
|
440
|
+
"""
|
|
441
|
+
mw = self.get_main_window()
|
|
442
|
+
if mw and hasattr(mw, "edit_actions_manager"):
|
|
443
|
+
clear = getattr(mw.edit_actions_manager, "clear_2d_editor", None)
|
|
444
|
+
if clear is not None:
|
|
445
|
+
clear(push_to_undo=push_to_undo)
|
|
446
|
+
|
|
447
|
+
def set_3d_features_enabled(self, enabled: bool) -> None:
|
|
448
|
+
"""Enable or disable the 3D visualization panel and related UI actions.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
enabled: True to enable 3D features, False to disable.
|
|
452
|
+
"""
|
|
453
|
+
mw = self.get_main_window()
|
|
454
|
+
if mw and hasattr(mw, "ui_manager"):
|
|
455
|
+
enable = getattr(mw.ui_manager, "enable_3d_features", None)
|
|
456
|
+
if enable is not None:
|
|
457
|
+
enable(enabled)
|
|
458
|
+
|
|
459
|
+
def set_analysis_enabled(self, enabled: bool) -> None:
|
|
460
|
+
"""Enable or disable the Analysis action in the main menu.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
enabled: True to enable, False to disable.
|
|
464
|
+
"""
|
|
465
|
+
mw = self.get_main_window()
|
|
466
|
+
if mw and hasattr(mw, "init_manager"):
|
|
467
|
+
action = getattr(mw.init_manager, "analysis_action", None)
|
|
468
|
+
if action is not None:
|
|
469
|
+
action.setEnabled(enabled)
|
|
470
|
+
|
|
471
|
+
def check_chemistry_problems(self) -> None:
|
|
472
|
+
"""Trigger a chemistry validation pass and update problem flags on atoms."""
|
|
473
|
+
mw = self.get_main_window()
|
|
474
|
+
if mw and hasattr(mw, "compute_manager"):
|
|
475
|
+
check = getattr(
|
|
476
|
+
mw.compute_manager, "check_chemistry_problems_fallback", None
|
|
477
|
+
)
|
|
478
|
+
if check is not None:
|
|
479
|
+
check()
|
|
480
|
+
|
|
481
|
+
def refresh_2d_scene(self) -> None:
|
|
482
|
+
"""Force a full redraw of the 2D canvas.
|
|
483
|
+
|
|
484
|
+
Recalculates ring geometry, then repaints every atom and bond item.
|
|
485
|
+
Use this after directly manipulating scene items (e.g. via scene.create_atom)
|
|
486
|
+
without going through context.current_molecule.
|
|
487
|
+
For a lightweight Qt repaint only, use context.scene.update() instead.
|
|
488
|
+
"""
|
|
489
|
+
mw = self.get_main_window()
|
|
490
|
+
if mw and hasattr(mw, "init_manager"):
|
|
491
|
+
scene = getattr(mw.init_manager, "scene", None)
|
|
492
|
+
if scene is not None and hasattr(scene, "update_all_items"):
|
|
493
|
+
scene.update_all_items()
|
|
494
|
+
|
|
495
|
+
def load_from_smiles(self, smiles: str) -> None:
|
|
496
|
+
"""Add a molecule from a SMILES string to the 2D editor."""
|
|
497
|
+
mw = self.get_main_window()
|
|
498
|
+
if mw and hasattr(mw, "string_importer_manager"):
|
|
499
|
+
mw.string_importer_manager.load_from_smiles(smiles)
|
|
500
|
+
|
|
501
|
+
def to_xyz_block(self) -> Optional[str]:
|
|
502
|
+
"""Return the current 3D structure as an XYZ block (only element x y z lines)."""
|
|
503
|
+
mol = self.current_mol
|
|
504
|
+
if not mol:
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
try:
|
|
508
|
+
conf = mol.GetConformer()
|
|
509
|
+
num_atoms = mol.GetNumAtoms()
|
|
510
|
+
xyz_lines = []
|
|
511
|
+
|
|
512
|
+
for i in range(num_atoms):
|
|
513
|
+
pos = conf.GetAtomPosition(i)
|
|
514
|
+
symbol = mol.GetAtomWithIdx(i).GetSymbol()
|
|
515
|
+
xyz_lines.append(
|
|
516
|
+
f" {symbol:<5}{pos.x:>15.8f}{pos.y:>15.8f}{pos.z:>15.8f}"
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
return "\n".join(xyz_lines)
|
|
520
|
+
except Exception:
|
|
521
|
+
return None
|
|
522
|
+
|
|
374
523
|
|
|
375
524
|
class Plugin3DController:
|
|
376
525
|
"""Helper to manipulate the 3D scene."""
|
{moleditpy_linux-3.6.6 → moleditpy_linux-4.0.1}/src/moleditpy_linux/plugins/plugin_manager.py
RENAMED
|
@@ -25,15 +25,11 @@ from PyQt6.QtCore import QUrl
|
|
|
25
25
|
from PyQt6.QtGui import QDesktopServices
|
|
26
26
|
from PyQt6.QtWidgets import QMessageBox
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
from .plugin_interface import PluginContext
|
|
30
|
-
except ImportError:
|
|
31
|
-
# Fallback if running as script
|
|
32
|
-
from moleditpy_linux.plugins.plugin_interface import PluginContext
|
|
28
|
+
from .plugin_interface import PluginContext
|
|
33
29
|
|
|
34
30
|
|
|
35
31
|
class PluginManager:
|
|
36
|
-
def
|
|
32
|
+
def compute_sha256(self, path: str) -> str:
|
|
37
33
|
"""Computes SHA-256 for a file or a directory (concatenated hashes of all files)."""
|
|
38
34
|
if os.path.isfile(path):
|
|
39
35
|
return self._sha256_for_file(path)
|
|
@@ -66,7 +62,10 @@ class PluginManager:
|
|
|
66
62
|
hasher.update(rel_path.encode("utf-8", errors="replace"))
|
|
67
63
|
hasher.update(b"\0")
|
|
68
64
|
with open(file_path, "rb") as f:
|
|
69
|
-
|
|
65
|
+
while True:
|
|
66
|
+
chunk = f.read(8192)
|
|
67
|
+
if not chunk:
|
|
68
|
+
break
|
|
70
69
|
hasher.update(chunk)
|
|
71
70
|
hasher.update(b"\0")
|
|
72
71
|
return hasher.hexdigest()
|
|
@@ -363,35 +362,18 @@ class PluginManager:
|
|
|
363
362
|
# Pass category to context if needed, currently not storing it in context directly but could be useful
|
|
364
363
|
try:
|
|
365
364
|
module.initialize(context)
|
|
366
|
-
except
|
|
367
|
-
AttributeError,
|
|
368
|
-
RuntimeError,
|
|
369
|
-
ValueError,
|
|
370
|
-
OSError,
|
|
371
|
-
ImportError,
|
|
372
|
-
SyntaxError,
|
|
373
|
-
) as e:
|
|
374
|
-
# [BROAD EXCEPTION] Plugins have root power; catch all potential failures during init.
|
|
365
|
+
except Exception as e: # plugins have full app access; catch everything to isolate faults
|
|
375
366
|
status = f"Error (Init): {e}"
|
|
376
|
-
|
|
377
|
-
logging.error(f"Plugin {plugin_name} initialize error: {e}")
|
|
367
|
+
logging.exception("Plugin %s initialize error", plugin_name)
|
|
378
368
|
elif has_autorun:
|
|
379
369
|
try:
|
|
380
370
|
if self.main_window:
|
|
381
371
|
module.autorun(self.main_window)
|
|
382
372
|
else:
|
|
383
373
|
status = "Skipped (No MW)"
|
|
384
|
-
except
|
|
385
|
-
AttributeError,
|
|
386
|
-
RuntimeError,
|
|
387
|
-
ValueError,
|
|
388
|
-
OSError,
|
|
389
|
-
ImportError,
|
|
390
|
-
SyntaxError,
|
|
391
|
-
) as e:
|
|
374
|
+
except Exception as e: # plugins have full app access; catch everything to isolate faults
|
|
392
375
|
status = f"Error (Autorun): {e}"
|
|
393
|
-
|
|
394
|
-
print(f"Plugin {plugin_name} autorun error: {e}")
|
|
376
|
+
logging.exception("Plugin %s autorun error", plugin_name)
|
|
395
377
|
elif not has_run:
|
|
396
378
|
status = "No Entry Point"
|
|
397
379
|
|
|
@@ -409,36 +391,25 @@ class PluginManager:
|
|
|
409
391
|
}
|
|
410
392
|
)
|
|
411
393
|
|
|
412
|
-
except
|
|
413
|
-
|
|
414
|
-
RuntimeError,
|
|
415
|
-
ValueError,
|
|
416
|
-
OSError,
|
|
417
|
-
ImportError,
|
|
418
|
-
SyntaxError,
|
|
419
|
-
) as e:
|
|
420
|
-
# [BROAD EXCEPTION] Loading failures are caught to prevent a single buggy plugin from
|
|
421
|
-
# crashing the entire discovery process.
|
|
422
|
-
logging.error(f"Failed to load plugin {module_name}: {e}")
|
|
394
|
+
except Exception as e: # plugins have full app access; isolate any load failure to prevent crashing discovery
|
|
395
|
+
logging.exception("Failed to load plugin %s: %s", module_name, e)
|
|
423
396
|
|
|
424
397
|
def run_plugin(self, module: Any, main_window: Any) -> None:
|
|
425
398
|
"""Executes the plugin's run method (Legacy manual trigger)."""
|
|
426
399
|
try:
|
|
427
400
|
module.run(main_window)
|
|
428
|
-
except
|
|
429
|
-
AttributeError,
|
|
430
|
-
RuntimeError,
|
|
431
|
-
ValueError,
|
|
432
|
-
OSError,
|
|
433
|
-
ImportError,
|
|
434
|
-
SyntaxError,
|
|
435
|
-
) as e:
|
|
401
|
+
except Exception as e: # plugins have full app access; catch everything so the error dialog can show the cause
|
|
436
402
|
QMessageBox.critical(
|
|
437
403
|
main_window,
|
|
438
404
|
"Plugin Error",
|
|
439
405
|
f"Error running plugin '{getattr(module, 'PLUGIN_NAME', 'Unknown')}':\n{e}",
|
|
440
406
|
)
|
|
441
407
|
|
|
408
|
+
def rebuild_plugin_menus(self) -> None:
|
|
409
|
+
"""Rebuild all plugin menus and toolbars."""
|
|
410
|
+
if self.main_window and hasattr(self.main_window, "plugin_menu_manager"):
|
|
411
|
+
self.main_window.plugin_menu_manager.rebuild_plugin_menus()
|
|
412
|
+
|
|
442
413
|
# --- Registration Callbacks ---
|
|
443
414
|
def register_menu_action(
|
|
444
415
|
self,
|
|
@@ -614,9 +585,11 @@ class PluginManager:
|
|
|
614
585
|
selected_indices.append(i)
|
|
615
586
|
except (RuntimeError, ValueError, TypeError):
|
|
616
587
|
continue
|
|
617
|
-
except
|
|
588
|
+
except (
|
|
589
|
+
ImportError
|
|
590
|
+
): # [OPTIONAL DEP] importlib.metadata unavailable (<3.8); silently skip.
|
|
618
591
|
pass
|
|
619
|
-
except
|
|
592
|
+
except (RuntimeError, AttributeError) as e:
|
|
620
593
|
logging.error(f"Error retrieving selected atom indices: {e}")
|
|
621
594
|
|
|
622
595
|
return selected_indices
|
|
@@ -636,17 +609,9 @@ class PluginManager:
|
|
|
636
609
|
for handler in self.document_reset_handlers:
|
|
637
610
|
try:
|
|
638
611
|
handler["callback"]()
|
|
639
|
-
except
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
ValueError,
|
|
643
|
-
OSError,
|
|
644
|
-
ImportError,
|
|
645
|
-
SyntaxError,
|
|
646
|
-
) as e:
|
|
647
|
-
# [BROAD EXCEPTION] Document reset handlers are user plugins; catch all to prevent data loss.
|
|
648
|
-
logging.error(
|
|
649
|
-
f"Error in document reset handler for {handler['plugin']}: {e}"
|
|
612
|
+
except Exception as e: # plugins have full app access; catch everything to prevent data loss on document reset
|
|
613
|
+
logging.exception(
|
|
614
|
+
"Error in document reset handler for %s: %s", handler["plugin"], e
|
|
650
615
|
)
|
|
651
616
|
|
|
652
617
|
def get_plugin_info_safe(self, file_path: str) -> Dict[str, str]:
|
|
@@ -675,10 +640,6 @@ class PluginManager:
|
|
|
675
640
|
if node.value: # AnnAssign might presumably not have value? (though usually does for module globals)
|
|
676
641
|
if isinstance(node.value, ast.Constant): # Py3.8+
|
|
677
642
|
val = node.value.value
|
|
678
|
-
elif hasattr(ast, "Str") and isinstance(
|
|
679
|
-
node.value, getattr(ast, "Str", type(None))
|
|
680
|
-
): # Py3.7 and below
|
|
681
|
-
val = node.value.s
|
|
682
643
|
elif isinstance(node.value, ast.Tuple):
|
|
683
644
|
# Handle version tuples e.g. (1, 0, 0)
|
|
684
645
|
try:
|
|
@@ -687,18 +648,13 @@ class PluginManager:
|
|
|
687
648
|
for elt in node.value.elts:
|
|
688
649
|
if isinstance(elt, ast.Constant):
|
|
689
650
|
elts.append(elt.value)
|
|
690
|
-
elif hasattr(ast, "Num") and isinstance(
|
|
691
|
-
elt, getattr(ast, "Num", type(None))
|
|
692
|
-
):
|
|
693
|
-
elts.append(elt.n)
|
|
694
651
|
val = ".".join(map(str, elts))
|
|
695
652
|
except (
|
|
696
653
|
AttributeError,
|
|
697
654
|
RuntimeError,
|
|
698
655
|
ValueError,
|
|
699
656
|
TypeError,
|
|
700
|
-
):
|
|
701
|
-
# Fallback for complex AST structures during metadata extraction
|
|
657
|
+
): # [AST PARSE] Complex/unexpected AST node shapes during metadata extraction; skip gracefully.
|
|
702
658
|
pass
|
|
703
659
|
|
|
704
660
|
if val is not None:
|