moleditpy 5.0.2__tar.gz → 5.0.3__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-5.0.2 → moleditpy-5.0.3}/PKG-INFO +2 -2
- {moleditpy-5.0.2 → moleditpy-5.0.3}/pyproject.toml +2 -2
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/MoleditPy.egg-info/PKG-INFO +2 -2
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/MoleditPy.egg-info/SOURCES.txt +1 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/MoleditPy.egg-info/requires.txt +1 -1
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/core/molecular_data.py +1 -0
- moleditpy-5.0.3/src/moleditpy/core/project_validation.py +117 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/plugins/plugin_interface.py +5 -2
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/plugins/plugin_manager.py +13 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/alignment_dialog.py +10 -1
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/compute_logic.py +19 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/export_logic.py +5 -11
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/geometry_base_dialog.py +6 -3
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/io_logic.py +21 -6
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/main_window_init.py +3 -3
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/molecular_scene_handler.py +4 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/preview_molecule.py +2 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/template_preview_item.py +1 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/ui_manager.py +3 -12
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/user_template_dialog.py +3 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/utils/default_settings.py +48 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/utils/label_style.py +9 -3
- {moleditpy-5.0.2 → moleditpy-5.0.3}/COPYING +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/DISTRIBUTION.md +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/LICENSE +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/LICENSING.md +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/README.md +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/THIRD_PARTY_NOTICES.md +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/setup.cfg +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/MoleditPy.egg-info/dependency_links.txt +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/MoleditPy.egg-info/entry_points.txt +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/MoleditPy.egg-info/top_level.txt +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/__init__.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/__main__.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/assets/file_icon.ico +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/assets/icon.icns +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/assets/icon.ico +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/assets/icon.png +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/core/__init__.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/core/mol_geometry.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/core/stereo_check.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/main.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/plugins/__init__.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/plugins/plugin_manager_window.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/__init__.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/about_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/align_plane_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/analysis_window.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/angle_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/app_state.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/atom_item.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/atom_picking.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/base_picking_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/bond_item.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/bond_length_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/calculation_worker.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/chain_mixin.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/chirality_warning_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/color_settings_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/constrained_optimization_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/custom_interactor_style.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/custom_qt_interactor.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/dialog_3d_picking_mixin.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/dialog_logic.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/dihedral_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/edit_3d_logic.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/edit_actions_logic.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/main_window.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/menu_utils.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/mirror_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/molecule_scene.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/move_dialog_mixin.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/move_group_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/move_selected_atoms_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/periodic_table_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/planarize_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/plugin_menu_manager.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/sdf_record_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/settings_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/settings_tabs/__init__.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/settings_tabs/settings_2d_cleanup_tab.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/settings_tabs/settings_2d_tab.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/settings_tabs/settings_3d_tabs.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/settings_tabs/settings_labels_tab.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/settings_tabs/settings_other_tab.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/string_importers.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/template_preview_view.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/translation_dialog.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/view_3d_logic.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/zoomable_view.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/utils/__init__.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/utils/constants.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/utils/finite_float.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/utils/hit_radius.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/utils/sdf_records.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/utils/suppress_log.py +0 -0
- {moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/utils/system_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.3
|
|
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-Expression: LGPL-3.0-or-later
|
|
@@ -28,7 +28,7 @@ Requires-Dist: pyside6-essentials<6.10,>=6.9; python_version < "3.10"
|
|
|
28
28
|
Requires-Dist: pyside6-essentials<6.12,>=6.10; python_version >= "3.10"
|
|
29
29
|
Requires-Dist: pyvista<0.50
|
|
30
30
|
Requires-Dist: pyvistaqt<0.14
|
|
31
|
-
Requires-Dist: rdkit<2026.
|
|
31
|
+
Requires-Dist: rdkit<2026.10
|
|
32
32
|
Dynamic: license-file
|
|
33
33
|
|
|
34
34
|
# MoleditPy — A Python Molecular Editor
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "MoleditPy"
|
|
7
7
|
|
|
8
|
-
version = "5.0.
|
|
8
|
+
version = "5.0.3"
|
|
9
9
|
|
|
10
10
|
license = "LGPL-3.0-or-later"
|
|
11
11
|
license-files = ["LICENSE", "COPYING", "THIRD_PARTY_NOTICES.md", "DISTRIBUTION.md", "LICENSING.md"]
|
|
@@ -39,7 +39,7 @@ dependencies = [
|
|
|
39
39
|
"pyside6-essentials >= 6.10, < 6.12; python_version >= '3.10'",
|
|
40
40
|
"pyvista < 0.50",
|
|
41
41
|
"pyvistaqt < 0.14",
|
|
42
|
-
"rdkit < 2026.
|
|
42
|
+
"rdkit < 2026.10"
|
|
43
43
|
]
|
|
44
44
|
|
|
45
45
|
[project.urls]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.3
|
|
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-Expression: LGPL-3.0-or-later
|
|
@@ -28,7 +28,7 @@ Requires-Dist: pyside6-essentials<6.10,>=6.9; python_version < "3.10"
|
|
|
28
28
|
Requires-Dist: pyside6-essentials<6.12,>=6.10; python_version >= "3.10"
|
|
29
29
|
Requires-Dist: pyvista<0.50
|
|
30
30
|
Requires-Dist: pyvistaqt<0.14
|
|
31
|
-
Requires-Dist: rdkit<2026.
|
|
31
|
+
Requires-Dist: rdkit<2026.10
|
|
32
32
|
Dynamic: license-file
|
|
33
33
|
|
|
34
34
|
# MoleditPy — A Python Molecular Editor
|
|
@@ -21,6 +21,7 @@ src/moleditpy/assets/icon.png
|
|
|
21
21
|
src/moleditpy/core/__init__.py
|
|
22
22
|
src/moleditpy/core/mol_geometry.py
|
|
23
23
|
src/moleditpy/core/molecular_data.py
|
|
24
|
+
src/moleditpy/core/project_validation.py
|
|
24
25
|
src/moleditpy/core/stereo_check.py
|
|
25
26
|
src/moleditpy/plugins/__init__.py
|
|
26
27
|
src/moleditpy/plugins/plugin_interface.py
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Validate project contents before replacing the live document."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import math
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from rdkit import Chem
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _integer(value: Any, minimum: int = 0) -> int:
|
|
14
|
+
if not isinstance(value, int) or isinstance(value, bool) or value < minimum:
|
|
15
|
+
raise ValueError("Project integer field is invalid")
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _coordinate(value: Any) -> None:
|
|
20
|
+
try:
|
|
21
|
+
if isinstance(value, bool) or not math.isfinite(float(value)):
|
|
22
|
+
raise ValueError("Project coordinates must be finite numbers")
|
|
23
|
+
except OverflowError as exc:
|
|
24
|
+
raise ValueError("Project coordinate is out of range") from exc
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _validate_2d(structure: Any) -> None:
|
|
28
|
+
if not isinstance(structure, dict):
|
|
29
|
+
raise ValueError("2D structure must be an object")
|
|
30
|
+
atoms, bonds = structure.get("atoms", []), structure.get("bonds", [])
|
|
31
|
+
if not isinstance(atoms, list) or not isinstance(bonds, list):
|
|
32
|
+
raise ValueError("Project atoms and bonds must be lists")
|
|
33
|
+
ids: set[int] = set()
|
|
34
|
+
for atom in atoms:
|
|
35
|
+
atom_id = _integer(atom["id"])
|
|
36
|
+
if atom_id in ids or not isinstance(atom["symbol"], str) or not atom["symbol"]:
|
|
37
|
+
raise ValueError("Project atoms need unique IDs and element symbols")
|
|
38
|
+
ids.add(atom_id)
|
|
39
|
+
_coordinate(atom["x"])
|
|
40
|
+
_coordinate(atom["y"])
|
|
41
|
+
_integer(atom.get("charge", 0), -(2**31))
|
|
42
|
+
if atom.get("charge", 0) >= 2**31:
|
|
43
|
+
raise ValueError("Project charge is out of range")
|
|
44
|
+
_integer(atom.get("radical", 0))
|
|
45
|
+
if atom.get("rs") not in (None, "R", "S"):
|
|
46
|
+
raise ValueError("Project R/S label must be R or S")
|
|
47
|
+
seen: set[frozenset[int]] = set()
|
|
48
|
+
for bond in bonds:
|
|
49
|
+
first, second = _integer(bond["atom1"]), _integer(bond["atom2"])
|
|
50
|
+
pair = frozenset((first, second))
|
|
51
|
+
if first == second or first not in ids or second not in ids or pair in seen:
|
|
52
|
+
raise ValueError("Project bond has invalid atom references")
|
|
53
|
+
seen.add(pair)
|
|
54
|
+
if isinstance(bond["order"], bool) or bond["order"] not in (1, 1.5, 2, 3):
|
|
55
|
+
raise ValueError("Project bond order is invalid")
|
|
56
|
+
if _integer(bond.get("stereo", 0)) not in (0, 1, 2, 3, 4):
|
|
57
|
+
raise ValueError("Project bond stereochemistry is invalid")
|
|
58
|
+
minimum = max(ids) + 1 if ids else 0
|
|
59
|
+
_integer(structure.get("next_atom_id", minimum), minimum)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _validate_molecule(binary: Any) -> Chem.Mol:
|
|
63
|
+
mol = Chem.Mol(binary)
|
|
64
|
+
for conformer in mol.GetConformers():
|
|
65
|
+
if not np.isfinite(conformer.GetPositions()).all():
|
|
66
|
+
raise ValueError("Project 3D coordinates must be finite")
|
|
67
|
+
return mol
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def validate_project_data(data: dict[str, Any]) -> None:
|
|
71
|
+
"""Reject corrupt geometry without changing application or plugin state."""
|
|
72
|
+
if "2d_structure" in data:
|
|
73
|
+
_validate_2d(data["2d_structure"])
|
|
74
|
+
structure = data.get("3d_structure")
|
|
75
|
+
if structure is None:
|
|
76
|
+
return
|
|
77
|
+
if not isinstance(structure, dict):
|
|
78
|
+
raise ValueError("3D structure must be an object")
|
|
79
|
+
atoms = structure.get("atoms", [])
|
|
80
|
+
if not isinstance(atoms, list):
|
|
81
|
+
raise ValueError("3D atoms must be a list")
|
|
82
|
+
encoded = structure.get("mol_binary_base64")
|
|
83
|
+
if encoded:
|
|
84
|
+
binary: Any = base64.b64decode(encoded.encode("ascii"), validate=True)
|
|
85
|
+
mol = _validate_molecule(binary)
|
|
86
|
+
if atoms and len(atoms) != mol.GetNumAtoms():
|
|
87
|
+
raise ValueError("3D atom metadata does not match its molecule")
|
|
88
|
+
seen: set[int] = set()
|
|
89
|
+
for atom in atoms:
|
|
90
|
+
index = _integer(atom["index"])
|
|
91
|
+
if index >= len(atoms) or index in seen:
|
|
92
|
+
raise ValueError("3D atom index is invalid")
|
|
93
|
+
seen.add(index)
|
|
94
|
+
for axis in ("x", "y", "z"):
|
|
95
|
+
_coordinate(atom.get(axis, 0.0))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def validate_raw_project_data(data: Any) -> None:
|
|
99
|
+
"""Validate legacy pickle geometry before any live state is cleared."""
|
|
100
|
+
if not isinstance(data, dict):
|
|
101
|
+
raise ValueError("Raw project must be an object")
|
|
102
|
+
atoms, bonds = data.get("atoms", {}), data.get("bonds", {})
|
|
103
|
+
if not isinstance(atoms, dict) or not isinstance(bonds, dict):
|
|
104
|
+
raise ValueError("Raw project atoms and bonds must be mappings")
|
|
105
|
+
structure = {
|
|
106
|
+
"atoms": [
|
|
107
|
+
dict(atom, id=atom_id, x=atom["pos"][0], y=atom["pos"][1])
|
|
108
|
+
for atom_id, atom in atoms.items()
|
|
109
|
+
],
|
|
110
|
+
"bonds": [
|
|
111
|
+
dict(bond, atom1=key[0], atom2=key[1]) for key, bond in bonds.items()
|
|
112
|
+
],
|
|
113
|
+
"next_atom_id": data.get("_next_atom_id", max(atoms) + 1 if atoms else 0),
|
|
114
|
+
}
|
|
115
|
+
_validate_2d(structure)
|
|
116
|
+
if data.get("mol_3d") is not None:
|
|
117
|
+
_validate_molecule(data["mol_3d"])
|
|
@@ -227,7 +227,7 @@ class PluginContext:
|
|
|
227
227
|
"""Set active RDKit molecule and redraw 3D view."""
|
|
228
228
|
mw = self.get_main_window()
|
|
229
229
|
if mw and hasattr(mw, "view_3d_manager"):
|
|
230
|
-
mw.
|
|
230
|
+
mw.set_current_molecule(mol)
|
|
231
231
|
mw.view_3d_manager.draw_molecule_3d(mol)
|
|
232
232
|
|
|
233
233
|
@property
|
|
@@ -613,7 +613,10 @@ class PluginContext:
|
|
|
613
613
|
|
|
614
614
|
for i in range(num_atoms):
|
|
615
615
|
pos = conf.GetAtomPosition(i)
|
|
616
|
-
|
|
616
|
+
atom = mol.GetAtomWithIdx(i)
|
|
617
|
+
symbol = atom.GetSymbol()
|
|
618
|
+
if atom.GetAtomicNum() == 0 and atom.HasProp("xyz_original_symbol"):
|
|
619
|
+
symbol = atom.GetProp("xyz_original_symbol")
|
|
617
620
|
xyz_lines.append(
|
|
618
621
|
f" {symbol:<5}{pos.x:>15.8f}{pos.y:>15.8f}{pos.z:>15.8f}"
|
|
619
622
|
)
|
|
@@ -260,6 +260,19 @@ class PluginManager:
|
|
|
260
260
|
if os.path.isdir(file_path):
|
|
261
261
|
# Copy entire directory
|
|
262
262
|
dest_path = self._safe_install_path(self.plugin_dir, filename)
|
|
263
|
+
source = os.path.normcase(os.path.realpath(file_path))
|
|
264
|
+
destination = os.path.normcase(os.path.realpath(dest_path))
|
|
265
|
+
if source == destination:
|
|
266
|
+
raise ValueError(
|
|
267
|
+
"Source and installed package are the same directory"
|
|
268
|
+
)
|
|
269
|
+
if (
|
|
270
|
+
os.path.splitdrive(source)[0] == os.path.splitdrive(destination)[0]
|
|
271
|
+
and os.path.commonpath([source, destination]) == source
|
|
272
|
+
):
|
|
273
|
+
raise ValueError(
|
|
274
|
+
"Cannot install a package inside its source directory"
|
|
275
|
+
)
|
|
263
276
|
if os.path.exists(dest_path):
|
|
264
277
|
# Option 1: Overwrite (remove then copy) - safer for clean install
|
|
265
278
|
if os.path.isdir(dest_path):
|
|
@@ -55,6 +55,7 @@ class AlignmentDialog(QDialog, Dialog3DPickingMixin):
|
|
|
55
55
|
Dialog3DPickingMixin.__init__(self)
|
|
56
56
|
self.mol = mol
|
|
57
57
|
self.main_window = main_window
|
|
58
|
+
self._invalidated = False
|
|
58
59
|
self.axis = axis
|
|
59
60
|
self._selected_atoms = SelectionList()
|
|
60
61
|
|
|
@@ -133,7 +134,7 @@ class AlignmentDialog(QDialog, Dialog3DPickingMixin):
|
|
|
133
134
|
|
|
134
135
|
def on_atom_picked(self, atom_idx: int) -> None:
|
|
135
136
|
"""Handle atom selection event."""
|
|
136
|
-
if self.main_window.view_3d_manager.current_mol is None:
|
|
137
|
+
if self._invalidated or self.main_window.view_3d_manager.current_mol is None:
|
|
137
138
|
return
|
|
138
139
|
|
|
139
140
|
if atom_idx in self.selected_atoms:
|
|
@@ -181,8 +182,16 @@ class AlignmentDialog(QDialog, Dialog3DPickingMixin):
|
|
|
181
182
|
for i, idx in enumerate(self.selected_atoms, 1):
|
|
182
183
|
self.add_selection_label(idx, f"#{i}")
|
|
183
184
|
|
|
185
|
+
def invalidate_molecule(self) -> None:
|
|
186
|
+
"""Close the tool before undo, import or calculation replaces its molecule."""
|
|
187
|
+
self._invalidated = True
|
|
188
|
+
self.setEnabled(False)
|
|
189
|
+
self.reject()
|
|
190
|
+
|
|
184
191
|
def apply_alignment(self) -> None:
|
|
185
192
|
"""Apply the specific axial alignment to the molecule."""
|
|
193
|
+
if self._invalidated:
|
|
194
|
+
return
|
|
186
195
|
if len(self.selected_atoms) != 2:
|
|
187
196
|
QMessageBox.warning(
|
|
188
197
|
self, "Warning", "Please select exactly 2 atoms for alignment."
|
|
@@ -90,6 +90,12 @@ class _CalculationReceiver(QObject):
|
|
|
90
90
|
# _retain_until_destroyed). Releasing it here would let shiboken
|
|
91
91
|
# delete this receiver inside its own queued slot call.
|
|
92
92
|
self.deleteLater()
|
|
93
|
+
if (
|
|
94
|
+
self.manager._close_after_calculation
|
|
95
|
+
and not self.manager._active_calc_threads
|
|
96
|
+
):
|
|
97
|
+
self.manager._close_after_calculation = False
|
|
98
|
+
QTimer.singleShot(0, self.manager.host.close)
|
|
93
99
|
|
|
94
100
|
|
|
95
101
|
class ComputeManager:
|
|
@@ -103,6 +109,7 @@ class ComputeManager:
|
|
|
103
109
|
self.host = host
|
|
104
110
|
self.last_successful_optimization_method: Optional[str] = None
|
|
105
111
|
self._active_calc_threads: List[QThread] = []
|
|
112
|
+
self._close_after_calculation = False
|
|
106
113
|
# id(QObject) -> Python objects kept alive until that QObject is destroyed.
|
|
107
114
|
self._calc_objects: Dict[int, Tuple[Any, ...]] = {}
|
|
108
115
|
self.halt_ids: Set[int] = set()
|
|
@@ -120,6 +127,18 @@ class ComputeManager:
|
|
|
120
127
|
"""Reset active calculation threads list."""
|
|
121
128
|
self._active_calc_threads = []
|
|
122
129
|
|
|
130
|
+
def prepare_calculation_shutdown(self) -> bool:
|
|
131
|
+
"""Cancel calculations and defer window destruction until Qt finishes them."""
|
|
132
|
+
running = [thread for thread in self._active_calc_threads if thread.isRunning()]
|
|
133
|
+
if not running:
|
|
134
|
+
return True
|
|
135
|
+
self._close_after_calculation = True
|
|
136
|
+
self.halt_conversion()
|
|
137
|
+
for thread in running:
|
|
138
|
+
thread.requestInterruption()
|
|
139
|
+
self.host.update_status_message("Canceling calculation before closing...")
|
|
140
|
+
return False
|
|
141
|
+
|
|
123
142
|
def _safe_disconnect(self, signal: Any) -> None:
|
|
124
143
|
"""Disconnect a signal, tolerating one that has nothing connected."""
|
|
125
144
|
try:
|
|
@@ -533,6 +533,7 @@ class ExportManager:
|
|
|
533
533
|
# This allows glyphs (where all atoms are in one mesh)
|
|
534
534
|
# to retain individual atom colors when exporting to OBJ/MTL.
|
|
535
535
|
try:
|
|
536
|
+
color_mesh = mesh_copy
|
|
536
537
|
colors = None
|
|
537
538
|
pd = mesh_copy.point_data
|
|
538
539
|
# Use red/green/blue arrays if available
|
|
@@ -559,10 +560,10 @@ class ExportManager:
|
|
|
559
560
|
if colors is None and "colors" in mesh_copy.cell_data:
|
|
560
561
|
try:
|
|
561
562
|
# Convert cell_data to point_data
|
|
562
|
-
|
|
563
|
-
if "colors" in
|
|
563
|
+
color_mesh = mesh_copy.cell_data_to_point_data()
|
|
564
|
+
if "colors" in color_mesh.point_data:
|
|
564
565
|
colors = np.asarray(
|
|
565
|
-
|
|
566
|
+
color_mesh.point_data["colors"]
|
|
566
567
|
)
|
|
567
568
|
except (
|
|
568
569
|
AttributeError,
|
|
@@ -616,15 +617,8 @@ class ExportManager:
|
|
|
616
617
|
if point_inds.size == 0:
|
|
617
618
|
continue
|
|
618
619
|
try:
|
|
619
|
-
# Use temp_mesh if point data available
|
|
620
|
-
target_mesh = (
|
|
621
|
-
temp_mesh
|
|
622
|
-
if "temp_mesh" in locals()
|
|
623
|
-
else mesh_copy
|
|
624
|
-
)
|
|
625
|
-
|
|
626
620
|
# extract_points with adjacent_cells=False to avoid pulling in neighbors
|
|
627
|
-
submesh =
|
|
621
|
+
submesh = color_mesh.extract_points(
|
|
628
622
|
point_inds, adjacent_cells=False
|
|
629
623
|
)
|
|
630
624
|
|
|
@@ -98,9 +98,12 @@ class GeometryBaseDialog(BasePickingDialog):
|
|
|
98
98
|
if wrap:
|
|
99
99
|
f_val = (f_val + 180) % 360 - 180
|
|
100
100
|
|
|
101
|
-
slider.
|
|
102
|
-
slider.
|
|
103
|
-
|
|
101
|
+
scaled = max(slider.minimum(), min(slider.maximum(), f_val * scale))
|
|
102
|
+
was_blocked = slider.blockSignals(True)
|
|
103
|
+
try:
|
|
104
|
+
slider.setValue(int(round(scaled)))
|
|
105
|
+
finally:
|
|
106
|
+
slider.blockSignals(was_blocked)
|
|
104
107
|
except (ValueError, TypeError):
|
|
105
108
|
# Safe defensive fallback catching ValueError, TypeError
|
|
106
109
|
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
@@ -13,6 +13,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
|
+
import math
|
|
16
17
|
import os
|
|
17
18
|
import json
|
|
18
19
|
import pickle
|
|
@@ -48,6 +49,8 @@ from rdkit.Chem import (
|
|
|
48
49
|
)
|
|
49
50
|
|
|
50
51
|
from ..utils.sdf_records import read_sdf_records
|
|
52
|
+
from ..utils.finite_float import finite_float
|
|
53
|
+
from ..core.project_validation import validate_project_data, validate_raw_project_data
|
|
51
54
|
from .sdf_record_dialog import SdfRecordDialog
|
|
52
55
|
from ..utils.constants import (
|
|
53
56
|
COVALENT_RADII,
|
|
@@ -242,11 +245,14 @@ class IOManager:
|
|
|
242
245
|
nums: list[float] = []
|
|
243
246
|
for tok in tokens:
|
|
244
247
|
try:
|
|
245
|
-
|
|
248
|
+
value = float(tok)
|
|
246
249
|
except ValueError:
|
|
247
250
|
if nums:
|
|
248
251
|
break # trailing non-numeric column after the coordinates
|
|
249
252
|
continue # leading label/separator column before the coordinates
|
|
253
|
+
if not math.isfinite(value):
|
|
254
|
+
return None
|
|
255
|
+
nums.append(value)
|
|
250
256
|
if len(nums) == 3:
|
|
251
257
|
break
|
|
252
258
|
if len(nums) == 3:
|
|
@@ -521,11 +527,17 @@ class IOManager:
|
|
|
521
527
|
layout.addLayout(hl)
|
|
522
528
|
result = {"accepted": False, "skip": False}
|
|
523
529
|
|
|
530
|
+
def _charge_value() -> int:
|
|
531
|
+
value = finite_float(line_edit.text().strip() or "0")
|
|
532
|
+
if not value.is_integer() or not -(2**31) <= value < 2**31:
|
|
533
|
+
raise ValueError("Charge must be a supported integer")
|
|
534
|
+
return int(value)
|
|
535
|
+
|
|
524
536
|
def _accept_if_valid() -> None:
|
|
525
537
|
"""Validate charge input and accept the dialog if valid."""
|
|
526
538
|
# Invalid input: show inline error and keep the dialog open
|
|
527
539
|
try:
|
|
528
|
-
|
|
540
|
+
_charge_value()
|
|
529
541
|
except ValueError:
|
|
530
542
|
error_label.setText("Invalid charge: enter an integer (e.g. 0, -1, 2).")
|
|
531
543
|
dialog.adjustSize()
|
|
@@ -544,7 +556,7 @@ class IOManager:
|
|
|
544
556
|
if result["skip"]:
|
|
545
557
|
return 0, True, True
|
|
546
558
|
try:
|
|
547
|
-
val =
|
|
559
|
+
val = _charge_value()
|
|
548
560
|
return val, True, False
|
|
549
561
|
except ValueError:
|
|
550
562
|
# Unreachable via OK (validated inline)
|
|
@@ -824,6 +836,7 @@ class IOManager:
|
|
|
824
836
|
)
|
|
825
837
|
return
|
|
826
838
|
|
|
839
|
+
validate_project_data(json_data)
|
|
827
840
|
if not self.host.edit_actions_manager.clear_all(skip_check=True):
|
|
828
841
|
return
|
|
829
842
|
|
|
@@ -1251,9 +1264,10 @@ class IOManager:
|
|
|
1251
1264
|
)
|
|
1252
1265
|
for i in range(num_atoms):
|
|
1253
1266
|
pos = conf.GetAtomPosition(i)
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
).
|
|
1267
|
+
atom = self.host.view_3d_manager.current_mol.GetAtomWithIdx(i)
|
|
1268
|
+
symbol = atom.GetSymbol()
|
|
1269
|
+
if atom.GetAtomicNum() == 0 and atom.HasProp("xyz_original_symbol"):
|
|
1270
|
+
symbol = atom.GetProp("xyz_original_symbol")
|
|
1257
1271
|
xyz_lines.append(
|
|
1258
1272
|
f" {symbol:<5}{pos.x:>15.8f}{pos.y:>15.8f}{pos.z:>15.8f}"
|
|
1259
1273
|
)
|
|
@@ -1311,6 +1325,7 @@ class IOManager:
|
|
|
1311
1325
|
# Unpickle before clearing, so an unreadable file keeps the document.
|
|
1312
1326
|
with open(file_path, "rb") as f:
|
|
1313
1327
|
loaded_data = pickle.load(f)
|
|
1328
|
+
validate_raw_project_data(loaded_data)
|
|
1314
1329
|
if not self.host.edit_actions_manager.clear_all(skip_check=True):
|
|
1315
1330
|
return
|
|
1316
1331
|
self.host.ui_manager.restore_ui_for_editing()
|
|
@@ -51,7 +51,7 @@ from PySide6.QtWidgets import (
|
|
|
51
51
|
QWidget,
|
|
52
52
|
)
|
|
53
53
|
|
|
54
|
-
from ..utils.default_settings import
|
|
54
|
+
from ..utils.default_settings import normalize_settings
|
|
55
55
|
from ..plugins.plugin_manager import PluginManager
|
|
56
56
|
from ..utils.system_utils import detect_system_dark_mode
|
|
57
57
|
|
|
@@ -552,7 +552,7 @@ class MainInitManager:
|
|
|
552
552
|
self._migrate_legacy_settings(loaded_settings)
|
|
553
553
|
|
|
554
554
|
# 4. Update settings with loaded values
|
|
555
|
-
self.settings
|
|
555
|
+
self.settings = normalize_settings(loaded_settings)
|
|
556
556
|
except (AttributeError, RuntimeError, ValueError, TypeError, IOError):
|
|
557
557
|
# Use defaults on any error
|
|
558
558
|
# Safe defensive fallback catching AttributeError, RuntimeError, ValueError, TypeError, IOError
|
|
@@ -633,7 +633,7 @@ class MainInitManager:
|
|
|
633
633
|
# --- Settings and Plugin Helpers ---
|
|
634
634
|
def _get_default_settings(self) -> Dict[str, Any]:
|
|
635
635
|
"""Return a dictionary of default application settings."""
|
|
636
|
-
return
|
|
636
|
+
return normalize_settings({})
|
|
637
637
|
|
|
638
638
|
def _migrate_legacy_settings(self, loaded_settings: Dict[str, Any]) -> None:
|
|
639
639
|
"""Handle migration of legacy settings keys."""
|
|
@@ -48,6 +48,7 @@ class TemplateMixin:
|
|
|
48
48
|
mode: Any
|
|
49
49
|
removeItem: Any
|
|
50
50
|
views: Any
|
|
51
|
+
set_atom_rs_label: Any
|
|
51
52
|
|
|
52
53
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
53
54
|
"""Initialize template placement mixin."""
|
|
@@ -696,6 +697,7 @@ class TemplateMixin:
|
|
|
696
697
|
atom_item.symbol == symbol
|
|
697
698
|
and atom_item.charge == charge
|
|
698
699
|
and atom_item.radical == radical
|
|
700
|
+
and atom_item.rs_label == atom_data.get("rs")
|
|
699
701
|
):
|
|
700
702
|
return
|
|
701
703
|
|
|
@@ -704,6 +706,7 @@ class TemplateMixin:
|
|
|
704
706
|
atom_item.symbol = symbol
|
|
705
707
|
atom_item.charge = charge
|
|
706
708
|
atom_item.radical = radical
|
|
709
|
+
self.set_atom_rs_label(atom_item, atom_data.get("rs"))
|
|
707
710
|
|
|
708
711
|
record = self.data.atoms.get(atom_item.atom_id)
|
|
709
712
|
if record is not None:
|
|
@@ -746,6 +749,7 @@ class TemplateMixin:
|
|
|
746
749
|
radical = atom_data.get("radical", 0)
|
|
747
750
|
|
|
748
751
|
atom_id = self.create_atom(symbol, pos, charge, radical)
|
|
752
|
+
self.set_atom_rs_label(self.atom_items[atom_id], atom_data.get("rs"))
|
|
749
753
|
atom_id_map[atom_data["id"]] = atom_id
|
|
750
754
|
|
|
751
755
|
# Create bonds (bonds_info is always id-based)
|
|
@@ -20,6 +20,7 @@ from PySide6.QtWidgets import QGraphicsItem, QGraphicsScene
|
|
|
20
20
|
|
|
21
21
|
from .atom_item import AtomItem
|
|
22
22
|
from .bond_item import BondItem
|
|
23
|
+
from ..core.molecular_data import rs_entry
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class PreviewScene(QGraphicsScene):
|
|
@@ -96,6 +97,7 @@ def build_preview_items(
|
|
|
96
97
|
radical=int(atom_data.get("radical", 0) or 0),
|
|
97
98
|
)
|
|
98
99
|
item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, False)
|
|
100
|
+
item.rs_label = rs_entry(atom_data).get("rs")
|
|
99
101
|
item.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, False)
|
|
100
102
|
item.setAcceptHoverEvents(False)
|
|
101
103
|
atom_items.append(item)
|
|
@@ -224,7 +224,9 @@ class UIManager(QObject):
|
|
|
224
224
|
Handle application close logic.
|
|
225
225
|
Returns True if close should proceed, False if it should be cancelled.
|
|
226
226
|
"""
|
|
227
|
-
|
|
227
|
+
calculations_ready = self.host.compute_manager.prepare_calculation_shutdown()
|
|
228
|
+
optimizations_ready = self.host.edit_3d_manager.prepare_optimization_shutdown()
|
|
229
|
+
if not calculations_ready or not optimizations_ready:
|
|
228
230
|
return False
|
|
229
231
|
# 1. Persist settings
|
|
230
232
|
try:
|
|
@@ -271,17 +273,6 @@ class UIManager(QObject):
|
|
|
271
273
|
# Safe defensive fallback catching RuntimeError, TypeError
|
|
272
274
|
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
273
275
|
|
|
274
|
-
# Stop calculation threads
|
|
275
|
-
active_threads = list(
|
|
276
|
-
getattr(self.host.compute_manager, "_active_calc_threads", []) or []
|
|
277
|
-
)
|
|
278
|
-
for thr in active_threads:
|
|
279
|
-
try:
|
|
280
|
-
thr.quit()
|
|
281
|
-
thr.wait(200)
|
|
282
|
-
except (RuntimeError, TypeError):
|
|
283
|
-
# Safe defensive fallback catching RuntimeError, TypeError
|
|
284
|
-
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
285
276
|
except (AttributeError, RuntimeError, TypeError, ValueError):
|
|
286
277
|
# Safe defensive fallback catching AttributeError, RuntimeError, TypeError, ValueError
|
|
287
278
|
logging.debug("Suppressed non-critical error", exc_info=True)
|
|
@@ -36,6 +36,7 @@ from .preview_molecule import (
|
|
|
36
36
|
from .template_preview_view import TemplatePreviewView
|
|
37
37
|
|
|
38
38
|
from ..utils.constants import VERSION
|
|
39
|
+
from ..core.molecular_data import rs_entry
|
|
39
40
|
import json
|
|
40
41
|
import logging
|
|
41
42
|
import os
|
|
@@ -402,6 +403,7 @@ class UserTemplateDialog(QDialog):
|
|
|
402
403
|
"charge": atom.get("charge", 0),
|
|
403
404
|
"radical": atom.get("radical", 0),
|
|
404
405
|
"pos": QPointF(atom["x"], atom["y"]),
|
|
406
|
+
**rs_entry(atom),
|
|
405
407
|
}
|
|
406
408
|
for atom in atoms
|
|
407
409
|
]
|
|
@@ -545,6 +547,7 @@ class UserTemplateDialog(QDialog):
|
|
|
545
547
|
"y": pos[1],
|
|
546
548
|
"charge": atom_info.get("charge", 0),
|
|
547
549
|
"radical": atom_info.get("radical", 0),
|
|
550
|
+
**rs_entry(atom_info),
|
|
548
551
|
}
|
|
549
552
|
)
|
|
550
553
|
|
|
@@ -10,6 +10,10 @@ Repo: https://github.com/HiroYokoyama/python_molecular_editor
|
|
|
10
10
|
DOI: 10.5281/zenodo.17268532
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import copy
|
|
14
|
+
import math
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
13
17
|
from .label_style import DEFAULT_LABEL_SETTINGS
|
|
14
18
|
|
|
15
19
|
DEFAULT_SETTINGS = {
|
|
@@ -96,3 +100,47 @@ DEFAULT_SETTINGS = {
|
|
|
96
100
|
"cleanup_straighten_bonds_2d": False,
|
|
97
101
|
"cleanup_avoid_clashes_2d": False,
|
|
98
102
|
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def normalize_settings(loaded: dict[str, Any]) -> dict[str, Any]:
|
|
106
|
+
"""Keep plugin settings while replacing unsafe built-in values with defaults."""
|
|
107
|
+
result = copy.deepcopy(DEFAULT_SETTINGS)
|
|
108
|
+
result.update(loaded)
|
|
109
|
+
for key, default in DEFAULT_SETTINGS.items():
|
|
110
|
+
value = result[key]
|
|
111
|
+
valid = True
|
|
112
|
+
if isinstance(default, bool):
|
|
113
|
+
valid = isinstance(value, bool)
|
|
114
|
+
elif isinstance(default, (int, float)):
|
|
115
|
+
try:
|
|
116
|
+
number = float(value)
|
|
117
|
+
valid = (
|
|
118
|
+
isinstance(value, (int, float))
|
|
119
|
+
and not isinstance(value, bool)
|
|
120
|
+
and math.isfinite(number)
|
|
121
|
+
and 0 <= number <= (2**31 - 1) / 1000
|
|
122
|
+
)
|
|
123
|
+
if isinstance(default, int):
|
|
124
|
+
valid = valid and number.is_integer()
|
|
125
|
+
if key.endswith("_resolution") or key in (
|
|
126
|
+
"bond_dash_count_2d",
|
|
127
|
+
"atom_font_size_2d",
|
|
128
|
+
):
|
|
129
|
+
valid = valid and number >= 1
|
|
130
|
+
if valid:
|
|
131
|
+
result[key] = int(number) if isinstance(default, int) else number
|
|
132
|
+
except (ValueError, TypeError, OverflowError):
|
|
133
|
+
valid = False
|
|
134
|
+
elif isinstance(default, str):
|
|
135
|
+
valid = isinstance(value, str) and bool(value.strip())
|
|
136
|
+
elif isinstance(default, dict):
|
|
137
|
+
valid = isinstance(value, dict)
|
|
138
|
+
if valid:
|
|
139
|
+
result[key] = {
|
|
140
|
+
symbol: color
|
|
141
|
+
for symbol, color in value.items()
|
|
142
|
+
if isinstance(symbol, str) and isinstance(color, str)
|
|
143
|
+
}
|
|
144
|
+
if not valid:
|
|
145
|
+
result[key] = copy.deepcopy(default)
|
|
146
|
+
return result
|
|
@@ -13,6 +13,7 @@ DOI: 10.5281/zenodo.17268532
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import re
|
|
16
|
+
import math
|
|
16
17
|
from collections.abc import Mapping
|
|
17
18
|
from typing import Any, Dict, Tuple
|
|
18
19
|
|
|
@@ -79,7 +80,7 @@ def _get(settings: Any, key: str) -> Any:
|
|
|
79
80
|
def _color(settings: Any, key: str) -> str:
|
|
80
81
|
"""A stored color if it is a valid #RRGGBB, else the default."""
|
|
81
82
|
value = _get(settings, key)
|
|
82
|
-
if isinstance(value, str) and _HEX_COLOR.
|
|
83
|
+
if isinstance(value, str) and _HEX_COLOR.fullmatch(value):
|
|
83
84
|
return value
|
|
84
85
|
return str(DEFAULT_LABEL_SETTINGS[key])
|
|
85
86
|
|
|
@@ -88,7 +89,12 @@ def _number(settings: Any, key: str, low: float, high: float) -> float:
|
|
|
88
89
|
"""A stored number clamped to [low, high], else the default."""
|
|
89
90
|
value = _get(settings, key)
|
|
90
91
|
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
91
|
-
|
|
92
|
+
try:
|
|
93
|
+
number = float(value)
|
|
94
|
+
except OverflowError:
|
|
95
|
+
return float(DEFAULT_LABEL_SETTINGS[key])
|
|
96
|
+
if math.isfinite(number):
|
|
97
|
+
return min(max(number, low), high)
|
|
92
98
|
return float(DEFAULT_LABEL_SETTINGS[key])
|
|
93
99
|
|
|
94
100
|
|
|
@@ -101,7 +107,7 @@ def _flag(settings: Any, key: str) -> bool:
|
|
|
101
107
|
def _family(settings: Any) -> str:
|
|
102
108
|
"""A stored VTK font family, else the default."""
|
|
103
109
|
value = _get(settings, FONT_FAMILY_KEY)
|
|
104
|
-
if value in {family for family, _ in FONT_FAMILIES}:
|
|
110
|
+
if isinstance(value, str) and value in {family for family, _ in FONT_FAMILIES}:
|
|
105
111
|
return str(value)
|
|
106
112
|
return str(DEFAULT_LABEL_SETTINGS[FONT_FAMILY_KEY])
|
|
107
113
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy-5.0.2 → moleditpy-5.0.3}/src/moleditpy/ui/settings_tabs/settings_2d_cleanup_tab.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|