MoleditPy 2.2.6__py3-none-any.whl → 2.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- moleditpy/modules/constants.py +1 -1
- moleditpy/modules/main_window_main_init.py +51 -27
- moleditpy/modules/plugin_manager.py +233 -95
- moleditpy/modules/plugin_manager_window.py +67 -14
- {moleditpy-2.2.6.dist-info → moleditpy-2.3.1.dist-info}/METADATA +1 -1
- {moleditpy-2.2.6.dist-info → moleditpy-2.3.1.dist-info}/RECORD +10 -10
- {moleditpy-2.2.6.dist-info → moleditpy-2.3.1.dist-info}/WHEEL +0 -0
- {moleditpy-2.2.6.dist-info → moleditpy-2.3.1.dist-info}/entry_points.txt +0 -0
- {moleditpy-2.2.6.dist-info → moleditpy-2.3.1.dist-info}/licenses/LICENSE +0 -0
- {moleditpy-2.2.6.dist-info → moleditpy-2.3.1.dist-info}/top_level.txt +0 -0
moleditpy/modules/constants.py
CHANGED
|
@@ -1878,39 +1878,63 @@ class MainWindowMainInit(object):
|
|
|
1878
1878
|
no_plugin_action.setEnabled(False)
|
|
1879
1879
|
plugin_menu.addAction(no_plugin_action)
|
|
1880
1880
|
else:
|
|
1881
|
-
# Sort plugins:
|
|
1882
|
-
|
|
1881
|
+
# Sort plugins:
|
|
1882
|
+
# 1. Categories (A-Z)
|
|
1883
|
+
# 2. Within Category: Items (A-Z)
|
|
1884
|
+
# 3. Root items (A-Z)
|
|
1885
|
+
|
|
1886
|
+
# Group plugins by category
|
|
1887
|
+
categorized_plugins = {}
|
|
1888
|
+
root_plugins = []
|
|
1883
1889
|
|
|
1884
|
-
# Dictionary to keep track of created submenus: path -> QMenu
|
|
1885
|
-
menus = { "": plugin_menu }
|
|
1886
|
-
|
|
1887
1890
|
for p in plugins:
|
|
1888
|
-
# Only add legacy plugins (with 'run' function) to the generic Plugins menu.
|
|
1889
1891
|
if hasattr(p['module'], 'run'):
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1892
|
+
category = p.get('category', p.get('rel_folder', '')).strip()
|
|
1893
|
+
if category:
|
|
1894
|
+
if category not in categorized_plugins:
|
|
1895
|
+
categorized_plugins[category] = []
|
|
1896
|
+
categorized_plugins[category].append(p)
|
|
1897
|
+
else:
|
|
1898
|
+
root_plugins.append(p)
|
|
1899
|
+
|
|
1900
|
+
# Sort categories
|
|
1901
|
+
sorted_categories = sorted(categorized_plugins.keys())
|
|
1902
|
+
|
|
1903
|
+
# Build menu: Categories first
|
|
1904
|
+
for cat in sorted_categories:
|
|
1905
|
+
# Create/Get Category Menu (Nested support)
|
|
1906
|
+
parts = cat.split(os.sep)
|
|
1907
|
+
parent_menu = plugin_menu
|
|
1908
|
+
|
|
1909
|
+
# Traverse/Create nested menus
|
|
1910
|
+
for part in parts:
|
|
1911
|
+
found_sub = False
|
|
1912
|
+
for act in parent_menu.actions():
|
|
1913
|
+
if act.menu() and act.text().replace('&', '') == part:
|
|
1914
|
+
parent_menu = act.menu()
|
|
1915
|
+
found_sub = True
|
|
1916
|
+
break
|
|
1917
|
+
if not found_sub:
|
|
1918
|
+
parent_menu = parent_menu.addMenu(part)
|
|
1919
|
+
|
|
1920
|
+
# Add items to the leaf category menu (Sorted A-Z)
|
|
1921
|
+
cat_items = sorted(categorized_plugins[cat], key=lambda x: x['name'])
|
|
1922
|
+
for p in cat_items:
|
|
1910
1923
|
action = QAction(p['name'], self)
|
|
1911
|
-
action.triggered.connect(lambda checked, mod=p['module']: self.plugin_manager.run_plugin(mod, self))
|
|
1924
|
+
action.triggered.connect(lambda checked, mod=p['module']: self.plugin_manager.run_plugin(mod, self))
|
|
1912
1925
|
parent_menu.addAction(action)
|
|
1913
1926
|
|
|
1927
|
+
# Add separator if needed <-- REMOVED per user request
|
|
1928
|
+
# if sorted_categories and root_plugins:
|
|
1929
|
+
# plugin_menu.addSeparator()
|
|
1930
|
+
|
|
1931
|
+
# Build menu: Root items last (Sorted A-Z)
|
|
1932
|
+
root_plugins.sort(key=lambda x: x['name'])
|
|
1933
|
+
for p in root_plugins:
|
|
1934
|
+
action = QAction(p['name'], self)
|
|
1935
|
+
action.triggered.connect(lambda checked, mod=p['module']: self.plugin_manager.run_plugin(mod, self))
|
|
1936
|
+
plugin_menu.addAction(action)
|
|
1937
|
+
|
|
1914
1938
|
# 4. Integrate Export Actions into Export Button and Menu
|
|
1915
1939
|
# 4. Integrate Export Actions into Export Button AND Main File->Export Menu
|
|
1916
1940
|
if self.plugin_manager.export_actions:
|
|
@@ -18,6 +18,7 @@ Manages discovery, loading, and execution of external plugins.
|
|
|
18
18
|
import os
|
|
19
19
|
import sys
|
|
20
20
|
import shutil
|
|
21
|
+
import zipfile
|
|
21
22
|
import importlib.util
|
|
22
23
|
import traceback
|
|
23
24
|
import ast
|
|
@@ -71,34 +72,109 @@ class PluginManager:
|
|
|
71
72
|
QDesktopServices.openUrl(QUrl.fromLocalFile(self.plugin_dir))
|
|
72
73
|
|
|
73
74
|
def install_plugin(self, file_path):
|
|
74
|
-
"""Copies a plugin file to the plugin directory."""
|
|
75
|
+
"""Copies a plugin file to the plugin directory. Supports .py and .zip."""
|
|
75
76
|
self.ensure_plugin_dir()
|
|
76
77
|
try:
|
|
78
|
+
# Handle trailing slash and normalize path
|
|
79
|
+
file_path = os.path.normpath(file_path)
|
|
77
80
|
filename = os.path.basename(file_path)
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
|
|
82
|
+
if os.path.isdir(file_path):
|
|
83
|
+
# Copy entire directory
|
|
84
|
+
dest_path = os.path.join(self.plugin_dir, filename)
|
|
85
|
+
if os.path.exists(dest_path):
|
|
86
|
+
# Option 1: Overwrite (remove then copy) - safer for clean install
|
|
87
|
+
if os.path.isdir(dest_path):
|
|
88
|
+
shutil.rmtree(dest_path)
|
|
89
|
+
else:
|
|
90
|
+
os.remove(dest_path)
|
|
91
|
+
|
|
92
|
+
# Copy directory, ignoring cache files
|
|
93
|
+
shutil.copytree(file_path, dest_path, ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git'))
|
|
94
|
+
msg = f"Installed package {filename}"
|
|
95
|
+
elif filename.lower().endswith('.zip'):
|
|
96
|
+
# Extract ZIP contents
|
|
97
|
+
with zipfile.ZipFile(file_path, 'r') as zf:
|
|
98
|
+
# Smart Extraction: Check if ZIP has a single top-level folder
|
|
99
|
+
# Fix for paths with backslashes on Windows if zip was created on Windows
|
|
100
|
+
roots = set()
|
|
101
|
+
for name in zf.namelist():
|
|
102
|
+
# Normalize path separators to forward slash for consistent check
|
|
103
|
+
name = name.replace('\\', '/')
|
|
104
|
+
parts = name.split('/')
|
|
105
|
+
if parts[0]:
|
|
106
|
+
roots.add(parts[0])
|
|
107
|
+
|
|
108
|
+
is_nested = (len(roots) == 1)
|
|
109
|
+
|
|
110
|
+
if is_nested:
|
|
111
|
+
# Case A: ZIP contains a single folder (e.g. MyPlugin/init.py)
|
|
112
|
+
top_folder = list(roots)[0]
|
|
113
|
+
|
|
114
|
+
# Guard: If the single item is __init__.py, we MUST create a wrapper folder
|
|
115
|
+
# otherwise we pollute the plugin_dir root.
|
|
116
|
+
if top_folder == "__init__.py":
|
|
117
|
+
is_nested = False
|
|
118
|
+
|
|
119
|
+
if is_nested:
|
|
120
|
+
# Case A (Confirmed): Extract directly
|
|
121
|
+
dest_path = os.path.join(self.plugin_dir, top_folder)
|
|
122
|
+
|
|
123
|
+
# Clean Install: Remove existing folder to prevent stale files
|
|
124
|
+
if os.path.exists(dest_path):
|
|
125
|
+
if os.path.isdir(dest_path):
|
|
126
|
+
shutil.rmtree(dest_path)
|
|
127
|
+
else:
|
|
128
|
+
os.remove(dest_path)
|
|
129
|
+
|
|
130
|
+
zf.extractall(self.plugin_dir)
|
|
131
|
+
msg = f"Installed package {top_folder} (from ZIP)"
|
|
132
|
+
else:
|
|
133
|
+
# Case B: ZIP is flat (e.g. file1.py, file2.py or just __init__.py)
|
|
134
|
+
# Extract into a new folder named after the ZIP file
|
|
135
|
+
folder_name = os.path.splitext(filename)[0]
|
|
136
|
+
dest_path = os.path.join(self.plugin_dir, folder_name)
|
|
137
|
+
|
|
138
|
+
if os.path.exists(dest_path):
|
|
139
|
+
if os.path.isdir(dest_path):
|
|
140
|
+
shutil.rmtree(dest_path)
|
|
141
|
+
else:
|
|
142
|
+
os.remove(dest_path)
|
|
143
|
+
|
|
144
|
+
os.makedirs(dest_path)
|
|
145
|
+
zf.extractall(dest_path)
|
|
146
|
+
msg = f"Installed package {folder_name} (from Flat ZIP)"
|
|
147
|
+
else:
|
|
148
|
+
# Standard file copy
|
|
149
|
+
dest_path = os.path.join(self.plugin_dir, filename)
|
|
150
|
+
if os.path.exists(dest_path):
|
|
151
|
+
if os.path.isdir(dest_path):
|
|
152
|
+
shutil.rmtree(dest_path)
|
|
153
|
+
shutil.copy2(file_path, dest_path)
|
|
154
|
+
msg = f"Installed {filename}"
|
|
155
|
+
|
|
80
156
|
# Reload plugins after install
|
|
81
157
|
if self.main_window:
|
|
82
158
|
self.discover_plugins(self.main_window)
|
|
83
|
-
return True,
|
|
159
|
+
return True, msg
|
|
84
160
|
except Exception as e:
|
|
85
161
|
return False, str(e)
|
|
86
162
|
|
|
87
163
|
def discover_plugins(self, parent=None):
|
|
88
164
|
"""
|
|
89
|
-
|
|
90
|
-
|
|
165
|
+
Hybrid discovery:
|
|
166
|
+
- Folders with '__init__.py' -> Treated as single package plugin.
|
|
167
|
+
- Folders without '__init__.py' -> Treated as category folders (scan for .py inside).
|
|
91
168
|
"""
|
|
92
169
|
if parent:
|
|
93
170
|
self.main_window = parent
|
|
94
171
|
|
|
95
172
|
self.ensure_plugin_dir()
|
|
96
173
|
self.plugins = []
|
|
174
|
+
# Clear registries
|
|
97
175
|
self.menu_actions = []
|
|
98
176
|
self.toolbar_actions = []
|
|
99
177
|
self.drop_handlers = []
|
|
100
|
-
|
|
101
|
-
# Clear extended registries
|
|
102
178
|
self.export_actions = []
|
|
103
179
|
self.optimization_methods = {}
|
|
104
180
|
self.file_openers = {}
|
|
@@ -111,81 +187,122 @@ class PluginManager:
|
|
|
111
187
|
return []
|
|
112
188
|
|
|
113
189
|
for root, dirs, files in os.walk(self.plugin_dir):
|
|
114
|
-
#
|
|
190
|
+
# Exclude hidden directories
|
|
115
191
|
dirs[:] = [d for d in dirs if not d.startswith('__') and d != '__pycache__']
|
|
116
192
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
193
|
+
# [Check] Is current dir a package (plugin body)?
|
|
194
|
+
if "__init__.py" in files:
|
|
195
|
+
# === Case 1: Package Plugin (Folder is the plugin) ===
|
|
196
|
+
|
|
197
|
+
# Stop recursion into this folder
|
|
198
|
+
dirs[:] = []
|
|
199
|
+
|
|
200
|
+
entry_point = os.path.join(root, "__init__.py")
|
|
201
|
+
# Category is relative path to parent folder
|
|
202
|
+
rel_path = os.path.relpath(os.path.dirname(root), self.plugin_dir)
|
|
203
|
+
category = rel_path if rel_path != "." else ""
|
|
204
|
+
|
|
205
|
+
# Module name is the folder name
|
|
206
|
+
module_name = os.path.basename(root)
|
|
207
|
+
|
|
208
|
+
self._load_single_plugin(entry_point, module_name, category)
|
|
209
|
+
|
|
210
|
+
else:
|
|
211
|
+
# === Case 2: Category Folder (Load individual .py files) ===
|
|
212
|
+
|
|
213
|
+
# Category is relative path to current folder
|
|
214
|
+
rel_path = os.path.relpath(root, self.plugin_dir)
|
|
215
|
+
category = rel_path if rel_path != "." else ""
|
|
216
|
+
|
|
217
|
+
for filename in files:
|
|
218
|
+
if filename.endswith(".py") and not filename.startswith("__"):
|
|
219
|
+
entry_point = os.path.join(root, filename)
|
|
220
|
+
module_name = os.path.splitext(filename)[0]
|
|
123
221
|
|
|
222
|
+
self._load_single_plugin(entry_point, module_name, category)
|
|
223
|
+
|
|
224
|
+
return self.plugins
|
|
225
|
+
|
|
226
|
+
def _load_single_plugin(self, filepath, module_name, category):
|
|
227
|
+
"""Common loading logic for both single-file and package plugins."""
|
|
228
|
+
try:
|
|
229
|
+
# Ensure unique module name by including category path
|
|
230
|
+
# e.g. Analysis.Docking
|
|
231
|
+
unique_module_name = f"{category.replace(os.sep, '.')}.{module_name}" if category else module_name
|
|
232
|
+
unique_module_name = unique_module_name.strip(".")
|
|
233
|
+
|
|
234
|
+
spec = importlib.util.spec_from_file_location(unique_module_name, filepath)
|
|
235
|
+
if spec and spec.loader:
|
|
236
|
+
module = importlib.util.module_from_spec(spec)
|
|
237
|
+
sys.modules[spec.name] = module
|
|
238
|
+
|
|
239
|
+
# Inject category info
|
|
240
|
+
module.PLUGIN_CATEGORY = category
|
|
241
|
+
|
|
242
|
+
spec.loader.exec_module(module)
|
|
243
|
+
|
|
244
|
+
# --- Metadata Extraction ---
|
|
245
|
+
# Metadata
|
|
246
|
+
# Priority: PLUGIN_XXX > __xxx__ > Fallback
|
|
247
|
+
plugin_name = getattr(module, 'PLUGIN_NAME', module_name)
|
|
248
|
+
plugin_version = getattr(module, 'PLUGIN_VERSION', getattr(module, '__version__', 'Unknown'))
|
|
249
|
+
plugin_author = getattr(module, 'PLUGIN_AUTHOR', getattr(module, '__author__', 'Unknown'))
|
|
250
|
+
plugin_desc = getattr(module, 'PLUGIN_DESCRIPTION', getattr(module, '__doc__', ''))
|
|
251
|
+
plugin_category = getattr(module, 'PLUGIN_CATEGORY', category)
|
|
252
|
+
|
|
253
|
+
# Additional cleanup for docstring (strip whitespace)
|
|
254
|
+
if plugin_desc is None: plugin_desc = ""
|
|
255
|
+
plugin_desc = str(plugin_desc).strip()
|
|
256
|
+
|
|
257
|
+
# Handle version tuple
|
|
258
|
+
if isinstance(plugin_version, tuple):
|
|
259
|
+
plugin_version = ".".join(map(str, plugin_version))
|
|
260
|
+
|
|
261
|
+
# Interface compliance
|
|
262
|
+
has_run = hasattr(module, 'run') and callable(module.run)
|
|
263
|
+
has_autorun = hasattr(module, 'autorun') and callable(module.autorun)
|
|
264
|
+
has_init = hasattr(module, 'initialize') and callable(module.initialize)
|
|
265
|
+
|
|
266
|
+
status = "Loaded"
|
|
267
|
+
|
|
268
|
+
# Execute initialization
|
|
269
|
+
if has_init:
|
|
270
|
+
context = PluginContext(self, plugin_name)
|
|
271
|
+
# Pass category to context if needed, currently not storing it in context directly but could be useful
|
|
124
272
|
try:
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
spec = importlib.util.spec_from_file_location(module_name, filepath)
|
|
128
|
-
if spec and spec.loader:
|
|
129
|
-
module = importlib.util.module_from_spec(spec)
|
|
130
|
-
sys.modules[spec.name] = module
|
|
131
|
-
spec.loader.exec_module(module)
|
|
132
|
-
|
|
133
|
-
# --- Metadata Extraction ---
|
|
134
|
-
plugin_name = getattr(module, 'PLUGIN_NAME', filename[:-3])
|
|
135
|
-
plugin_version = getattr(module, 'PLUGIN_VERSION', getattr(module, '__version__', 'Unknown'))
|
|
136
|
-
plugin_author = getattr(module, 'PLUGIN_AUTHOR', getattr(module, '__author__', 'Unknown'))
|
|
137
|
-
plugin_desc = getattr(module, 'PLUGIN_DESCRIPTION', getattr(module, '__doc__', ''))
|
|
138
|
-
|
|
139
|
-
# Clean up docstring if used as description
|
|
140
|
-
if plugin_desc:
|
|
141
|
-
plugin_desc = plugin_desc.strip().split('\n')[0]
|
|
142
|
-
|
|
143
|
-
# check for interface compliance
|
|
144
|
-
has_run = hasattr(module, 'run') and callable(module.run)
|
|
145
|
-
has_autorun = hasattr(module, 'autorun') and callable(module.autorun)
|
|
146
|
-
has_init = hasattr(module, 'initialize') and callable(module.initialize)
|
|
147
|
-
|
|
148
|
-
status = "Loaded"
|
|
149
|
-
|
|
150
|
-
# Execute loading logic
|
|
151
|
-
if has_init:
|
|
152
|
-
context = PluginContext(self, plugin_name)
|
|
153
|
-
try:
|
|
154
|
-
module.initialize(context)
|
|
155
|
-
except Exception as e:
|
|
156
|
-
status = f"Error (Init): {e}"
|
|
157
|
-
print(f"Plugin {plugin_name} initialize error: {e}")
|
|
158
|
-
traceback.print_exc()
|
|
159
|
-
elif has_autorun:
|
|
160
|
-
try:
|
|
161
|
-
if self.main_window:
|
|
162
|
-
module.autorun(self.main_window)
|
|
163
|
-
else:
|
|
164
|
-
status = "Skipped (No MW)"
|
|
165
|
-
except Exception as e:
|
|
166
|
-
status = f"Error (Autorun): {e}"
|
|
167
|
-
print(f"Plugin {plugin_name} autorun error: {e}")
|
|
168
|
-
traceback.print_exc()
|
|
169
|
-
elif not has_run:
|
|
170
|
-
status = "No Entry Point"
|
|
171
|
-
|
|
172
|
-
self.plugins.append({
|
|
173
|
-
'name': plugin_name,
|
|
174
|
-
'version': plugin_version,
|
|
175
|
-
'author': plugin_author,
|
|
176
|
-
'description': plugin_desc,
|
|
177
|
-
'module': module,
|
|
178
|
-
'rel_folder': rel_folder,
|
|
179
|
-
'status': status,
|
|
180
|
-
'filepath': filepath,
|
|
181
|
-
'has_run': has_run # for menu manual run
|
|
182
|
-
})
|
|
183
|
-
|
|
273
|
+
module.initialize(context)
|
|
184
274
|
except Exception as e:
|
|
185
|
-
|
|
275
|
+
status = f"Error (Init): {e}"
|
|
276
|
+
print(f"Plugin {plugin_name} initialize error: {e}")
|
|
186
277
|
traceback.print_exc()
|
|
187
|
-
|
|
188
|
-
|
|
278
|
+
elif has_autorun:
|
|
279
|
+
try:
|
|
280
|
+
if self.main_window:
|
|
281
|
+
module.autorun(self.main_window)
|
|
282
|
+
else:
|
|
283
|
+
status = "Skipped (No MW)"
|
|
284
|
+
except Exception as e:
|
|
285
|
+
status = f"Error (Autorun): {e}"
|
|
286
|
+
print(f"Plugin {plugin_name} autorun error: {e}")
|
|
287
|
+
traceback.print_exc()
|
|
288
|
+
elif not has_run:
|
|
289
|
+
status = "No Entry Point"
|
|
290
|
+
|
|
291
|
+
self.plugins.append({
|
|
292
|
+
'name': plugin_name,
|
|
293
|
+
'version': plugin_version,
|
|
294
|
+
'author': plugin_author,
|
|
295
|
+
'description': plugin_desc,
|
|
296
|
+
'module': module,
|
|
297
|
+
'category': plugin_category, # Store category
|
|
298
|
+
'status': status,
|
|
299
|
+
'filepath': filepath,
|
|
300
|
+
'has_run': has_run
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
except Exception as e:
|
|
304
|
+
print(f"Failed to load plugin {module_name}: {e}")
|
|
305
|
+
traceback.print_exc()
|
|
189
306
|
|
|
190
307
|
def run_plugin(self, module, main_window):
|
|
191
308
|
"""Executes the plugin's run method (Legacy manual trigger)."""
|
|
@@ -266,29 +383,50 @@ class PluginManager:
|
|
|
266
383
|
tree = ast.parse(f.read())
|
|
267
384
|
|
|
268
385
|
for node in tree.body:
|
|
386
|
+
targets = []
|
|
269
387
|
if isinstance(node, ast.Assign):
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
388
|
+
targets = node.targets
|
|
389
|
+
elif isinstance(node, ast.AnnAssign):
|
|
390
|
+
targets = [node.target]
|
|
391
|
+
|
|
392
|
+
for target in targets:
|
|
393
|
+
if isinstance(target, ast.Name):
|
|
394
|
+
# Helper to extract value
|
|
395
|
+
val = None
|
|
396
|
+
if node.value: # AnnAssign might presumably not have value? (though usually does for module globals)
|
|
274
397
|
if isinstance(node.value, ast.Constant): # Py3.8+
|
|
275
398
|
val = node.value.value
|
|
276
399
|
elif hasattr(ast, 'Str') and isinstance(node.value, ast.Str): # Py3.7 and below
|
|
277
400
|
val = node.value.s
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
401
|
+
elif isinstance(node.value, ast.Tuple):
|
|
402
|
+
# Handle version tuples e.g. (1, 0, 0)
|
|
403
|
+
try:
|
|
404
|
+
# Extract simple constants from tuple
|
|
405
|
+
elts = []
|
|
406
|
+
for elt in node.value.elts:
|
|
407
|
+
if isinstance(elt, ast.Constant):
|
|
408
|
+
elts.append(elt.value)
|
|
409
|
+
elif hasattr(ast, 'Num') and isinstance(elt, ast.Num):
|
|
410
|
+
elts.append(elt.n)
|
|
411
|
+
val = ".".join(map(str, elts))
|
|
412
|
+
except:
|
|
413
|
+
pass
|
|
414
|
+
|
|
415
|
+
if val is not None:
|
|
416
|
+
if target.id == 'PLUGIN_NAME':
|
|
417
|
+
info['name'] = val
|
|
418
|
+
elif target.id == 'PLUGIN_VERSION':
|
|
419
|
+
info['version'] = val
|
|
420
|
+
elif target.id == 'PLUGIN_AUTHOR':
|
|
421
|
+
info['author'] = val
|
|
422
|
+
elif target.id == 'PLUGIN_DESCRIPTION':
|
|
423
|
+
info['description'] = val
|
|
424
|
+
elif target.id == 'PLUGIN_CATEGORY':
|
|
425
|
+
info['category'] = val
|
|
426
|
+
elif target.id == '__version__' and info['version'] == 'Unknown':
|
|
427
|
+
info['version'] = val
|
|
428
|
+
elif target.id == '__author__' and info['author'] == 'Unknown':
|
|
429
|
+
info['author'] = val
|
|
292
430
|
|
|
293
431
|
# Docstring extraction
|
|
294
432
|
if isinstance(node, ast.Expr):
|
|
@@ -18,6 +18,7 @@ from PyQt6.QtWidgets import (
|
|
|
18
18
|
)
|
|
19
19
|
from PyQt6.QtCore import Qt, QMimeData, QUrl
|
|
20
20
|
from PyQt6.QtGui import QDragEnterEvent, QDropEvent, QDesktopServices
|
|
21
|
+
import shutil
|
|
21
22
|
|
|
22
23
|
class PluginManagerWindow(QDialog):
|
|
23
24
|
def __init__(self, plugin_manager, parent=None):
|
|
@@ -33,8 +34,7 @@ class PluginManagerWindow(QDialog):
|
|
|
33
34
|
def init_ui(self):
|
|
34
35
|
layout = QVBoxLayout(self)
|
|
35
36
|
|
|
36
|
-
|
|
37
|
-
lbl_info = QLabel("Drag & Drop .py files here to install plugins.")
|
|
37
|
+
lbl_info = QLabel("Drag & Drop .py or .zip files here to install plugins.")
|
|
38
38
|
lbl_info.setStyleSheet("color: gray; font-style: italic;")
|
|
39
39
|
layout.addWidget(lbl_info)
|
|
40
40
|
|
|
@@ -152,16 +152,31 @@ class PluginManagerWindow(QDialog):
|
|
|
152
152
|
filepath = plugin.get('filepath')
|
|
153
153
|
|
|
154
154
|
if filepath and os.path.exists(filepath):
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
155
|
+
# Check if it is a package plugin (based on __init__.py)
|
|
156
|
+
is_package = os.path.basename(filepath) == "__init__.py"
|
|
157
|
+
target_path = os.path.dirname(filepath) if is_package else filepath
|
|
158
|
+
|
|
159
|
+
msg = f"Are you sure you want to remove '{plugin.get('name', 'Unknown')}'?"
|
|
160
|
+
if is_package:
|
|
161
|
+
msg += f"\n\nThis will delete the entire folder:\n{target_path}"
|
|
162
|
+
else:
|
|
163
|
+
msg += f"\n\nFile: {filepath}"
|
|
164
|
+
|
|
165
|
+
msg += "\nThis cannot be undone."
|
|
166
|
+
|
|
167
|
+
reply = QMessageBox.question(self, "Remove Plugin", msg,
|
|
168
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
169
|
+
if reply == QMessageBox.StandardButton.Yes:
|
|
170
|
+
try:
|
|
171
|
+
if is_package:
|
|
172
|
+
shutil.rmtree(target_path)
|
|
173
|
+
else:
|
|
174
|
+
os.remove(target_path)
|
|
175
|
+
|
|
176
|
+
self.on_reload(silent=True) # Reload list and plugins
|
|
177
|
+
QMessageBox.information(self, "Success", f"Removed '{plugin.get('name', 'Unknown')}'.")
|
|
178
|
+
except Exception as e:
|
|
179
|
+
QMessageBox.critical(self, "Error", f"Failed to delete plugin: {e}")
|
|
165
180
|
else:
|
|
166
181
|
QMessageBox.warning(self, "Error", f"Plugin file not found:\n{filepath}")
|
|
167
182
|
|
|
@@ -189,9 +204,47 @@ class PluginManagerWindow(QDialog):
|
|
|
189
204
|
errors = []
|
|
190
205
|
for url in event.mimeData().urls():
|
|
191
206
|
file_path = url.toLocalFile()
|
|
192
|
-
|
|
207
|
+
|
|
208
|
+
is_valid = False
|
|
209
|
+
is_zip = False
|
|
210
|
+
is_folder = False
|
|
211
|
+
|
|
212
|
+
if os.path.isfile(file_path):
|
|
213
|
+
# Special handling: If user drops __init__.py, assume they want to install the package (folder)
|
|
214
|
+
if os.path.basename(file_path) == "__init__.py":
|
|
215
|
+
file_path = os.path.dirname(file_path)
|
|
216
|
+
is_valid = True
|
|
217
|
+
is_folder = True
|
|
218
|
+
elif file_path.endswith('.py'):
|
|
219
|
+
is_valid = True
|
|
220
|
+
elif file_path.endswith('.zip'):
|
|
221
|
+
is_valid = True
|
|
222
|
+
is_zip = True
|
|
223
|
+
|
|
224
|
+
if os.path.isdir(file_path):
|
|
225
|
+
# Check for __init__.py to confirm it's a plugin package?
|
|
226
|
+
# Or just assume any folder is fair game (could be category folder too?)
|
|
227
|
+
# We'll allow any folder and let manager handle it.
|
|
228
|
+
is_valid = True
|
|
229
|
+
is_folder = True
|
|
230
|
+
|
|
231
|
+
if is_valid:
|
|
193
232
|
# Extract info and confirm
|
|
194
|
-
info =
|
|
233
|
+
info = {'name': os.path.basename(file_path), 'version': 'Unknown', 'author': 'Unknown', 'description': ''}
|
|
234
|
+
|
|
235
|
+
if is_folder:
|
|
236
|
+
info['description'] = "Folder Plugin / Category"
|
|
237
|
+
# Try to parse __init__.py if it exists
|
|
238
|
+
init_path = os.path.join(file_path, "__init__.py")
|
|
239
|
+
if os.path.exists(init_path):
|
|
240
|
+
info = self.plugin_manager.get_plugin_info_safe(init_path)
|
|
241
|
+
info['description'] += f" (Package: {info['name']})"
|
|
242
|
+
|
|
243
|
+
elif is_zip:
|
|
244
|
+
info['description'] = "ZIP Package Plugin"
|
|
245
|
+
elif file_path.endswith('.py'):
|
|
246
|
+
info = self.plugin_manager.get_plugin_info_safe(file_path)
|
|
247
|
+
|
|
195
248
|
msg = (f"Do you want to install this plugin?\n\n"
|
|
196
249
|
f"Name: {info['name']}\n"
|
|
197
250
|
f"Author: {info['author']}\n"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: MoleditPy
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.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
|
|
@@ -12,7 +12,7 @@ moleditpy/modules/bond_item.py,sha256=eVkEeKvM4igYI67DYxpey3FllqDyt_iWDo4VPYMhaP
|
|
|
12
12
|
moleditpy/modules/bond_length_dialog.py,sha256=6bFPGssnqlgINuqpxLv-OhjMH3_hspnaH8QtorAyu2M,14782
|
|
13
13
|
moleditpy/modules/calculation_worker.py,sha256=KiGQY7i-QCQofEoE0r65KoQgpEGFcbhmxWv6egfkUdc,42324
|
|
14
14
|
moleditpy/modules/color_settings_dialog.py,sha256=Ow44BhCOLo0AFb6klO001k6B4drOgKX9DeNBQhZLp5o,15474
|
|
15
|
-
moleditpy/modules/constants.py,sha256=
|
|
15
|
+
moleditpy/modules/constants.py,sha256=oCixX94SBFCkjSZCVrrfBfieq1LgawOd8UMH_pPyKSY,4702
|
|
16
16
|
moleditpy/modules/constrained_optimization_dialog.py,sha256=REsk4ePsqNmAGPMTS_jckeM7jexrU3krwun8sKqKUCs,30062
|
|
17
17
|
moleditpy/modules/custom_interactor_style.py,sha256=LDNODMJoNHGe1AUSrvqv6PdeJm-hpPmSpWINppnJLt0,38942
|
|
18
18
|
moleditpy/modules/custom_qt_interactor.py,sha256=vCZsDfRO-FtphD5cTP7Ps-5rpHZMIGloaoe6EaKzrsw,4139
|
|
@@ -25,7 +25,7 @@ moleditpy/modules/main_window_dialog_manager.py,sha256=QR96LqHAPSOShXbc9cK-Ffq8a
|
|
|
25
25
|
moleditpy/modules/main_window_edit_3d.py,sha256=CUArB5wcsgq1C7LygAEC6URlbnn4RhRYDa5n-Y-etWI,19731
|
|
26
26
|
moleditpy/modules/main_window_edit_actions.py,sha256=yEc0Nw-VpN0P4e4neUu7pDuUHPGEcu6eFmwWFrSBIQ8,64815
|
|
27
27
|
moleditpy/modules/main_window_export.py,sha256=dSVfylsybDDboDuXU9Inotf6YkrKJwgBTqGYSfq1lRE,38241
|
|
28
|
-
moleditpy/modules/main_window_main_init.py,sha256=
|
|
28
|
+
moleditpy/modules/main_window_main_init.py,sha256=BChZuyXgWfOTFqAM1OUukFgtL5KLusW4t29GnfCN1QE,92018
|
|
29
29
|
moleditpy/modules/main_window_molecular_parsers.py,sha256=KR6vzuqc3nutOcorpYr0QOyX3MFBcxTwDhZX96VgJ9Q,48291
|
|
30
30
|
moleditpy/modules/main_window_project_io.py,sha256=TWwtuKDuvgcvPZ9IGmW8r1EJJOrgxrIJRnxe_f4C1oM,17149
|
|
31
31
|
moleditpy/modules/main_window_string_importers.py,sha256=v47wOd4RtjKYcF-aLP-mogGGdYTpTEo3dDyAu79_5MM,10782
|
|
@@ -39,8 +39,8 @@ moleditpy/modules/move_group_dialog.py,sha256=Fyuy3Uq1KsFsk9qR96r_FxPbAM_-zSfW2d
|
|
|
39
39
|
moleditpy/modules/periodic_table_dialog.py,sha256=ItEZUts1XCietz9paY-spvbzxh6SXak3GnikwqkHZCw,4006
|
|
40
40
|
moleditpy/modules/planarize_dialog.py,sha256=eaqI1MpF35e-VUMpJATt-EtGG5FhcSUlbAenUaFGabY,8593
|
|
41
41
|
moleditpy/modules/plugin_interface.py,sha256=srzPZ3a_aRTx28NAvWNKRVUDYQNfQOTcjzx-5YW2Pb4,8164
|
|
42
|
-
moleditpy/modules/plugin_manager.py,sha256=
|
|
43
|
-
moleditpy/modules/plugin_manager_window.py,sha256=
|
|
42
|
+
moleditpy/modules/plugin_manager.py,sha256=J-BeUGvOFa4v0djYln_Olurr6JVWDAETcYMwOlZ0VkI,19998
|
|
43
|
+
moleditpy/modules/plugin_manager_window.py,sha256=b4kEv0DaWHZG76ZaFTOxn6CVtA62_0MpPYYr10ehCtA,12544
|
|
44
44
|
moleditpy/modules/settings_dialog.py,sha256=Nr7yE8UmYRi3VObWvRlrnv0DnjSjmYXbvqryZ02O12k,65348
|
|
45
45
|
moleditpy/modules/template_preview_item.py,sha256=djdq3tz73d_fJGOvai3E-V9Hk9q9ZW7skx7BV59mooA,6556
|
|
46
46
|
moleditpy/modules/template_preview_view.py,sha256=4OCHZDO51BvJpKdfrBWJ4_4WfLfFSKxsVIyf7I-Kj2E,3350
|
|
@@ -51,9 +51,9 @@ moleditpy/modules/assets/file_icon.ico,sha256=yyVj084A7HuMNbV073cE_Ag3Ne405qgOP3
|
|
|
51
51
|
moleditpy/modules/assets/icon.icns,sha256=wD5R6-Vw7K662tVKhu2E1ImN0oUuyAP4youesEQsn9c,139863
|
|
52
52
|
moleditpy/modules/assets/icon.ico,sha256=RfgFcx7-dHY_2STdsOQCQziY5SNhDr3gPnjO6jzEDPI,147975
|
|
53
53
|
moleditpy/modules/assets/icon.png,sha256=kCFN1WacYIdy0GN6SFEbNA00ef39pCczBnFdkkBI8Bs,147110
|
|
54
|
-
moleditpy-2.
|
|
55
|
-
moleditpy-2.
|
|
56
|
-
moleditpy-2.
|
|
57
|
-
moleditpy-2.
|
|
58
|
-
moleditpy-2.
|
|
59
|
-
moleditpy-2.
|
|
54
|
+
moleditpy-2.3.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
55
|
+
moleditpy-2.3.1.dist-info/METADATA,sha256=7uof_qhynKs1RVqwxoQjfE6oOUH-zs53C2Plo15-hZk,60629
|
|
56
|
+
moleditpy-2.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
57
|
+
moleditpy-2.3.1.dist-info/entry_points.txt,sha256=yH1h9JjALhok1foXT3-hYrC4ufoZt8b7oiBcsdnGNNM,54
|
|
58
|
+
moleditpy-2.3.1.dist-info/top_level.txt,sha256=ARICrS4ihlPXqywlKl6o-oJa3Qz3gZRWu_VZsQ3_c44,10
|
|
59
|
+
moleditpy-2.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|