MoleditPy-linux 4.12.1__tar.gz → 4.13.0__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.12.1 → moleditpy_linux-4.13.0}/PKG-INFO +1 -1
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/pyproject.toml +1 -1
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/MoleditPy_linux.egg-info/PKG-INFO +1 -1
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/core/molecular_data.py +95 -1
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/core/stereo_check.py +134 -7
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/app_state.py +4 -2
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/atom_item.py +91 -1
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/compute_logic.py +1 -1
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/edit_actions_logic.py +15 -2
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/main_window_init.py +19 -7
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/molecular_scene_handler.py +93 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/molecule_scene.py +45 -5
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/ui_manager.py +6 -1
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/zoomable_view.py +1 -1
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/LICENSE +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/README.md +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/setup.cfg +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/MoleditPy_linux.egg-info/SOURCES.txt +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/MoleditPy_linux.egg-info/dependency_links.txt +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/MoleditPy_linux.egg-info/entry_points.txt +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/MoleditPy_linux.egg-info/requires.txt +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/MoleditPy_linux.egg-info/top_level.txt +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/__init__.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/__main__.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/assets/file_icon.ico +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/assets/icon.icns +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/assets/icon.ico +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/assets/icon.png +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/core/__init__.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/core/mol_geometry.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/main.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/plugins/__init__.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/plugins/plugin_interface.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/plugins/plugin_manager.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/plugins/plugin_manager_window.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/__init__.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/about_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/align_plane_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/alignment_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/analysis_window.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/angle_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/atom_picking.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/base_picking_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/bond_item.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/bond_length_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/calculation_worker.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/chain_mixin.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/chirality_warning_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/color_settings_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/constrained_optimization_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/custom_interactor_style.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/custom_qt_interactor.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/dialog_3d_picking_mixin.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/dialog_logic.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/dihedral_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/edit_3d_logic.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/export_logic.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/geometry_base_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/io_logic.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/main_window.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/mirror_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/move_dialog_mixin.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/move_group_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/move_selected_atoms_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/periodic_table_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/planarize_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/plugin_menu_manager.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/preview_molecule.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/sdf_record_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/settings_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/settings_tabs/__init__.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/settings_tabs/settings_2d_cleanup_tab.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/settings_tabs/settings_2d_tab.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/settings_tabs/settings_3d_tabs.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/settings_tabs/settings_labels_tab.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/settings_tabs/settings_other_tab.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/settings_tabs/settings_tab_base.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/string_importers.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/template_preview_item.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/template_preview_view.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/translation_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/user_template_dialog.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/view_3d_logic.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/utils/__init__.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/utils/constants.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/utils/default_settings.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/utils/finite_float.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/utils/hit_radius.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/utils/label_style.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/utils/sdf_records.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/utils/sip_isdeleted_safe.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/utils/suppress_log.py +0 -0
- {moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/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.
|
|
3
|
+
Version: 4.13.0
|
|
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.
|
|
3
|
+
Version: 4.13.0
|
|
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.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/core/molecular_data.py
RENAMED
|
@@ -390,6 +390,85 @@ def resolve_wedge_chirality(mol: Chem.Mol) -> None:
|
|
|
390
390
|
logging.warning("Resolving wedge stereochemistry failed: %s", e)
|
|
391
391
|
|
|
392
392
|
|
|
393
|
+
RS_LABELS = ("R", "S")
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def rs_entry(atom: Dict[str, Any]) -> Dict[str, str]:
|
|
397
|
+
"""{"rs": label} when *atom* carries a valid R/S label, else {}.
|
|
398
|
+
|
|
399
|
+
Spread into serialized atom records so an unlabelled atom is written
|
|
400
|
+
exactly as before the label existed.
|
|
401
|
+
"""
|
|
402
|
+
label = atom.get("rs")
|
|
403
|
+
return {"rs": label} if label in RS_LABELS else {}
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _tag_for_label(
|
|
407
|
+
mol: Chem.Mol, center: int, label: str, tags: List[Chem.ChiralType]
|
|
408
|
+
) -> Optional[Chem.ChiralType]:
|
|
409
|
+
"""Chiral tag that gives *center* CIP *label*, or None if it has no R/S.
|
|
410
|
+
|
|
411
|
+
The other centers keep *tags* (their drawn configuration), since a
|
|
412
|
+
center's CIP label can depend on them.
|
|
413
|
+
"""
|
|
414
|
+
cw, ccw = Chem.ChiralType.CHI_TETRAHEDRAL_CW, Chem.ChiralType.CHI_TETRAHEDRAL_CCW
|
|
415
|
+
probe = Chem.Mol(mol)
|
|
416
|
+
for atom, other in zip(probe.GetAtoms(), tags):
|
|
417
|
+
atom.SetChiralTag(other)
|
|
418
|
+
probe.GetAtomWithIdx(center).SetChiralTag(cw)
|
|
419
|
+
Chem.AssignStereochemistry(probe, cleanIt=True, force=True)
|
|
420
|
+
if probe.GetAtomWithIdx(center).GetChiralTag() != cw:
|
|
421
|
+
return None # perception dropped it: not a stereocenter
|
|
422
|
+
rdCIPLabeler.AssignCIPLabels(probe)
|
|
423
|
+
atom = probe.GetAtomWithIdx(center)
|
|
424
|
+
code = atom.GetProp("_CIPCode") if atom.HasProp("_CIPCode") else None
|
|
425
|
+
if code not in RS_LABELS:
|
|
426
|
+
return None # no CIP label, or pseudo-asymmetric (r/s)
|
|
427
|
+
# The mirror tag gives the mirror label.
|
|
428
|
+
return cw if code == label else ccw
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def apply_rs_labels(mol: Chem.Mol, wanted: Dict[int, str]) -> None:
|
|
432
|
+
"""Re-wedge each labelled center of *mol* so it reads as its R/S label.
|
|
433
|
+
|
|
434
|
+
*wanted* maps atom index to "R" or "S". A label is the user's explicit
|
|
435
|
+
choice, so it overrides whatever the wedges and hashes at that center
|
|
436
|
+
say; it is written back as wedges because that is what every later
|
|
437
|
+
reader (MOL block, 3D embedding, stereo check) takes chirality from.
|
|
438
|
+
Labels on atoms that are not stereocenters are ignored, and other
|
|
439
|
+
centers keep their drawn configuration.
|
|
440
|
+
"""
|
|
441
|
+
if not wanted or mol.GetNumConformers() == 0:
|
|
442
|
+
return
|
|
443
|
+
try:
|
|
444
|
+
read = _rdkit_tags(mol)
|
|
445
|
+
fixes: Dict[int, Chem.ChiralType] = {}
|
|
446
|
+
for center, label in wanted.items():
|
|
447
|
+
tag = _tag_for_label(mol, center, label, read)
|
|
448
|
+
if tag is None:
|
|
449
|
+
logging.debug("R/S label on atom %d ignored: no stereocenter", center)
|
|
450
|
+
elif tag != read[center]:
|
|
451
|
+
fixes[center] = tag
|
|
452
|
+
if not fixes:
|
|
453
|
+
return
|
|
454
|
+
expected = [fixes.get(i, tag) for i, tag in enumerate(read)]
|
|
455
|
+
for rewedge in (_rdkit_rewedge, _clear_and_pick_wedges):
|
|
456
|
+
trial = Chem.Mol(mol)
|
|
457
|
+
rewedge(trial, fixes)
|
|
458
|
+
if _rdkit_tags(trial) == expected:
|
|
459
|
+
rewedge(mol, fixes)
|
|
460
|
+
return
|
|
461
|
+
logging.warning("Could not draw R/S labels on atoms %s", sorted(fixes))
|
|
462
|
+
except (RuntimeError, ValueError) as e:
|
|
463
|
+
logging.warning("Applying R/S labels failed: %s", e)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _clear_and_pick_wedges(mol: Chem.Mol, fixes: Dict[int, Chem.ChiralType]) -> None:
|
|
467
|
+
"""Drop the wedges at *fixes* and pick one free bond per center afresh."""
|
|
468
|
+
_clear_own_wedges(mol, fixes)
|
|
469
|
+
_pick_wedges(mol, fixes)
|
|
470
|
+
|
|
471
|
+
|
|
393
472
|
class MolecularData:
|
|
394
473
|
"""In-memory graph of atoms and bonds, independent of any UI framework."""
|
|
395
474
|
|
|
@@ -511,10 +590,14 @@ class MolecularData:
|
|
|
511
590
|
self.adjacency_list[id2].remove(id1)
|
|
512
591
|
self.bonds.pop(key_to_remove, None)
|
|
513
592
|
|
|
514
|
-
def to_rdkit_mol(
|
|
593
|
+
def to_rdkit_mol(
|
|
594
|
+
self, use_2d_stereo: bool = True, use_rs_labels: bool = True
|
|
595
|
+
) -> Optional[Chem.Mol]:
|
|
515
596
|
"""
|
|
516
597
|
use_2d_stereo: True estimates E/Z from 2D coordinates (as before). False prioritizes E/Z labels.
|
|
517
598
|
Call with use_2d_stereo=False for 3D conversion.
|
|
599
|
+
use_rs_labels: True re-wedges atoms carrying an R/S label ("rs") so the
|
|
600
|
+
label wins over their drawn wedges; False reads the drawing alone.
|
|
518
601
|
"""
|
|
519
602
|
if not self.atoms:
|
|
520
603
|
return None
|
|
@@ -691,6 +774,17 @@ class MolecularData:
|
|
|
691
774
|
|
|
692
775
|
resolve_wedge_chirality(final_mol)
|
|
693
776
|
|
|
777
|
+
# R/S labels are the user's explicit choice and override the wedges.
|
|
778
|
+
if use_rs_labels:
|
|
779
|
+
apply_rs_labels(
|
|
780
|
+
final_mol,
|
|
781
|
+
{
|
|
782
|
+
atom_id_to_idx_map[atom_id]: data["rs"]
|
|
783
|
+
for atom_id, data in self.atoms.items()
|
|
784
|
+
if data.get("rs") in RS_LABELS and atom_id in atom_id_to_idx_map
|
|
785
|
+
},
|
|
786
|
+
)
|
|
787
|
+
|
|
694
788
|
# The labels are CIP E/Z; the neighbors picked above are not always
|
|
695
789
|
# the CIP-higher ones.
|
|
696
790
|
align_ez_to_cip(final_mol, ez_wanted)
|
|
@@ -14,12 +14,12 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
16
|
from dataclasses import dataclass
|
|
17
|
-
from typing import Dict, FrozenSet, List, Optional, Tuple
|
|
17
|
+
from typing import Any, Dict, FrozenSet, List, Optional, Tuple
|
|
18
18
|
|
|
19
19
|
from rdkit import Chem
|
|
20
20
|
from rdkit.Chem import rdCIPLabeler
|
|
21
21
|
|
|
22
|
-
from .molecular_data import MolecularData
|
|
22
|
+
from .molecular_data import RS_LABELS, MolecularData
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
@dataclass(frozen=True)
|
|
@@ -55,12 +55,20 @@ def _cip_labels_by_atom_id(mol: Chem.Mol) -> Dict[int, str]:
|
|
|
55
55
|
return labels
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
def drawn_chirality(data: MolecularData) -> Dict[int, str]:
|
|
59
|
-
"""CIP labels of the stereocenters the 2D drawing specifies
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
def drawn_chirality(data: MolecularData, use_rs_labels: bool = True) -> Dict[int, str]:
|
|
59
|
+
"""CIP labels of the stereocenters the 2D drawing specifies.
|
|
60
|
+
|
|
61
|
+
These come from the wedges and hashes and, unless *use_rs_labels* is
|
|
62
|
+
False, the atoms' R/S labels, which override the wedges at their center.
|
|
63
|
+
"""
|
|
64
|
+
# Nothing is specified without a wedge, hash or label; skip CIP labelling.
|
|
65
|
+
has_wedge = any(bond.get("stereo") in (1, 2) for bond in data.bonds.values())
|
|
66
|
+
has_label = use_rs_labels and any(
|
|
67
|
+
atom.get("rs") in RS_LABELS for atom in data.atoms.values()
|
|
68
|
+
)
|
|
69
|
+
if not has_wedge and not has_label:
|
|
62
70
|
return {}
|
|
63
|
-
mol = data.to_rdkit_mol(use_2d_stereo=False)
|
|
71
|
+
mol = data.to_rdkit_mol(use_2d_stereo=False, use_rs_labels=use_rs_labels)
|
|
64
72
|
if mol is None:
|
|
65
73
|
return {}
|
|
66
74
|
Chem.AssignChiralTypesFromBondDirs(mol)
|
|
@@ -68,6 +76,125 @@ def drawn_chirality(data: MolecularData) -> Dict[int, str]:
|
|
|
68
76
|
return _cip_labels_by_atom_id(mol)
|
|
69
77
|
|
|
70
78
|
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class WedgeForLabel:
|
|
81
|
+
"""How to draw a bond so that the stereocenter at its narrow end reads as R/S."""
|
|
82
|
+
|
|
83
|
+
key: Tuple[int, int] # (stereocenter, other end): data.bonds key, narrow end first
|
|
84
|
+
stereo: int # 1 = wedge, 2 = hash
|
|
85
|
+
cleared: Tuple[Tuple[int, int], ...] # other wedges at the center to make plain
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def wedge_for_label(
|
|
89
|
+
data: MolecularData,
|
|
90
|
+
bond_key: Tuple[int, int],
|
|
91
|
+
label: str,
|
|
92
|
+
centers: Optional[List[int]] = None,
|
|
93
|
+
) -> Optional[WedgeForLabel]:
|
|
94
|
+
"""Wedge or hash on *bond_key* that makes one of its ends CIP *label*.
|
|
95
|
+
|
|
96
|
+
*centers* is the order in which the bond's ends are tried (default: the
|
|
97
|
+
key order). Each end is tried first with the other wedges kept; if they
|
|
98
|
+
make *label* unreachable, then with this center's other wedges and hashes
|
|
99
|
+
made plain, so the requested label wins. Returns None when neither end
|
|
100
|
+
can be *label* (not a stereocenter). *data* is left unchanged.
|
|
101
|
+
"""
|
|
102
|
+
if bond_key not in data.bonds or label not in RS_LABELS:
|
|
103
|
+
return None
|
|
104
|
+
ends = list(centers) if centers else list(bond_key)
|
|
105
|
+
original = list(data.bonds.items())
|
|
106
|
+
template = dict(data.bonds[bond_key], order=1)
|
|
107
|
+
trial_bonds = {k: dict(v) for k, v in original if k != bond_key}
|
|
108
|
+
try:
|
|
109
|
+
for center in ends:
|
|
110
|
+
if center not in bond_key:
|
|
111
|
+
continue
|
|
112
|
+
other = bond_key[1] if center == bond_key[0] else bond_key[0]
|
|
113
|
+
own = tuple(
|
|
114
|
+
k
|
|
115
|
+
for k, v in trial_bonds.items()
|
|
116
|
+
if k[0] == center and v.get("stereo") in (1, 2)
|
|
117
|
+
)
|
|
118
|
+
for cleared in ((), own) if own else ((),):
|
|
119
|
+
stereo = _stereo_reading_as(
|
|
120
|
+
data, trial_bonds, (center, other), template, cleared, label
|
|
121
|
+
)
|
|
122
|
+
if stereo is not None:
|
|
123
|
+
return WedgeForLabel((center, other), stereo, cleared)
|
|
124
|
+
return None
|
|
125
|
+
except (RuntimeError, ValueError, TypeError) as e:
|
|
126
|
+
logging.warning("R/S wedge search failed: %s", e)
|
|
127
|
+
return None
|
|
128
|
+
finally:
|
|
129
|
+
data.bonds.clear()
|
|
130
|
+
data.bonds.update(original)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _stereo_reading_as(
|
|
134
|
+
data: MolecularData,
|
|
135
|
+
base: Dict[Tuple[int, int], Dict[str, Any]],
|
|
136
|
+
key: Tuple[int, int],
|
|
137
|
+
template: Dict[str, Any],
|
|
138
|
+
cleared: Tuple[Tuple[int, int], ...],
|
|
139
|
+
label: str,
|
|
140
|
+
) -> Optional[int]:
|
|
141
|
+
"""Wedge (1) or hash (2) on *key* that makes its narrow end read *label*.
|
|
142
|
+
|
|
143
|
+
Tried on *data* rebuilt from *base* with the *cleared* bonds made plain;
|
|
144
|
+
the caller restores *data*.
|
|
145
|
+
"""
|
|
146
|
+
for stereo in (1, 2):
|
|
147
|
+
data.bonds.clear()
|
|
148
|
+
for k, v in base.items():
|
|
149
|
+
data.bonds[k] = dict(v, stereo=0) if k in cleared else dict(v)
|
|
150
|
+
data.bonds[key] = dict(template, stereo=stereo)
|
|
151
|
+
if drawn_chirality(data, use_rs_labels=False).get(key[0]) == label:
|
|
152
|
+
return stereo
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def align_wedges_to_label(
|
|
157
|
+
data: MolecularData, center: int, label: str
|
|
158
|
+
) -> Dict[Tuple[int, int], int]:
|
|
159
|
+
"""Stereo changes that make the wedges at *center* draw R/S *label*.
|
|
160
|
+
|
|
161
|
+
Only the wedges and hashes starting at *center* are touched, and only when
|
|
162
|
+
there are some: a center drawn without them keeps its plain bonds (its
|
|
163
|
+
label alone carries the configuration). If the drawing already reads as
|
|
164
|
+
*label*, nothing changes. Otherwise every stereo bond at the center is
|
|
165
|
+
swapped (wedge <-> hash), which mirrors it; if that still does not read
|
|
166
|
+
as *label* (an ambiguous drawing), one of them is redrawn and the rest
|
|
167
|
+
made plain. Returns {data.bonds key: new stereo}; empty when nothing
|
|
168
|
+
needs to change or no drawing gives *label*.
|
|
169
|
+
"""
|
|
170
|
+
own = [
|
|
171
|
+
key
|
|
172
|
+
for key, bond in data.bonds.items()
|
|
173
|
+
if key[0] == center and bond.get("stereo") in (1, 2)
|
|
174
|
+
]
|
|
175
|
+
if not own or label not in RS_LABELS:
|
|
176
|
+
return {}
|
|
177
|
+
if drawn_chirality(data, use_rs_labels=False).get(center) == label:
|
|
178
|
+
return {}
|
|
179
|
+
original = {key: data.bonds[key]["stereo"] for key in own}
|
|
180
|
+
swapped = {key: 3 - stereo for key, stereo in original.items()}
|
|
181
|
+
try:
|
|
182
|
+
for key, stereo in swapped.items():
|
|
183
|
+
data.bonds[key]["stereo"] = stereo
|
|
184
|
+
if drawn_chirality(data, use_rs_labels=False).get(center) == label:
|
|
185
|
+
return swapped
|
|
186
|
+
finally:
|
|
187
|
+
for key, stereo in original.items():
|
|
188
|
+
data.bonds[key]["stereo"] = stereo
|
|
189
|
+
for key in own:
|
|
190
|
+
found = wedge_for_label(data, key, label, centers=[center])
|
|
191
|
+
if found is not None:
|
|
192
|
+
changes = dict.fromkeys(found.cleared, 0)
|
|
193
|
+
changes[key] = found.stereo
|
|
194
|
+
return changes
|
|
195
|
+
return {}
|
|
196
|
+
|
|
197
|
+
|
|
71
198
|
def actual_chirality(mol_3d: Chem.Mol) -> Dict[int, str]:
|
|
72
199
|
"""CIP labels of the stereocenters in a 3D structure, read from coordinates."""
|
|
73
200
|
if mol_3d.GetNumConformers() == 0:
|
|
@@ -33,7 +33,7 @@ from PyQt6.QtCore import QDateTime, Qt
|
|
|
33
33
|
from PyQt6.QtWidgets import QMessageBox
|
|
34
34
|
|
|
35
35
|
from ..utils.constants import VERSION
|
|
36
|
-
from ..core.molecular_data import MolecularData
|
|
36
|
+
from ..core.molecular_data import MolecularData, rs_entry
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
from typing import TYPE_CHECKING
|
|
@@ -90,6 +90,7 @@ class StateManager:
|
|
|
90
90
|
"pos": data["pos"], # Already a tuple (x, y)
|
|
91
91
|
"charge": data.get("charge", 0),
|
|
92
92
|
"radical": data.get("radical", 0),
|
|
93
|
+
**rs_entry(data),
|
|
93
94
|
}
|
|
94
95
|
for atom_id, data in self.data.atoms.items()
|
|
95
96
|
}
|
|
@@ -332,7 +333,7 @@ class StateManager:
|
|
|
332
333
|
|
|
333
334
|
if self.data:
|
|
334
335
|
try:
|
|
335
|
-
mol = self.data.to_rdkit_mol()
|
|
336
|
+
mol = self.data.to_rdkit_mol(use_rs_labels=False)
|
|
336
337
|
if mol:
|
|
337
338
|
mol_with_hs = Chem.AddHs(mol)
|
|
338
339
|
mol_formula = rdMolDescriptors.CalcMolFormula(mol)
|
|
@@ -370,6 +371,7 @@ class StateManager:
|
|
|
370
371
|
"y": pos[1],
|
|
371
372
|
"charge": data.get("charge", 0),
|
|
372
373
|
"radical": data.get("radical", 0),
|
|
374
|
+
**rs_entry(data),
|
|
373
375
|
}
|
|
374
376
|
atoms_2d.append(atom_data)
|
|
375
377
|
|
|
@@ -22,6 +22,7 @@ from PyQt6.QtGui import (
|
|
|
22
22
|
QFontMetricsF,
|
|
23
23
|
QPainter,
|
|
24
24
|
QPainterPath,
|
|
25
|
+
QPainterPathStroker,
|
|
25
26
|
QPen,
|
|
26
27
|
)
|
|
27
28
|
from PyQt6.QtWidgets import (
|
|
@@ -34,6 +35,7 @@ from PyQt6.QtWidgets import (
|
|
|
34
35
|
from ..utils.constants import (
|
|
35
36
|
ATOM_RADIUS,
|
|
36
37
|
CPK_COLORS,
|
|
38
|
+
EZ_LABEL_TEXT_OUTLINE,
|
|
37
39
|
FONT_FAMILY,
|
|
38
40
|
FONT_WEIGHT_BOLD,
|
|
39
41
|
)
|
|
@@ -86,6 +88,8 @@ class AtomItem(QGraphicsItem):
|
|
|
86
88
|
self.radical: int = radical
|
|
87
89
|
self.bonds: List[BondItem] = []
|
|
88
90
|
self.chiral_label: Optional[str] = None
|
|
91
|
+
# User-set R/S label ("R"/"S"); mirrors data.atoms[atom_id]["rs"].
|
|
92
|
+
self.rs_label: Optional[str] = None
|
|
89
93
|
|
|
90
94
|
self.setPos(pos)
|
|
91
95
|
self.implicit_h_count: int = 0
|
|
@@ -250,9 +254,91 @@ class AtomItem(QGraphicsItem):
|
|
|
250
254
|
def boundingRect(self) -> QRectF:
|
|
251
255
|
"""Return the drawn rect plus the hit shape, which Qt requires it to cover."""
|
|
252
256
|
hit_r = self.hit_radius()
|
|
253
|
-
|
|
257
|
+
rect = self.visual_rect().united(
|
|
254
258
|
QRectF(-hit_r, -hit_r, hit_r * 2.0, hit_r * 2.0)
|
|
255
259
|
)
|
|
260
|
+
rs_rect = self._rs_label_rect()
|
|
261
|
+
if rs_rect is not None:
|
|
262
|
+
rect = rect.united(rs_rect.adjusted(-2, -2, 2, 2))
|
|
263
|
+
return rect
|
|
264
|
+
|
|
265
|
+
def _rs_label_font(self) -> QFont:
|
|
266
|
+
"""Italic font of the R/S label, sized from the atom label font."""
|
|
267
|
+
base = self._label_font()
|
|
268
|
+
font = QFont(
|
|
269
|
+
base.family(), max(8, int(base.pointSize() * 0.7)), FONT_WEIGHT_BOLD
|
|
270
|
+
)
|
|
271
|
+
font.setItalic(True)
|
|
272
|
+
return font
|
|
273
|
+
|
|
274
|
+
def _rs_label_rect(self) -> Optional[QRectF]:
|
|
275
|
+
"""Where the "(R)"/"(S)" label goes: beside the atom, away from its bonds."""
|
|
276
|
+
if self.rs_label not in ("R", "S"):
|
|
277
|
+
return None
|
|
278
|
+
rect = QFontMetricsF(self._rs_label_font()).boundingRect(f"({self.rs_label})")
|
|
279
|
+
dx = dy = 0.0
|
|
280
|
+
for bond in self.bonds:
|
|
281
|
+
try:
|
|
282
|
+
partner = bond.atom2 if bond.atom1 is self else bond.atom1
|
|
283
|
+
if partner is None or sip_isdeleted_safe(partner):
|
|
284
|
+
continue
|
|
285
|
+
vec = partner.pos() - self.pos()
|
|
286
|
+
length = (vec.x() ** 2 + vec.y() ** 2) ** 0.5
|
|
287
|
+
if length > 1e-6:
|
|
288
|
+
dx += vec.x() / length
|
|
289
|
+
dy += vec.y() / length
|
|
290
|
+
except (AttributeError, RuntimeError, TypeError):
|
|
291
|
+
continue
|
|
292
|
+
length = (dx**2 + dy**2) ** 0.5
|
|
293
|
+
# Opposite the bonds; upper right when they balance out or there are none.
|
|
294
|
+
ux, uy = (-dx / length, -dy / length) if length > 0.3 else (0.71, -0.71)
|
|
295
|
+
distance = 14.0 + max(rect.width(), rect.height()) / 2.0
|
|
296
|
+
rect.moveCenter(QPointF(ux * distance, uy * distance))
|
|
297
|
+
return rect
|
|
298
|
+
|
|
299
|
+
def _relayout_rs_labels(self) -> None:
|
|
300
|
+
"""Re-place the R/S labels a move of this atom shifts.
|
|
301
|
+
|
|
302
|
+
A label sits opposite its atom's bonds, so moving this atom moves its
|
|
303
|
+
own label and those of its labelled neighbors.
|
|
304
|
+
"""
|
|
305
|
+
if self.rs_label is not None:
|
|
306
|
+
self.prepareGeometryChange()
|
|
307
|
+
for bond in self.bonds:
|
|
308
|
+
partner = bond.atom2 if bond.atom1 is self else bond.atom1
|
|
309
|
+
if partner is None or sip_isdeleted_safe(partner):
|
|
310
|
+
continue
|
|
311
|
+
if getattr(partner, "rs_label", None) is not None:
|
|
312
|
+
partner.prepareGeometryChange()
|
|
313
|
+
|
|
314
|
+
def _paint_rs_label(self, painter: QPainter) -> None:
|
|
315
|
+
"""Draw the user-set R/S label, outlined like the E/Z label."""
|
|
316
|
+
rect = self._rs_label_rect()
|
|
317
|
+
if rect is None:
|
|
318
|
+
return
|
|
319
|
+
painter.save()
|
|
320
|
+
font = self._rs_label_font()
|
|
321
|
+
text = f"({self.rs_label})"
|
|
322
|
+
path = QPainterPath()
|
|
323
|
+
fm = QFontMetricsF(font)
|
|
324
|
+
path.addText(QPointF(rect.left(), rect.top() + fm.ascent()), font, text)
|
|
325
|
+
outline_color = QColor(255, 255, 255)
|
|
326
|
+
scene: Any = self.scene()
|
|
327
|
+
try:
|
|
328
|
+
if scene is not None:
|
|
329
|
+
outline_color = scene.backgroundBrush().color()
|
|
330
|
+
except (AttributeError, RuntimeError, TypeError):
|
|
331
|
+
logging.debug("R/S label outline color fallback", exc_info=True)
|
|
332
|
+
stroker = QPainterPathStroker()
|
|
333
|
+
stroker.setWidth(EZ_LABEL_TEXT_OUTLINE)
|
|
334
|
+
painter.setPen(Qt.PenStyle.NoPen)
|
|
335
|
+
painter.setBrush(outline_color)
|
|
336
|
+
painter.drawPath(stroker.createStroke(path))
|
|
337
|
+
text_color = QColor("gray")
|
|
338
|
+
painter.setBrush(text_color)
|
|
339
|
+
painter.setPen(text_color)
|
|
340
|
+
painter.drawPath(path)
|
|
341
|
+
painter.restore()
|
|
256
342
|
|
|
257
343
|
def get_bg_ellipse_path(self) -> QPainterPath:
|
|
258
344
|
"""Return the elliptical background path used for bond endpoint clipping."""
|
|
@@ -381,6 +467,9 @@ class AtomItem(QGraphicsItem):
|
|
|
381
467
|
QPointF(text_rect.center().x() + 5, radical_pos_y), 3, 3
|
|
382
468
|
)
|
|
383
469
|
|
|
470
|
+
# Drawn even on a skeletal carbon, which has no symbol of its own.
|
|
471
|
+
self._paint_rs_label(painter)
|
|
472
|
+
|
|
384
473
|
# --- Selection highlights etc. ---
|
|
385
474
|
if self.has_problem:
|
|
386
475
|
painter.setBrush(Qt.BrushStyle.NoBrush)
|
|
@@ -405,6 +494,7 @@ class AtomItem(QGraphicsItem):
|
|
|
405
494
|
for bond in self.bonds:
|
|
406
495
|
if bond.scene():
|
|
407
496
|
bond.update_position()
|
|
497
|
+
self._relayout_rs_labels()
|
|
408
498
|
elif change == QGraphicsItem.GraphicsItemChange.ItemSceneHasChanged:
|
|
409
499
|
if self.scene() is not None:
|
|
410
500
|
self.update_style()
|
|
@@ -817,7 +817,7 @@ class ComputeManager:
|
|
|
817
817
|
def update_aromatic_rings(self) -> None:
|
|
818
818
|
"""Update aromatic ring visualization."""
|
|
819
819
|
try:
|
|
820
|
-
mol = self.host.state_manager.data.to_rdkit_mol()
|
|
820
|
+
mol = self.host.state_manager.data.to_rdkit_mol(use_rs_labels=False)
|
|
821
821
|
if not mol:
|
|
822
822
|
return
|
|
823
823
|
Chem.SanitizeMol(mol)
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/edit_actions_logic.py
RENAMED
|
@@ -44,7 +44,7 @@ from rdkit import Chem
|
|
|
44
44
|
from .atom_item import AtomItem
|
|
45
45
|
from .bond_item import BondItem
|
|
46
46
|
from ..utils.constants import CLIPBOARD_MIME_TYPE, UNDO_STACK_MAX_DEPTH
|
|
47
|
-
from ..core.molecular_data import MolecularData
|
|
47
|
+
from ..core.molecular_data import RS_LABELS, MolecularData, rs_entry
|
|
48
48
|
from ..utils.sip_isdeleted_safe import sip_isdeleted_safe
|
|
49
49
|
|
|
50
50
|
|
|
@@ -159,6 +159,7 @@ class EditActionsManager:
|
|
|
159
159
|
v["pos"].y() if hasattr(v["pos"], "y") else v["pos"][1],
|
|
160
160
|
v.get("charge", 0),
|
|
161
161
|
v.get("radical", 0),
|
|
162
|
+
v.get("rs"),
|
|
162
163
|
)
|
|
163
164
|
for k, v in self.host.state_manager.data.atoms.items()
|
|
164
165
|
},
|
|
@@ -196,6 +197,7 @@ class EditActionsManager:
|
|
|
196
197
|
v["pos"].y() if hasattr(v["pos"], "y") else v["pos"][1],
|
|
197
198
|
v.get("charge", 0),
|
|
198
199
|
v.get("radical", 0),
|
|
200
|
+
v.get("rs"),
|
|
199
201
|
)
|
|
200
202
|
for k, v in last_atoms.items()
|
|
201
203
|
},
|
|
@@ -322,6 +324,9 @@ class EditActionsManager:
|
|
|
322
324
|
"rel_pos": atom.pos() - center,
|
|
323
325
|
"charge": atom.charge,
|
|
324
326
|
"radical": atom.radical,
|
|
327
|
+
**rs_entry(
|
|
328
|
+
self.host.state_manager.data.atoms.get(atom.atom_id, {})
|
|
329
|
+
),
|
|
325
330
|
}
|
|
326
331
|
)
|
|
327
332
|
|
|
@@ -350,6 +355,7 @@ class EditActionsManager:
|
|
|
350
355
|
"rel_pos": [a["rel_pos"].x(), a["rel_pos"].y()],
|
|
351
356
|
"charge": a["charge"],
|
|
352
357
|
"radical": a["radical"],
|
|
358
|
+
**rs_entry(a),
|
|
353
359
|
}
|
|
354
360
|
for a in fragment_atoms
|
|
355
361
|
],
|
|
@@ -441,12 +447,15 @@ class EditActionsManager:
|
|
|
441
447
|
raise TypeError(
|
|
442
448
|
"Clipboard atom charge and radical must be integers"
|
|
443
449
|
)
|
|
450
|
+
if atom_data.get("rs") not in (None, *RS_LABELS):
|
|
451
|
+
raise ValueError("Clipboard atom R/S label must be R or S")
|
|
444
452
|
validated_atoms.append(
|
|
445
453
|
{
|
|
446
454
|
"symbol": symbol,
|
|
447
455
|
"rel_pos": (rel_x, rel_y),
|
|
448
456
|
"charge": charge,
|
|
449
457
|
"radical": radical,
|
|
458
|
+
**rs_entry(atom_data),
|
|
450
459
|
}
|
|
451
460
|
)
|
|
452
461
|
|
|
@@ -500,6 +509,10 @@ class EditActionsManager:
|
|
|
500
509
|
radical=atom_data.get("radical", 0),
|
|
501
510
|
)
|
|
502
511
|
new_item = self.host.init_manager.scene.atom_items[new_id]
|
|
512
|
+
if atom_data.get("rs"):
|
|
513
|
+
self.host.init_manager.scene.set_atom_rs_label(
|
|
514
|
+
new_item, atom_data["rs"]
|
|
515
|
+
)
|
|
503
516
|
new_atoms.append(new_item)
|
|
504
517
|
new_item.setSelected(True)
|
|
505
518
|
|
|
@@ -1208,7 +1221,7 @@ class EditActionsManager:
|
|
|
1208
1221
|
|
|
1209
1222
|
mol = None
|
|
1210
1223
|
try:
|
|
1211
|
-
mol = self.host.state_manager.data.to_rdkit_mol()
|
|
1224
|
+
mol = self.host.state_manager.data.to_rdkit_mol(use_rs_labels=False)
|
|
1212
1225
|
except (AttributeError, RuntimeError, ValueError, TypeError) as e:
|
|
1213
1226
|
logging.debug(f"to_rdkit_mol failed during H-update: {e}")
|
|
1214
1227
|
mol = None
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/main_window_init.py
RENAMED
|
@@ -848,6 +848,7 @@ class MainInitManager:
|
|
|
848
848
|
("Triple Bond", "bond_3_0", "3", "triple"),
|
|
849
849
|
("Wedge Bond", "bond_1_1", "W", "wedge"),
|
|
850
850
|
("Dash Bond", "bond_1_2", "D", "dash"),
|
|
851
|
+
("Toggle R/S", "rs_label", "R", "rs_toggle"),
|
|
851
852
|
("Toggle E/Z", "bond_2_5", "E/Z", "ez_toggle"),
|
|
852
853
|
]
|
|
853
854
|
|
|
@@ -1032,17 +1033,28 @@ class MainInitManager:
|
|
|
1032
1033
|
offset = QPointF(v.dx(), v.dy()) * 2.0
|
|
1033
1034
|
painter.drawLine(linez.translated(offset))
|
|
1034
1035
|
painter.drawLine(linez.translated(-offset))
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
)
|
|
1036
|
+
self._draw_icon_caption(painter, size, "Z⇌E")
|
|
1037
|
+
elif bond_type == "rs_toggle":
|
|
1038
|
+
p1z, p2z = QPointF(6, size * 0.75), QPointF(size - 6, size * 0.75)
|
|
1039
|
+
normal = QLineF(p1z, p2z).unitVector().normalVector()
|
|
1040
|
+
offset = QPointF(normal.dx(), normal.dy()) * 3.0
|
|
1041
|
+
painter.drawPolygon(QPolygonF([p1z, p2z + offset, p2z - offset]))
|
|
1042
|
+
self._draw_icon_caption(painter, size, "R⇌S")
|
|
1042
1043
|
|
|
1043
1044
|
painter.end()
|
|
1044
1045
|
return QIcon(pixmap)
|
|
1045
1046
|
|
|
1047
|
+
@staticmethod
|
|
1048
|
+
def _draw_icon_caption(painter: QPainter, size: int, text: str) -> None:
|
|
1049
|
+
"""Bold caption across the top of a toolbar icon (e.g. "Z⇌E")."""
|
|
1050
|
+
font = painter.font()
|
|
1051
|
+
font.setPointSize(10)
|
|
1052
|
+
font.setBold(True)
|
|
1053
|
+
painter.setFont(font)
|
|
1054
|
+
painter.drawText(
|
|
1055
|
+
QRectF(0, 0, size, size * 0.6), Qt.AlignmentFlag.AlignCenter, text
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1046
1058
|
def _create_chain_icon(self, size: int = 32) -> QIcon:
|
|
1047
1059
|
"""Generate the -(-)n- repeat-unit icon for the alkyl chain tool."""
|
|
1048
1060
|
fg = self._get_icon_foreground_color()
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/molecular_scene_handler.py
RENAMED
|
@@ -25,6 +25,8 @@ from .bond_item import BondItem
|
|
|
25
25
|
from ..utils.sip_isdeleted_safe import sip_isdeleted_safe
|
|
26
26
|
|
|
27
27
|
from ..core.mol_geometry import is_problematic_valence
|
|
28
|
+
from ..core.molecular_data import RS_LABELS
|
|
29
|
+
from ..core.stereo_check import align_wedges_to_label, drawn_chirality
|
|
28
30
|
from ..utils.constants import DEFAULT_BOND_LENGTH, SUM_TOLERANCE
|
|
29
31
|
|
|
30
32
|
|
|
@@ -893,6 +895,7 @@ class KeyboardMixin:
|
|
|
893
895
|
# Provided by the composed MoleculeScene at runtime
|
|
894
896
|
_calculate_polygon_from_edge: Any
|
|
895
897
|
add_molecule_fragment: Any
|
|
898
|
+
apply_rs_label: Any
|
|
896
899
|
atom_items: Any
|
|
897
900
|
clear: Any
|
|
898
901
|
create_atom: Any
|
|
@@ -1201,6 +1204,20 @@ class KeyboardMixin:
|
|
|
1201
1204
|
event.accept()
|
|
1202
1205
|
return
|
|
1203
1206
|
|
|
1207
|
+
# --- 0c. R/S label (R cycles the hovered atom: R -> S -> none) ---
|
|
1208
|
+
if (
|
|
1209
|
+
key == Qt.Key.Key_R
|
|
1210
|
+
and modifiers == Qt.KeyboardModifier.NoModifier
|
|
1211
|
+
and isinstance(item_at_cursor, AtomItem)
|
|
1212
|
+
):
|
|
1213
|
+
if self.apply_rs_label(
|
|
1214
|
+
item_at_cursor, next_rs_label(item_at_cursor.rs_label)
|
|
1215
|
+
):
|
|
1216
|
+
self.update_all_items()
|
|
1217
|
+
self.window.edit_actions_manager.push_undo_state()
|
|
1218
|
+
event.accept()
|
|
1219
|
+
return
|
|
1220
|
+
|
|
1204
1221
|
# --- 1. Atom operations (change symbol) ---
|
|
1205
1222
|
if isinstance(item_at_cursor, AtomItem):
|
|
1206
1223
|
new_symbol = None
|
|
@@ -1851,3 +1868,79 @@ class SceneQueryMixin:
|
|
|
1851
1868
|
)
|
|
1852
1869
|
self.window.statusBar().showMessage(f"Error: {e}", 5000)
|
|
1853
1870
|
self.update_all_items()
|
|
1871
|
+
|
|
1872
|
+
def set_atom_rs_label(self, atom_item: Any, label: Optional[str]) -> None:
|
|
1873
|
+
"""Store R/S *label* (None clears it) on the atom's data and item."""
|
|
1874
|
+
record = self.data.atoms.get(atom_item.atom_id)
|
|
1875
|
+
if record is None:
|
|
1876
|
+
return
|
|
1877
|
+
if label in RS_LABELS:
|
|
1878
|
+
record["rs"] = label
|
|
1879
|
+
else:
|
|
1880
|
+
record.pop("rs", None)
|
|
1881
|
+
label = None
|
|
1882
|
+
atom_item.prepareGeometryChange()
|
|
1883
|
+
atom_item.rs_label = label
|
|
1884
|
+
atom_item.update()
|
|
1885
|
+
|
|
1886
|
+
def apply_rs_label(self, atom_item: Any, label: Optional[str]) -> bool:
|
|
1887
|
+
"""Give *atom_item* R/S *label* (None clears it); True if anything changed.
|
|
1888
|
+
|
|
1889
|
+
A label overrides the wedges at its center. Where the center already
|
|
1890
|
+
has wedges or hashes, they are redrawn to agree with the label; a
|
|
1891
|
+
center without them keeps its plain bonds. An atom that cannot be R/S
|
|
1892
|
+
(not a stereocenter) is left alone with a status-bar note.
|
|
1893
|
+
"""
|
|
1894
|
+
if atom_item is None or atom_item.atom_id not in self.data.atoms:
|
|
1895
|
+
return False
|
|
1896
|
+
if getattr(atom_item, "rs_label", None) == label:
|
|
1897
|
+
return False
|
|
1898
|
+
atom_id = atom_item.atom_id
|
|
1899
|
+
if label is None:
|
|
1900
|
+
self.set_atom_rs_label(atom_item, None)
|
|
1901
|
+
return True
|
|
1902
|
+
record = self.data.atoms[atom_id]
|
|
1903
|
+
previous = record.get("rs")
|
|
1904
|
+
record["rs"] = label
|
|
1905
|
+
try:
|
|
1906
|
+
reachable = drawn_chirality(self.data).get(atom_id) == label
|
|
1907
|
+
finally:
|
|
1908
|
+
if previous is None:
|
|
1909
|
+
record.pop("rs", None)
|
|
1910
|
+
else:
|
|
1911
|
+
record["rs"] = previous
|
|
1912
|
+
if not reachable:
|
|
1913
|
+
self.window.statusBar().showMessage(
|
|
1914
|
+
f"{atom_item.symbol}{atom_id} is not a stereocenter; no R/S label set.",
|
|
1915
|
+
4000,
|
|
1916
|
+
)
|
|
1917
|
+
return False
|
|
1918
|
+
bond_changes = align_wedges_to_label(self.data, atom_id, label)
|
|
1919
|
+
self.set_atom_rs_label(atom_item, label)
|
|
1920
|
+
for key, stereo in bond_changes.items():
|
|
1921
|
+
self._set_bond_stereo_by_key(key, stereo)
|
|
1922
|
+
return True
|
|
1923
|
+
|
|
1924
|
+
def _set_bond_stereo_by_key(self, key: Tuple[int, int], stereo: int) -> None:
|
|
1925
|
+
"""Set a bond's stereo in data and scene, keeping plain-bond keys sorted."""
|
|
1926
|
+
bond_data = self.data.bonds.get(key)
|
|
1927
|
+
bond_item = self.bond_items.get(key)
|
|
1928
|
+
if bond_data is None:
|
|
1929
|
+
return
|
|
1930
|
+
bond_data["stereo"] = stereo
|
|
1931
|
+
new_key = key
|
|
1932
|
+
if stereo == 0 and key[0] > key[1]:
|
|
1933
|
+
new_key = (key[1], key[0])
|
|
1934
|
+
self.data.bonds[new_key] = self.data.bonds.pop(key)
|
|
1935
|
+
if bond_item is None:
|
|
1936
|
+
return
|
|
1937
|
+
if new_key != key:
|
|
1938
|
+
self.rekey_bond_item(bond_item, new_key)
|
|
1939
|
+
bond_item.atom1, bond_item.atom2 = bond_item.atom2, bond_item.atom1
|
|
1940
|
+
bond_item.update_position()
|
|
1941
|
+
bond_item.set_stereo(stereo)
|
|
1942
|
+
|
|
1943
|
+
|
|
1944
|
+
def next_rs_label(current: Optional[str]) -> Optional[str]:
|
|
1945
|
+
"""The R/S tool's click cycle: none -> R -> S -> none."""
|
|
1946
|
+
return {None: "R", "R": "S"}.get(current)
|
|
@@ -25,6 +25,8 @@ from PyQt6.QtWidgets import (
|
|
|
25
25
|
QGraphicsSceneMouseEvent,
|
|
26
26
|
)
|
|
27
27
|
|
|
28
|
+
from ..core.molecular_data import rs_entry
|
|
29
|
+
|
|
28
30
|
if TYPE_CHECKING:
|
|
29
31
|
from ..core.molecular_data import MolecularData
|
|
30
32
|
from .main_window import MainWindow
|
|
@@ -36,7 +38,12 @@ from .template_preview_item import TemplatePreviewItem
|
|
|
36
38
|
|
|
37
39
|
from ..utils.sip_isdeleted_safe import sip_isdeleted_safe
|
|
38
40
|
|
|
39
|
-
from .molecular_scene_handler import
|
|
41
|
+
from .molecular_scene_handler import (
|
|
42
|
+
KeyboardMixin,
|
|
43
|
+
SceneQueryMixin,
|
|
44
|
+
TemplateMixin,
|
|
45
|
+
next_rs_label,
|
|
46
|
+
)
|
|
40
47
|
from .chain_mixin import ChainMixin
|
|
41
48
|
|
|
42
49
|
|
|
@@ -126,7 +133,7 @@ class MoleculeScene(
|
|
|
126
133
|
return
|
|
127
134
|
|
|
128
135
|
# 1. Generate RDKit molecule for topology analysis
|
|
129
|
-
mol = self.data.to_rdkit_mol(use_2d_stereo=False)
|
|
136
|
+
mol = self.data.to_rdkit_mol(use_2d_stereo=False, use_rs_labels=False)
|
|
130
137
|
if not mol:
|
|
131
138
|
# Fallback: reset all ring info if molecule generation fails
|
|
132
139
|
for bond_item in self.bond_items.values():
|
|
@@ -231,7 +238,9 @@ class MoleculeScene(
|
|
|
231
238
|
"pos": raw_pos,
|
|
232
239
|
"charge": charge,
|
|
233
240
|
"radical": radical,
|
|
241
|
+
**rs_entry(data),
|
|
234
242
|
}
|
|
243
|
+
atom_item.rs_label = self.data.atoms[atom_id].get("rs")
|
|
235
244
|
self.atom_items[atom_id] = atom_item
|
|
236
245
|
self.addItem(atom_item)
|
|
237
246
|
|
|
@@ -276,7 +285,9 @@ class MoleculeScene(
|
|
|
276
285
|
"pos": raw_pos,
|
|
277
286
|
"charge": charge,
|
|
278
287
|
"radical": radical,
|
|
288
|
+
**rs_entry(atom_data),
|
|
279
289
|
}
|
|
290
|
+
atom_item.rs_label = self.data.atoms[atom_id].get("rs")
|
|
280
291
|
self.atom_items[atom_id] = atom_item
|
|
281
292
|
self.addItem(atom_item)
|
|
282
293
|
|
|
@@ -439,7 +450,9 @@ class MoleculeScene(
|
|
|
439
450
|
if (
|
|
440
451
|
len(selected_items) > 1
|
|
441
452
|
and item in selected_items
|
|
442
|
-
and not self.mode.startswith(
|
|
453
|
+
and not self.mode.startswith(
|
|
454
|
+
("template", "charge", "radical", "rs_label")
|
|
455
|
+
)
|
|
443
456
|
):
|
|
444
457
|
# Delete the entire rectangular selection
|
|
445
458
|
data_changed = self.delete_items(set(selected_items))
|
|
@@ -484,6 +497,9 @@ class MoleculeScene(
|
|
|
484
497
|
self.data.atoms[item.atom_id]["radical"] = 0
|
|
485
498
|
item.update_style()
|
|
486
499
|
data_changed = True
|
|
500
|
+
# Clear the R/S label if in R/S mode
|
|
501
|
+
elif self.mode == "rs_label" and item.rs_label is not None:
|
|
502
|
+
data_changed = self.apply_rs_label(item, None)
|
|
487
503
|
# Set charge to 0 if in charge mode
|
|
488
504
|
elif self.mode in ["charge_plus", "charge_minus"] and item.charge != 0:
|
|
489
505
|
item.prepareGeometryChange()
|
|
@@ -492,11 +508,15 @@ class MoleculeScene(
|
|
|
492
508
|
item.update_style()
|
|
493
509
|
data_changed = True
|
|
494
510
|
# Delete atom in modes other than those above (excluding template, charge, radical)
|
|
495
|
-
elif not self.mode.startswith(
|
|
511
|
+
elif not self.mode.startswith(
|
|
512
|
+
("template", "charge", "radical", "rs_label")
|
|
513
|
+
):
|
|
496
514
|
data_changed = self.delete_items({item})
|
|
497
515
|
elif isinstance(item, BondItem):
|
|
498
516
|
# Delete bond in modes other than template, charge, radical
|
|
499
|
-
if not self.mode.startswith(
|
|
517
|
+
if not self.mode.startswith(
|
|
518
|
+
("template", "charge", "radical", "rs_label")
|
|
519
|
+
):
|
|
500
520
|
data_changed = self.delete_items({item})
|
|
501
521
|
|
|
502
522
|
if data_changed:
|
|
@@ -692,6 +712,19 @@ class MoleculeScene(
|
|
|
692
712
|
self.press_pos = None
|
|
693
713
|
self.window.edit_actions_manager.push_undo_state()
|
|
694
714
|
return
|
|
715
|
+
elif (
|
|
716
|
+
self.mode == "rs_label" and is_click and isinstance(released_item, AtomItem)
|
|
717
|
+
):
|
|
718
|
+
changed = self.apply_rs_label(
|
|
719
|
+
released_item, next_rs_label(released_item.rs_label)
|
|
720
|
+
)
|
|
721
|
+
self.start_atom = None
|
|
722
|
+
self.start_pos = None
|
|
723
|
+
self.press_pos = None
|
|
724
|
+
if changed:
|
|
725
|
+
self.update_all_items()
|
|
726
|
+
self.window.edit_actions_manager.push_undo_state()
|
|
727
|
+
return
|
|
695
728
|
elif (
|
|
696
729
|
(self.mode == "charge_plus" or self.mode == "charge_minus")
|
|
697
730
|
and is_click
|
|
@@ -922,6 +955,13 @@ class MoleculeScene(
|
|
|
922
955
|
"""Handle double click events."""
|
|
923
956
|
item = self.itemAt(event.scenePos(), self.views()[0].transform())
|
|
924
957
|
|
|
958
|
+
if self.mode == "rs_label" and isinstance(item, AtomItem):
|
|
959
|
+
# The second click of a double-click advances the cycle once more.
|
|
960
|
+
if self.apply_rs_label(item, next_rs_label(item.rs_label)):
|
|
961
|
+
self.update_all_items()
|
|
962
|
+
self.window.edit_actions_manager.push_undo_state()
|
|
963
|
+
event.accept()
|
|
964
|
+
return
|
|
925
965
|
if self.mode in ["charge_plus", "charge_minus", "radical"] and isinstance(
|
|
926
966
|
item, AtomItem
|
|
927
967
|
):
|
|
@@ -85,7 +85,7 @@ class UIManager(QObject):
|
|
|
85
85
|
self.host.init_manager.view_2d.setCursor(Qt.CursorShape.ArrowCursor)
|
|
86
86
|
elif mode_str.startswith(("atom", "bond", "template", "chain")):
|
|
87
87
|
self.host.init_manager.view_2d.setCursor(Qt.CursorShape.CrossCursor)
|
|
88
|
-
elif mode_str.startswith(("charge", "radical")):
|
|
88
|
+
elif mode_str.startswith(("charge", "radical", "rs_label")):
|
|
89
89
|
self.host.init_manager.view_2d.setCursor(Qt.CursorShape.CrossCursor)
|
|
90
90
|
else:
|
|
91
91
|
self.host.init_manager.view_2d.setCursor(Qt.CursorShape.ArrowCursor)
|
|
@@ -139,6 +139,11 @@ class UIManager(QObject):
|
|
|
139
139
|
elif mode_str == "radical":
|
|
140
140
|
self.host.update_status_message("Mode: Toggle Radical (Click on Atom)")
|
|
141
141
|
self.host.init_manager.view_2d.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
142
|
+
elif mode_str == "rs_label":
|
|
143
|
+
self.host.update_status_message(
|
|
144
|
+
"Mode: Toggle R/S (Click on a stereocenter: R -> S -> none)"
|
|
145
|
+
)
|
|
146
|
+
self.host.init_manager.view_2d.setDragMode(QGraphicsView.DragMode.NoDrag)
|
|
142
147
|
|
|
143
148
|
else: # Select mode
|
|
144
149
|
self.host.update_status_message("Mode: Select")
|
|
@@ -127,7 +127,7 @@ class ZoomableView(QGraphicsView):
|
|
|
127
127
|
self.setCursor(Qt.CursorShape.ArrowCursor)
|
|
128
128
|
elif current_mode.startswith(("atom", "bond", "template", "chain")):
|
|
129
129
|
self.setCursor(Qt.CursorShape.CrossCursor)
|
|
130
|
-
elif current_mode.startswith(("charge", "radical")):
|
|
130
|
+
elif current_mode.startswith(("charge", "radical", "rs_label")):
|
|
131
131
|
self.setCursor(Qt.CursorShape.CrossCursor)
|
|
132
132
|
else:
|
|
133
133
|
self.setCursor(Qt.CursorShape.ArrowCursor)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/MoleditPy_linux.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/MoleditPy_linux.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/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
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/plugins/plugin_interface.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/plugins/plugin_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/align_plane_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/alignment_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/base_picking_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/bond_length_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/calculation_worker.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/chirality_warning_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/color_settings_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/custom_interactor_style.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/custom_qt_interactor.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/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.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/geometry_base_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/move_dialog_mixin.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/move_group_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/periodic_table_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/planarize_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/plugin_menu_manager.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/preview_molecule.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/sdf_record_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/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.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/string_importers.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/template_preview_item.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/template_preview_view.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/translation_dialog.py
RENAMED
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/ui/user_template_dialog.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{moleditpy_linux-4.12.1 → moleditpy_linux-4.13.0}/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.12.1 → moleditpy_linux-4.13.0}/src/moleditpy_linux/utils/sip_isdeleted_safe.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|