MoleditPy-linux 4.11.2__tar.gz → 4.11.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_linux-4.11.2 → moleditpy_linux-4.11.3}/PKG-INFO +1 -1
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/pyproject.toml +1 -1
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/core/molecular_data.py +10 -7
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/plugins/plugin_manager.py +89 -56
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/compute_logic.py +17 -5
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/edit_actions_logic.py +9 -1
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/io_logic.py +41 -6
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/LICENSE +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/README.md +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/setup.cfg +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/__init__.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/__main__.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/assets/file_icon.ico +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/assets/icon.icns +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/assets/icon.ico +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/assets/icon.png +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/core/__init__.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/core/mol_geometry.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/core/stereo_check.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/main.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/plugins/__init__.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/plugins/plugin_manager_window.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/__init__.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/about_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/analysis_window.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/app_state.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/atom_item.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/atom_picking.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/bond_item.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/chain_mixin.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/chirality_warning_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/dialog_logic.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/export_logic.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/main_window.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/main_window_init.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/molecular_scene_handler.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/molecule_scene.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/move_dialog_mixin.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/plugin_menu_manager.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/preview_molecule.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/sdf_record_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/settings_tabs/settings_2d_cleanup_tab.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/settings_tabs/settings_labels_tab.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/string_importers.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/ui_manager.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/zoomable_view.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/utils/__init__.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/utils/constants.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/utils/default_settings.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/utils/finite_float.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/utils/hit_radius.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/utils/label_style.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/utils/sdf_records.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/utils/suppress_log.py +0 -0
- {moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/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: 4.11.
|
|
3
|
+
Version: 4.11.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: GNU GENERAL PUBLIC LICENSE
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy-linux
|
|
3
|
-
Version: 4.11.
|
|
3
|
+
Version: 4.11.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: GNU GENERAL PUBLIC LICENSE
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/core/molecular_data.py
RENAMED
|
@@ -438,13 +438,16 @@ class MolecularData:
|
|
|
438
438
|
bond.SetStereo(Chem.BondStereo.STEREOE)
|
|
439
439
|
ez_wanted[bond_idx] = "Z" if stereo_type == 3 else "E"
|
|
440
440
|
|
|
441
|
-
#
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
441
|
+
# Preserve explicit wedge/dash labels, including those applied later.
|
|
442
|
+
for center_idx, neighbor_idx in (
|
|
443
|
+
(begin_atom_idx, neigh1_idx),
|
|
444
|
+
(end_atom_idx, neigh2_idx),
|
|
445
|
+
):
|
|
446
|
+
adjacent = final_mol.GetBondBetweenAtoms(center_idx, neighbor_idx)
|
|
447
|
+
if adjacent is not None and bond_stereo_info.get(
|
|
448
|
+
adjacent.GetIdx(), {}
|
|
449
|
+
).get("type") not in (1, 2):
|
|
450
|
+
adjacent.SetBondDir(Chem.BondDir.NONE)
|
|
448
451
|
|
|
449
452
|
# The labels are CIP E/Z; the neighbors picked above are not always
|
|
450
453
|
# the CIP-higher ones.
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/plugins/plugin_manager.py
RENAMED
|
@@ -16,8 +16,10 @@ import hashlib
|
|
|
16
16
|
import importlib.util
|
|
17
17
|
import json
|
|
18
18
|
import logging
|
|
19
|
+
import ntpath
|
|
19
20
|
import os
|
|
20
21
|
import shutil
|
|
22
|
+
import stat
|
|
21
23
|
import sys
|
|
22
24
|
import zipfile
|
|
23
25
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
@@ -164,6 +166,88 @@ class PluginManager:
|
|
|
164
166
|
self.ensure_plugin_dir()
|
|
165
167
|
QDesktopServices.openUrl(QUrl.fromLocalFile(self.plugin_dir))
|
|
166
168
|
|
|
169
|
+
@staticmethod
|
|
170
|
+
def _safe_install_path(root: str, name: str) -> str:
|
|
171
|
+
"""Resolve a portable relative path strictly within an install directory."""
|
|
172
|
+
normalized = name.replace("\\", "/").rstrip("/")
|
|
173
|
+
parts = normalized.split("/")
|
|
174
|
+
reserved = {"CON", "PRN", "AUX", "NUL"} | {
|
|
175
|
+
f"{prefix}{number}" for prefix in ("COM", "LPT") for number in range(1, 10)
|
|
176
|
+
}
|
|
177
|
+
if (
|
|
178
|
+
not normalized
|
|
179
|
+
or normalized.startswith("/")
|
|
180
|
+
or ntpath.splitdrive(normalized)[0]
|
|
181
|
+
or any(
|
|
182
|
+
part in ("", ".", "..")
|
|
183
|
+
or part.endswith((".", " "))
|
|
184
|
+
or any(char in part for char in '<>:"|?*\0')
|
|
185
|
+
or part.split(".")[0].upper() in reserved
|
|
186
|
+
for part in parts
|
|
187
|
+
)
|
|
188
|
+
):
|
|
189
|
+
raise ValueError(f"Unsafe plugin path: {name!r}")
|
|
190
|
+
destination = os.path.abspath(os.path.join(root, *parts))
|
|
191
|
+
resolved_root = os.path.normcase(os.path.realpath(root))
|
|
192
|
+
resolved_destination = os.path.normcase(os.path.realpath(destination))
|
|
193
|
+
if (
|
|
194
|
+
resolved_destination == resolved_root
|
|
195
|
+
or os.path.commonpath([resolved_root, resolved_destination])
|
|
196
|
+
!= resolved_root
|
|
197
|
+
):
|
|
198
|
+
raise ValueError(f"Plugin path escapes install directory: {name!r}")
|
|
199
|
+
return destination
|
|
200
|
+
|
|
201
|
+
def _install_zip(self, file_path: str, filename: str) -> str:
|
|
202
|
+
"""Validate every archive member before removing or writing plugin files."""
|
|
203
|
+
with zipfile.ZipFile(file_path, "r") as archive:
|
|
204
|
+
members = []
|
|
205
|
+
for member in archive.infolist():
|
|
206
|
+
self._safe_install_path(self.plugin_dir, member.orig_filename)
|
|
207
|
+
if stat.S_ISLNK(member.external_attr >> 16):
|
|
208
|
+
raise ValueError(
|
|
209
|
+
"Plugin ZIP archives cannot contain symbolic links"
|
|
210
|
+
)
|
|
211
|
+
members.append((member, member.filename.replace("\\", "/")))
|
|
212
|
+
if not members:
|
|
213
|
+
raise ValueError("Plugin ZIP archive is empty")
|
|
214
|
+
|
|
215
|
+
roots = {name.split("/")[0] for _, name in members}
|
|
216
|
+
nested = (
|
|
217
|
+
len(roots) == 1
|
|
218
|
+
and any("/" in name for _, name in members)
|
|
219
|
+
and next(iter(roots)) != "__init__.py"
|
|
220
|
+
)
|
|
221
|
+
folder = next(iter(roots)) if nested else os.path.splitext(filename)[0]
|
|
222
|
+
destination = self._safe_install_path(self.plugin_dir, folder)
|
|
223
|
+
relative_members = [
|
|
224
|
+
(member, name.partition("/")[2] if nested else name)
|
|
225
|
+
for member, name in members
|
|
226
|
+
]
|
|
227
|
+
# Validate against the package boundary as well as the plugin root.
|
|
228
|
+
for _, name in relative_members:
|
|
229
|
+
if name:
|
|
230
|
+
self._safe_install_path(destination, name)
|
|
231
|
+
|
|
232
|
+
if os.path.exists(destination):
|
|
233
|
+
if os.path.isdir(destination):
|
|
234
|
+
shutil.rmtree(destination)
|
|
235
|
+
else:
|
|
236
|
+
os.remove(destination)
|
|
237
|
+
os.makedirs(destination)
|
|
238
|
+
for member, name in relative_members:
|
|
239
|
+
if not name:
|
|
240
|
+
continue
|
|
241
|
+
target = self._safe_install_path(destination, name)
|
|
242
|
+
if name.endswith("/"):
|
|
243
|
+
os.makedirs(target, exist_ok=True)
|
|
244
|
+
else:
|
|
245
|
+
os.makedirs(os.path.dirname(target), exist_ok=True)
|
|
246
|
+
with archive.open(member) as source, open(target, "wb") as output:
|
|
247
|
+
shutil.copyfileobj(source, output)
|
|
248
|
+
kind = "ZIP" if nested else "Flat ZIP"
|
|
249
|
+
return f"Installed package {folder} (from {kind})"
|
|
250
|
+
|
|
167
251
|
def install_plugin(self, file_path: str) -> Tuple[bool, str]:
|
|
168
252
|
"""Copies a plugin file to the plugin directory. Supports .py and .zip."""
|
|
169
253
|
self.ensure_plugin_dir()
|
|
@@ -174,7 +258,7 @@ class PluginManager:
|
|
|
174
258
|
|
|
175
259
|
if os.path.isdir(file_path):
|
|
176
260
|
# Copy entire directory
|
|
177
|
-
dest_path =
|
|
261
|
+
dest_path = self._safe_install_path(self.plugin_dir, filename)
|
|
178
262
|
if os.path.exists(dest_path):
|
|
179
263
|
# Option 1: Overwrite (remove then copy) - safer for clean install
|
|
180
264
|
if os.path.isdir(dest_path):
|
|
@@ -190,63 +274,10 @@ class PluginManager:
|
|
|
190
274
|
)
|
|
191
275
|
msg = f"Installed package {filename}"
|
|
192
276
|
elif filename.lower().endswith(".zip"):
|
|
193
|
-
|
|
194
|
-
with zipfile.ZipFile(file_path, "r") as zf:
|
|
195
|
-
# Smart Extraction: Check if ZIP has a single top-level folder
|
|
196
|
-
# Fix for paths with backslashes on Windows if zip was created on Windows
|
|
197
|
-
roots = set()
|
|
198
|
-
root_is_dir = False
|
|
199
|
-
for name in zf.namelist():
|
|
200
|
-
# Normalize path separators to forward slash for consistent check
|
|
201
|
-
name = name.replace("\\", "/")
|
|
202
|
-
parts = name.split("/")
|
|
203
|
-
if parts[0]:
|
|
204
|
-
roots.add(parts[0])
|
|
205
|
-
# Sub-entries prove the root is a folder
|
|
206
|
-
if len(parts) > 1:
|
|
207
|
-
root_is_dir = True
|
|
208
|
-
|
|
209
|
-
is_nested = len(roots) == 1 and root_is_dir
|
|
210
|
-
|
|
211
|
-
if is_nested:
|
|
212
|
-
# Case A: ZIP contains a single folder (e.g. MyPlugin/init.py)
|
|
213
|
-
top_folder = list(roots)[0]
|
|
214
|
-
|
|
215
|
-
# A root named __init__.py always needs a wrapper folder
|
|
216
|
-
if top_folder == "__init__.py":
|
|
217
|
-
is_nested = False
|
|
218
|
-
|
|
219
|
-
if is_nested:
|
|
220
|
-
# Case A (Confirmed): Extract directly
|
|
221
|
-
dest_path = os.path.join(self.plugin_dir, top_folder)
|
|
222
|
-
|
|
223
|
-
# Clean Install: Remove existing folder to prevent stale files
|
|
224
|
-
if os.path.exists(dest_path):
|
|
225
|
-
if os.path.isdir(dest_path):
|
|
226
|
-
shutil.rmtree(dest_path)
|
|
227
|
-
else:
|
|
228
|
-
os.remove(dest_path)
|
|
229
|
-
|
|
230
|
-
zf.extractall(self.plugin_dir)
|
|
231
|
-
msg = f"Installed package {top_folder} (from ZIP)"
|
|
232
|
-
else:
|
|
233
|
-
# Case B: ZIP is flat (e.g. file1.py, file2.py or just __init__.py)
|
|
234
|
-
# Extract into a new folder named after the ZIP file
|
|
235
|
-
folder_name = os.path.splitext(filename)[0]
|
|
236
|
-
dest_path = os.path.join(self.plugin_dir, folder_name)
|
|
237
|
-
|
|
238
|
-
if os.path.exists(dest_path):
|
|
239
|
-
if os.path.isdir(dest_path):
|
|
240
|
-
shutil.rmtree(dest_path)
|
|
241
|
-
else:
|
|
242
|
-
os.remove(dest_path)
|
|
243
|
-
|
|
244
|
-
os.makedirs(dest_path)
|
|
245
|
-
zf.extractall(dest_path)
|
|
246
|
-
msg = f"Installed package {folder_name} (from Flat ZIP)"
|
|
277
|
+
msg = self._install_zip(file_path, filename)
|
|
247
278
|
else:
|
|
248
279
|
# Standard file copy
|
|
249
|
-
dest_path =
|
|
280
|
+
dest_path = self._safe_install_path(self.plugin_dir, filename)
|
|
250
281
|
if os.path.exists(dest_path):
|
|
251
282
|
if os.path.isdir(dest_path):
|
|
252
283
|
shutil.rmtree(dest_path)
|
|
@@ -265,7 +296,9 @@ class PluginManager:
|
|
|
265
296
|
OSError,
|
|
266
297
|
ImportError,
|
|
267
298
|
SyntaxError,
|
|
299
|
+
zipfile.BadZipFile,
|
|
268
300
|
) as e:
|
|
301
|
+
logging.warning("Plugin installation failed: %s", e, exc_info=True)
|
|
269
302
|
return False, str(e)
|
|
270
303
|
|
|
271
304
|
def discover_plugins(self, parent: Any = None) -> List[Dict[str, Any]]:
|
|
@@ -58,6 +58,7 @@ class ComputeManager:
|
|
|
58
58
|
"""Initialize ComputeManager with reference to host window."""
|
|
59
59
|
self._calculating_text_actor: Any = None
|
|
60
60
|
self.original_atom_properties: Dict[int, int] = {}
|
|
61
|
+
self._worker_atom_properties: Dict[int, Dict[int, int]] = {}
|
|
61
62
|
self.host = host
|
|
62
63
|
self.last_successful_optimization_method: Optional[str] = None
|
|
63
64
|
self._active_calc_threads: List[QThread] = []
|
|
@@ -270,6 +271,7 @@ class ComputeManager:
|
|
|
270
271
|
self.next_conversion_id = run_id + 1
|
|
271
272
|
self.active_worker_ids.add(run_id)
|
|
272
273
|
self._conversion_run_ids.add(run_id)
|
|
274
|
+
self._worker_atom_properties[run_id] = self.original_atom_properties.copy()
|
|
273
275
|
|
|
274
276
|
# The worker only runs built-in force fields. If the selected method is a
|
|
275
277
|
# plugin optimizer, pre-optimize with MMFF (RDKit) in the worker (which
|
|
@@ -336,6 +338,7 @@ class ComputeManager:
|
|
|
336
338
|
self.halt_ids.update(wids_to_halt)
|
|
337
339
|
self.active_worker_ids.clear()
|
|
338
340
|
self._pending_plugin_opt.clear()
|
|
341
|
+
self._worker_atom_properties.clear()
|
|
339
342
|
getattr(self, "_conversion_run_ids", set()).clear()
|
|
340
343
|
|
|
341
344
|
self._restore_button_ui()
|
|
@@ -394,6 +397,11 @@ class ComputeManager:
|
|
|
394
397
|
run_id = int(getattr(self, "next_conversion_id", 1))
|
|
395
398
|
self.next_conversion_id = run_id + 1
|
|
396
399
|
options["worker_id"] = run_id
|
|
400
|
+
self._worker_atom_properties[run_id] = {
|
|
401
|
+
atom.GetIdx(): atom.GetIntProp("_original_atom_id")
|
|
402
|
+
for atom in self.host.view_3d_manager.current_mol.GetAtoms()
|
|
403
|
+
if atom.HasProp("_original_atom_id")
|
|
404
|
+
}
|
|
397
405
|
|
|
398
406
|
self.active_worker_ids.add(run_id)
|
|
399
407
|
|
|
@@ -601,9 +609,14 @@ class ComputeManager:
|
|
|
601
609
|
self.host.set_current_molecule(mol)
|
|
602
610
|
self.host.is_xyz_derived = False
|
|
603
611
|
|
|
604
|
-
#
|
|
612
|
+
# MOL blocks omit custom atom properties; restore the submitting molecule's IDs.
|
|
613
|
+
atom_properties = (
|
|
614
|
+
self._worker_atom_properties.pop(worker_id, {})
|
|
615
|
+
if worker_id is not None
|
|
616
|
+
else self.original_atom_properties
|
|
617
|
+
)
|
|
605
618
|
if mol:
|
|
606
|
-
for i, orig_id in
|
|
619
|
+
for i, orig_id in atom_properties.items():
|
|
607
620
|
if i < mol.GetNumAtoms():
|
|
608
621
|
mol.GetAtomWithIdx(i).SetIntProp("_original_atom_id", orig_id)
|
|
609
622
|
|
|
@@ -713,11 +726,10 @@ class ComputeManager:
|
|
|
713
726
|
# Accept either a string or (worker_id, message) tuple from the worker signal
|
|
714
727
|
if isinstance(message, tuple) and len(message) == 2:
|
|
715
728
|
worker_id, msg = message
|
|
729
|
+
self._worker_atom_properties.pop(worker_id, None)
|
|
716
730
|
getattr(self, "_conversion_run_ids", set()).discard(worker_id)
|
|
717
731
|
if worker_id not in self.active_worker_ids:
|
|
718
|
-
#
|
|
719
|
-
self._remove_calculating_text()
|
|
720
|
-
self._restore_button_ui()
|
|
732
|
+
# A stale worker must not reset the current calculation's UI.
|
|
721
733
|
# If it was a halt or error from a stale worker, show with ID
|
|
722
734
|
if msg == "Halt" or msg == "Halted":
|
|
723
735
|
self.host.statusBar().showMessage( # type: ignore[union-attr]
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/edit_actions_logic.py
RENAMED
|
@@ -93,7 +93,7 @@ class Rotate2DDialog(QDialog):
|
|
|
93
93
|
|
|
94
94
|
# Slider
|
|
95
95
|
self.slider = QSlider(Qt.Orientation.Horizontal)
|
|
96
|
-
self.slider.setRange(-
|
|
96
|
+
self.slider.setRange(-360, 360)
|
|
97
97
|
self.slider.setValue(int(initial_angle))
|
|
98
98
|
self.slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
|
99
99
|
self.slider.setTickInterval(15)
|
|
@@ -240,6 +240,9 @@ class EditActionsManager:
|
|
|
240
240
|
finally:
|
|
241
241
|
self.host.is_restoring_state = False
|
|
242
242
|
|
|
243
|
+
self.host.set_has_unsaved_changes(True)
|
|
244
|
+
self.host.state_manager.update_window_title()
|
|
245
|
+
|
|
243
246
|
# Re-evaluate menu states based on 3D structure after Undo
|
|
244
247
|
if (
|
|
245
248
|
self.host.view_3d_manager.current_mol
|
|
@@ -265,6 +268,9 @@ class EditActionsManager:
|
|
|
265
268
|
finally:
|
|
266
269
|
self.host.is_restoring_state = False
|
|
267
270
|
|
|
271
|
+
self.host.set_has_unsaved_changes(True)
|
|
272
|
+
self.host.state_manager.update_window_title()
|
|
273
|
+
|
|
268
274
|
# Re-evaluate menu states based on 3D structure after Redo
|
|
269
275
|
if (
|
|
270
276
|
self.host.view_3d_manager.current_mol
|
|
@@ -954,6 +960,8 @@ class EditActionsManager:
|
|
|
954
960
|
# Cancel if requested
|
|
955
961
|
return False
|
|
956
962
|
|
|
963
|
+
# Invalidate outstanding results before replacing the document.
|
|
964
|
+
self.host.compute_manager.halt_conversion()
|
|
957
965
|
self.host.ui_manager.restore_ui_for_editing()
|
|
958
966
|
|
|
959
967
|
# Reset 3D mode
|
|
@@ -16,6 +16,8 @@ import logging
|
|
|
16
16
|
import os
|
|
17
17
|
import json
|
|
18
18
|
import pickle
|
|
19
|
+
import stat
|
|
20
|
+
import tempfile
|
|
19
21
|
import unicodedata
|
|
20
22
|
from ..utils.suppress_log import suppress_log
|
|
21
23
|
from typing import Any, Callable, List, Optional, Tuple
|
|
@@ -606,6 +608,42 @@ class IOManager:
|
|
|
606
608
|
|
|
607
609
|
return len(bonds_added)
|
|
608
610
|
|
|
611
|
+
@staticmethod
|
|
612
|
+
def _write_project_json(file_path: str, data: dict[str, Any]) -> None:
|
|
613
|
+
"""Replace a project only after its complete JSON has been written."""
|
|
614
|
+
# Resolve symlinks so os.replace updates the linked project rather
|
|
615
|
+
# than swapping the link itself for a regular file.
|
|
616
|
+
destination = os.path.realpath(file_path)
|
|
617
|
+
try:
|
|
618
|
+
existing_mode = stat.S_IMODE(os.stat(destination).st_mode)
|
|
619
|
+
except FileNotFoundError:
|
|
620
|
+
existing_mode = None
|
|
621
|
+
temporary_path = None
|
|
622
|
+
try:
|
|
623
|
+
with tempfile.NamedTemporaryFile(
|
|
624
|
+
mode="w",
|
|
625
|
+
encoding="utf-8",
|
|
626
|
+
dir=os.path.dirname(destination),
|
|
627
|
+
prefix=f".{os.path.basename(destination)}.",
|
|
628
|
+
suffix=".tmp",
|
|
629
|
+
delete=False,
|
|
630
|
+
) as stream:
|
|
631
|
+
temporary_path = stream.name
|
|
632
|
+
json.dump(data, stream, indent=2, ensure_ascii=False)
|
|
633
|
+
stream.flush()
|
|
634
|
+
os.fsync(stream.fileno())
|
|
635
|
+
if existing_mode is not None:
|
|
636
|
+
# NamedTemporaryFile is owner-only; keep the project's access.
|
|
637
|
+
os.chmod(temporary_path, existing_mode)
|
|
638
|
+
os.replace(temporary_path, destination)
|
|
639
|
+
temporary_path = None
|
|
640
|
+
finally:
|
|
641
|
+
if temporary_path is not None:
|
|
642
|
+
try:
|
|
643
|
+
os.unlink(temporary_path)
|
|
644
|
+
except OSError:
|
|
645
|
+
logging.exception("Failed to remove temporary project file")
|
|
646
|
+
|
|
609
647
|
def save_project(self) -> None:
|
|
610
648
|
"""Save (Ctrl+S) - Defaults to PMEPRJ format."""
|
|
611
649
|
if (
|
|
@@ -627,8 +665,7 @@ class IOManager:
|
|
|
627
665
|
if current_path and current_path.lower().endswith(".pmeprj"):
|
|
628
666
|
try:
|
|
629
667
|
json_data = self.host.state_manager.create_json_data()
|
|
630
|
-
|
|
631
|
-
json.dump(json_data, f, indent=2, ensure_ascii=False)
|
|
668
|
+
self._write_project_json(current_path, json_data)
|
|
632
669
|
|
|
633
670
|
self.host.set_has_unsaved_changes(False)
|
|
634
671
|
self.host.state_manager.update_window_title()
|
|
@@ -669,8 +706,7 @@ class IOManager:
|
|
|
669
706
|
|
|
670
707
|
try:
|
|
671
708
|
json_data = self.host.state_manager.create_json_data()
|
|
672
|
-
|
|
673
|
-
json.dump(json_data, f, indent=2, ensure_ascii=False)
|
|
709
|
+
self._write_project_json(file_path, json_data)
|
|
674
710
|
|
|
675
711
|
self.host.set_has_unsaved_changes(False)
|
|
676
712
|
self.host.set_current_file_path(file_path)
|
|
@@ -740,8 +776,7 @@ class IOManager:
|
|
|
740
776
|
file_path += ".pmeprj"
|
|
741
777
|
|
|
742
778
|
json_data = self.host.state_manager.create_json_data()
|
|
743
|
-
|
|
744
|
-
json.dump(json_data, f, indent=2, ensure_ascii=False)
|
|
779
|
+
self._write_project_json(file_path, json_data)
|
|
745
780
|
|
|
746
781
|
self.host.set_has_unsaved_changes(False)
|
|
747
782
|
self.host.set_current_file_path(file_path)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/MoleditPy_linux.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/MoleditPy_linux.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/MoleditPy_linux.egg-info/top_level.txt
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
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/plugins/plugin_interface.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/align_plane_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/alignment_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/base_picking_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/bond_length_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/calculation_worker.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/chirality_warning_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/color_settings_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/custom_interactor_style.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/custom_qt_interactor.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/geometry_base_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/main_window_init.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/molecular_scene_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/move_dialog_mixin.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/move_group_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/periodic_table_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/planarize_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/plugin_menu_manager.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/preview_molecule.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/sdf_record_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/settings_tabs/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/string_importers.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/template_preview_item.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/template_preview_view.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/translation_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/ui/user_template_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/utils/default_settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.11.2 → moleditpy_linux-4.11.3}/src/moleditpy_linux/utils/sip_isdeleted_safe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|