Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/source/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ organisation on `GitHub <https://github.com/openbiosim/sire>`__.

* Please add an item to this CHANGELOG for any new features or bug fixes when creating a PR.

* Fixed ``sire.restraints.boresch()`` setting a dynamic ``_use_pbc`` Python attribute on
the returned ``BoreschRestraints`` instead of calling ``set_uses_pbc()``, which broke
pickling (e.g. for ``multiprocessing``/``ProcessPoolExecutor``) and meant the flag did
not survive a ``sire.stream.save``/``load`` round-trip.

* Fixed a module load-order bug where ``sire.mm`` failed to import (``cannot import
name '_fix_siremm' from 'sire.mm'``) if it was the first ``sire`` submodule touched in
a process, due to ``use_new_api()`` being called before ``_fix_siremm`` was defined.

`2026.1.0 <https://github.com/openbiosim/sire/compare/2025.4.0...2026.1.0>`__ - June 2026
-----------------------------------------------------------------------------------------

Expand Down
19 changes: 15 additions & 4 deletions src/sire/mm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@

from ..legacy import MM as _MM

from .. import use_new_api as _use_new_api

_use_new_api()

AngleRestraint = _MM.AngleRestraint
AngleRestraints = _MM.AngleRestraints

Expand Down Expand Up @@ -217,3 +213,18 @@ def _fix_siremm():
_fix_siremm()
except ImportError:
pass

# This must be called after _fix_siremm() above, not at the top of this
# module (as in most other new-API submodules). use_new_api() eagerly loads
# every other lazily-loaded new-API submodule, including sire.mol - and
# sire.mol's own module-level code does 'from ..mm import _fix_siremm'. If
# sire.mm is the first Sire submodule touched in a process, calling
# use_new_api() before _fix_siremm is defined means that reentrant load of
# sire.mol fails with 'cannot import name _fix_siremm from sire.mm', which
# then cascades into 'sire.mm could not be loaded' via the lazy_import
# wrapper. Nothing above this point depends on use_new_api() having run
# (it only pythonizes names already pulled directly from the raw legacy
# _MM module), so it is safe to defer to here.
from .. import use_new_api as _use_new_api

_use_new_api()
3 changes: 1 addition & 2 deletions src/sire/restraints/_restraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ def boresch(
b = BoreschRestraints(name, b)

# Set the use_pbc flag.
b._use_pbc = use_pbc
b.set_uses_pbc(use_pbc)

return b

Expand Down Expand Up @@ -876,7 +876,6 @@ def morse_potential(
for bond in changed_bonds:
bond_name, length0, length1, k0, k1 = bond
if k1 == 0 or k0 == 0:

# If the bond is being created (k0 == 0), then we should
# use the parameters from the final state (length1, k1).
# If the bond is being annihilated (k1 == 0), then we don't
Expand Down
32 changes: 31 additions & 1 deletion tests/restraints/test_boresch.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pickle

import pytest

import sire as sr
from sire.restraints import boresch

# Valid Boresch restraint parameters.
Expand Down Expand Up @@ -203,6 +204,35 @@ def test_boresch_restraint_params(thrombin_complex):
assert boresch_restraint.phi0()[2].value() == 1.4147


def test_boresch_restraints_pickles_ok(thrombin_complex):
"""
Regression test: sire.restraints.boresch() used to set a dynamic
'_use_pbc' Python attribute on the returned BoreschRestraints instead of
calling set_uses_pbc(), which broke pickling (fixed).
"""
boresch_restraints = boresch(
thrombin_complex,
receptor=thrombin_complex["protein"][
BORESCH_PARAMS_DEFAULT["receptor_selection"]
],
ligand=thrombin_complex["resname LIG"][
BORESCH_PARAMS_DEFAULT["ligand_selection"]
],
kr=BORESCH_PARAMS_DEFAULT["kr"],
ktheta=BORESCH_PARAMS_DEFAULT["ktheta"],
kphi=BORESCH_PARAMS_DEFAULT["kphi"],
r0=BORESCH_PARAMS_DEFAULT["r0"],
theta0=BORESCH_PARAMS_DEFAULT["theta0"],
phi0=BORESCH_PARAMS_DEFAULT["phi0"],
name=BORESCH_PARAMS_DEFAULT["name"],
)

data = pickle.dumps(boresch_restraints)
reloaded = pickle.loads(data)

assert reloaded[0].kr().value() == boresch_restraints[0].kr().value()


@pytest.mark.parametrize(
(
"receptor_selection",
Expand Down
41 changes: 41 additions & 0 deletions tests/stream/test_pickle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import pickle

import pytest

import sire as sr


def test_dynamic_attribute_breaks_pickling():
"""
General, still-open Sire issue: sire_pickle_suite<T> (wrapper/Qt/
qdatastream.hpp) never overrides pickle_suite::getstate_manages_dict(),
so any object using it that also has a dynamic Python attribute set on
it (populating __dict__) fails to pickle with 'Incomplete pickle support
(__getstate_manages_dict__ not set)', even though the object's own
QDataStream-based serialisation (sire.stream.save/load) is unaffected.
"""
from sire.mm import BoreschRestraint, BoreschRestraints

b = BoreschRestraint(
receptor=[1574, 1554, 1576],
ligand=[4, 3, 5],
r0=sr.u("7.687 A"),
theta0=[sr.u("1.3031 rad"), sr.u("1.4777 rad")],
phi0=[sr.u("2.5569 rad"), sr.u("2.9359 rad"), sr.u("1.4147 rad")],
kr=sr.u("6.2012 kcal mol-1 A-2"),
ktheta=[sr.u("28.7685 kcal mol-1 rad-2"), sr.u("24.8204 kcal mol-1 rad-2")],
kphi=[
sr.u("59.8626 kcal mol-1 rad-2"),
sr.u("0.7923 kcal mol-1 rad-2"),
sr.u("55.1775 kcal mol-1 rad-2"),
],
)
restraints = BoreschRestraints(b)

# Pickles fine before any dynamic attribute is set.
pickle.dumps(restraints)

restraints._some_dynamic_attribute = True

with pytest.raises(RuntimeError, match="Incomplete pickle support"):
pickle.dumps(restraints)
Loading