MoleditPy Static API Checker
This directory contains the tools used to maintain API compatibility between the MoleditPy core application and its extensive plugin ecosystem. These tools use Static Analysis (via the Python ast module) to detect broken attribute accesses without actually executing the code.
Core Scripts
plugin_api_checker.py (The Engine)
The core scanning logic. It is a standalone, portable tool that requires explicit paths for both the application and the plugin. Use this for general-purpose auditing or when working on a single plugin outside of this repository.
check_api.py (The Repo Runner)
A repository-specific wrapper pre-configured for the moleditpy-plugins environment. Its primary advantages are:
- Registry Support: Can scan all plugins listed in
REGISTRY/plugins.jsonin one pass. - Pre-configured Paths: Defaults to the standard relative paths for this workspace layout.
- CI Integration: Used by the automated test suite to ensure the entire collection remains compatible.
Technical Architecture
Phase 1: API Surface Extraction
Before checking any plugins, the tool builds a truth map of the application API:
- MainWindow Analysis: Parses
main_window.pyto index all methods, class attributes, and signals. - Manager Discovery: Identifies attributes on
MainWindowthat are assigned instances of known Manager classes (e.g.View3DManager,StateManager). - Cross-File Parsing: For every discovered Manager, locates the source file and indexes public methods,
@propertygetters, and attributes assigned toselfin__init__and setup methods. - Host-Delegation Support: Scans every
.pyfile in the core app for theself.host.xxx = ...pattern, handling cases where a Manager dynamically adds an attribute toMainWindow.
Phase 2: Plugin Verification
Plugin files are scanned in two passes:
- Pass 1 (Alias Tracking): Identifies which variables hold references to the application — standard names (
mw,main_window), method calls (context.get_main_window()), and propagated aliases (self.mw = mw). - Pass 2 (Access Checking): Examines every attribute access.
- Direct access:
mw.some_methodmust exist onMainWindow. - Chained access:
mw.manager.attrmust verify bothmanageronMainWindowandattron the Manager class.
- Direct access:
Allowlist Design
There are three allowlist mechanisms with different scopes and risk profiles:
| Source | What it suppresses | Activated by | Risk |
|---|---|---|---|
_MANAGER_ALLOWLIST |
Manager attrs set via self.host.manager.X = ... (AST-invisible) |
--default-allowlist |
Low — confirmed false positives |
_MW_ALLOWLIST |
Direct mw.X legacy compat bridge attrs (mw.host, mw.view3d, …) |
--mw-allowlist |
High — can hide real V3 migration bugs |
.moleditpy-api-allowlist |
Per-repo site-specific attrs (e.g. hasattr-guarded V2 compat patterns) |
Auto-discovered | Low — opt-in per plugin repo |
Rule of thumb: always pass --default-allowlist. Only pass --mw-allowlist when you know the plugin intentionally uses V2 compat patterns. Use .moleditpy-api-allowlist in the plugin repo for per-repo suppressions.
.moleditpy-api-allowlist — per-repo site allowlist
Place this JSON file in the plugin repo root. Both plugin_api_checker.py and check_api.py auto-discover it by walking up from the --plugin path — no flag required.
Each value accepts a list (attrs only) or an object (attr → reason). The reason strings are ignored by the checker and serve as inline comments explaining why each attr is safe to skip:
{
"mw": {
"select_all": "hasattr-guarded V2 compat — interaction.py:702",
"last_open_path": "hasattr-guarded V2 compat — mode_manager.py:817"
},
"manager": {
"state_manager": {
"data": "runtime-injected by molecule loader, not in __init__"
}
}
}
List form also accepted (no comments):
{ "mw": ["attr1", "attr2"] }
Use this for attrs that are accessed via hasattr() guards in the plugin — the scanner cannot see through hasattr at the static level, so these are safe false positives specific to that repo.
Example: moleditpy_reaction_sketcher_plugin/.moleditpy-api-allowlist suppresses 6 V2 compat attrs that are all guarded by hasattr(mw, ...) in interaction.py, mode_manager.py, and patcher.py.
Usage Guide
For External Developers (Recommended)
Use plugin_api_checker.py for standalone plugin development. It is portable and requires explicit paths.
Routine scan (suppress manager false positives):
python api-checker/plugin_api_checker.py --app path/to/python_molecular_editor --plugin path/to/MyPlugin --default-allowlist
Full audit (also suppress legacy mw.X compat bridges):
python api-checker/plugin_api_checker.py --app path/to/python_molecular_editor --plugin path/to/MyPlugin --default-allowlist --mw-allowlist
For moleditpy-plugins Contributors (Internal)
The primary way to verify the repository is to scan all visible plugins listed in the registry.
Full Registry Scan (Recommended for maintenance):
This command parses REGISTRY/plugins.json and scans only the plugins where "visible": true.
python api-checker/check_api.py --app ../python_molecular_editor --registry --default-allowlist --mw-allowlist
Individual Plugin Scan: Use this while developing a specific plugin before adding it to the registry.
python api-checker/check_api.py --app ../python_molecular_editor --plugin plugins/MyPlugin --default-allowlist
Main app source/test scan: Verify that the core application’s own internal logic correctly uses the V3 manager API.
# Using the generic engine for intra-app audit:
python api-checker/plugin_api_checker.py --app ../python_molecular_editor --plugin ../python_molecular_editor/moleditpy/src --default-allowlist
# Scan the main app's test suite:
python api-checker/plugin_api_checker.py --app ../python_molecular_editor --plugin ../python_molecular_editor/tests --default-allowlist
Show the detected API surface
python api-checker/plugin_api_checker.py --app path/to/python_molecular_editor --show-api
Handling False Positives
Static analysis cannot see everything that happens at runtime. The tool uses several mechanisms to reduce noise:
- Inherited Methods: A built-in list (
_QT_INHERITED) of standardQWidget/QMainWindowmethods (setVisible,show,resize, …) that are always permitted. - Try-Block Heuristic: Accesses inside a
try: ... except:block are flagged with a[try]tag. These often indicate the developer has provided a fallback and are safe to suppress with--skip-try. - Manager Allowlist (
--default-allowlist): Suppresses manager attrs set viaself.host.manager.X = ...patterns invisible to AST (e.g.init_manager.scene,state_manager.data). Safe to enable in all scans. - MW Allowlist (
--mw-allowlist): Suppresses directmw.Xlegacy compat bridge attrs. Off by default — enabling it can hide real V3 migration bugs.
mw.host and mw.view3d — why they are false positives
The scanner flags these as UNKNOWN_MW_ATTR because neither exists as a declared attribute on MainWindow in V3. However, all plugin usages are guarded by hasattr:
def run(mw):
if hasattr(mw, 'host'): # V2 compat: unwrap host wrapper object
mw = mw.host
if hasattr(mw, 'view3d'): # V2 compat: optional direct 3D view shortcut
mw.view3d.draw_standard_3d_style(mol, style_override='ball_and_stick')
mw.host— V2-era pattern where plugins were passed a wrapper object holding the realMainWindowas.host. In V3 the wrapper is gone;hasattrreturnsFalseand the unwrap is skipped.mw.view3d— V2 shortcut to the 3D view widget, moved tomw.view_3d_managerin V3. Thehasattrguard makes the old call a no-op, not a crash.
Suppress with --mw-allowlist for routine scans. Leave visible when auditing a plugin that should be fully migrated to V3.
Scanning Test Code
Scanning a test directory produces false positives from patterns the AST scanner cannot see:
Pattern 1 — cls.X = lambda monkey-patching (e.g. gui/conftest.py)
Test conftest files patch MainWindow with proxy lambdas for the duration of the test session:
cls.halt_all_calculations = lambda self: self.compute_manager.halt_conversion()
cls.toggle_atom_info_display = lambda self, m: self.view_3d_manager.toggle_atom_info_display(m)
cls.close_all_3d_edit_dialogs = lambda self: self.edit_3d_manager.close_all_3d_edit_dialogs()
When those methods are later called on main_window, the scanner flags them as UNKNOWN_MW_ATTR — it saw the usage but not the cls.X = lambda definition, because the scanner only tracks def X(self) style definitions.
Pattern 2 — MainWindow subclassing (e.g. unit/test_app_state_persistence.py)
Unit tests sometimes define a thin subclass that forwards manager methods:
class TestableMainWindow(MainWindow):
def create_json_data(self): return super().create_json_data()
def clear_2d_editor(self, push_to_undo=True): ...
The scanner sees mw.create_json_data() and flags it, because those methods are not declared on MainWindow itself.
Verdict: These are false positives. Do not add them to _MW_ALLOWLIST and do not fix the test code. Ignore UNKNOWN_MW_ATTR hits when scanning test directories, or pass --mw-allowlist scoped to test runs only.
Maintenance & Troubleshooting
Attribute flagged as “Not Found” but it is valid
- Check visibility: Ensure the attribute is assigned to
selfin the Manager’s__init__or a named setup method — not inside a helper called indirectly. - Check host-delegation: If set via
self.host.attr = ..., ensure the scanner is pointed at the file containing that assignment. - Update the Manager Allowlist: If the attribute is genuinely runtime-injected and confirmed valid, add it to
_MANAGER_ALLOWLISTincheck_api.py. Do not use_MW_ALLOWLISTunless it is a directmw.Xlegacy bridge.
Error Codes
| Code | Meaning |
|---|---|
[UNKNOWN_MW_ATTR] |
Attribute called directly on MainWindow — not found in its API surface. |
[UNKNOWN_MANAGER_ATTR] |
Attribute called on a sub-manager (e.g. mw.io_manager.xxx) — not found on that Manager class. |
[UNKNOWN_CONTEXT_ATTR] |
Attribute called on the PluginContext object — not found in the plugin interface. |
[try] prefix |
The access is inside a try: ... except: block — likely has a fallback; lower priority. |