From f7af67935657ab2633a54bd574d908f354f1bb12 Mon Sep 17 00:00:00 2001 From: Ben Webb Date: Thu, 5 Sep 2024 16:57:26 -0700 Subject: [PATCH] Squashed 'modules/core/dependency/python-ihm/' changes from 56116f58b1..db438ff7e7 db438ff7e7 Fix test of dump-time check 385893312b Note that reference sequences may be numbered differently f667231ccb Check residue ranges at creation time d84a0c1885 Explicitly check residue ranges at dump time git-subtree-dir: modules/core/dependency/python-ihm git-subtree-split: db438ff7e7bf185cb011c83dc85262f8b77f4235 --- .../core/dependency/python-ihm/docs/usage.rst | 6 ++++++ .../dependency/python-ihm/ihm/__init__.py | 18 +++++++++-------- .../core/dependency/python-ihm/ihm/dumper.py | 6 +++++- .../core/dependency/python-ihm/ihm/reader.py | 6 ++++-- .../python-ihm/ihm/util/__init__.py | 12 +++++++++++ .../dependency/python-ihm/test/test_dumper.py | 20 +++++++++++++++++++ .../dependency/python-ihm/test/test_main.py | 10 ++++++++++ 7 files changed, 67 insertions(+), 11 deletions(-) diff --git a/modules/core/dependency/python-ihm/docs/usage.rst b/modules/core/dependency/python-ihm/docs/usage.rst index 1778891d27..0e8ea6c760 100644 --- a/modules/core/dependency/python-ihm/docs/usage.rst +++ b/modules/core/dependency/python-ihm/docs/usage.rst @@ -140,6 +140,12 @@ of the data used in modeling: line up with the internal numbering. In this case an offset from starting model numbering to internal numbering can be provided - see the ``offset`` parameter to :class:`~ihm.startmodel.StartingModel`. + - *Reference sequence numbering*. The modeled sequence may differ from that + in a database such as UniProt, which is itself numbered sequentially from 1 + (for example, the modeled sequence may be a subset of the UniProt sequence, + such that the first modeled residue is not the first residue in UniProt). + The correspondence between the internal and reference sequences is given + with :class:`ihm.reference.Alignment` objects. Output ====== diff --git a/modules/core/dependency/python-ihm/ihm/__init__.py b/modules/core/dependency/python-ihm/ihm/__init__.py index 98b3c566c6..4129838cf0 100644 --- a/modules/core/dependency/python-ihm/ihm/__init__.py +++ b/modules/core/dependency/python-ihm/ihm/__init__.py @@ -1235,12 +1235,13 @@ class EntityRange(object): entity = ihm.Entity(sequence=...) rng = entity(4,7) """ - def __init__(self, entity, seq_id_begin, seq_id_end): + def __init__(self, entity, seq_id_begin, seq_id_end, _check=True): if not entity.is_polymeric(): raise TypeError("Can only create ranges for polymeric entities") self.entity = entity - # todo: check range for validity (at property read time) self.seq_id_range = (seq_id_begin, seq_id_end) + if _check: + util._check_residue_range(self) def __eq__(self, other): try: @@ -1467,8 +1468,8 @@ def __hash__(self): else: return hash(self.sequence) - def __call__(self, seq_id_begin, seq_id_end): - return EntityRange(self, seq_id_begin, seq_id_end) + def __call__(self, seq_id_begin, seq_id_end, _check=True): + return EntityRange(self, seq_id_begin, seq_id_end, _check=_check) def __get_seq_id_range(self): if self.is_polymeric() or self.is_branched(): @@ -1487,12 +1488,13 @@ class AsymUnitRange(object): asym = ihm.AsymUnit(entity) rng = asym(4,7) """ - def __init__(self, asym, seq_id_begin, seq_id_end): + def __init__(self, asym, seq_id_begin, seq_id_end, _check=True): if asym.entity is not None and not asym.entity.is_polymeric(): raise TypeError("Can only create ranges for polymeric entities") self.asym = asym - # todo: check range for validity (at property read time) self.seq_id_range = (seq_id_begin, seq_id_end) + if _check: + util._check_residue_range(self) def __eq__(self, other): try: @@ -1613,8 +1615,8 @@ def _get_pdb_auth_seq_id_ins_code(self, seq_id): auth_seq_num = self.orig_auth_seq_id_map.get(seq_id, pdb_seq_num) return pdb_seq_num, auth_seq_num, ins_code - def __call__(self, seq_id_begin, seq_id_end): - return AsymUnitRange(self, seq_id_begin, seq_id_end) + def __call__(self, seq_id_begin, seq_id_end, _check=True): + return AsymUnitRange(self, seq_id_begin, seq_id_end, _check=_check) def residue(self, seq_id): """Get a :class:`Residue` at the given sequence position""" diff --git a/modules/core/dependency/python-ihm/ihm/dumper.py b/modules/core/dependency/python-ihm/ihm/dumper.py index 3782325d29..f582b4232a 100644 --- a/modules/core/dependency/python-ihm/ihm/dumper.py +++ b/modules/core/dependency/python-ihm/ihm/dumper.py @@ -683,7 +683,11 @@ def dump(self, system, writer): ["id", "entity_id", "seq_id_begin", "seq_id_end", "comp_id_begin", "comp_id_end"]) as lp: for rng in self._ranges_by_id: - entity = rng.entity if hasattr(rng, 'entity') else rng + if hasattr(rng, 'entity'): + entity = rng.entity + util._check_residue_range(rng) + else: + entity = rng lp.write( id=rng._range_id, entity_id=entity._id, seq_id_begin=rng.seq_id_range[0], diff --git a/modules/core/dependency/python-ihm/ihm/reader.py b/modules/core/dependency/python-ihm/ihm/reader.py index 2ce8b9e543..024c079fa9 100644 --- a/modules/core/dependency/python-ihm/ihm/reader.py +++ b/modules/core/dependency/python-ihm/ihm/reader.py @@ -197,7 +197,8 @@ def get(self, asym_or_entity, range_id): if range_id is None: return asym_or_entity else: - return asym_or_entity(*self._id_map[range_id]) + # Allow reading out-of-range ranges + return asym_or_entity(*self._id_map[range_id], _check=False) class _AnalysisIDMapper(IDMapper): @@ -2239,7 +2240,8 @@ def __call__(self, feature_id, entity_id, asym_id, seq_id_begin, asym_or_entity = self._get_asym_or_entity(asym_id, entity_id) r1 = int(seq_id_begin) r2 = int(seq_id_end) - f.ranges.append(asym_or_entity(r1, r2)) + # allow out-of-range ranges + f.ranges.append(asym_or_entity(r1, r2, _check=False)) class _FeatureListHandler(Handler): diff --git a/modules/core/dependency/python-ihm/ihm/util/__init__.py b/modules/core/dependency/python-ihm/ihm/util/__init__.py index 98516d247d..7b5533ed5f 100644 --- a/modules/core/dependency/python-ihm/ihm/util/__init__.py +++ b/modules/core/dependency/python-ihm/ihm/util/__init__.py @@ -64,3 +64,15 @@ def setfunc(obj, val): setattr(obj, "_" + attr, val) return property(getfunc, setfunc, doc=doc) + + +def _check_residue_range(rng): + """Make sure that a residue range is not out of range of its Entity""" + if rng.seq_id_range[1] < rng.seq_id_range[0]: + raise ValueError("Range %d-%d is invalid; end is before start" + % rng.seq_id_range) + if (rng.seq_id_range[1] > len(rng.entity.sequence) + or rng.seq_id_range[0] < 1): + raise IndexError("Range %d-%d out of range for %s (1-%d)" + % (rng.seq_id_range[0], rng.seq_id_range[1], + rng.entity, len(rng.entity.sequence))) diff --git a/modules/core/dependency/python-ihm/test/test_dumper.py b/modules/core/dependency/python-ihm/test/test_dumper.py index b831adc831..4051a7310d 100644 --- a/modules/core/dependency/python-ihm/test/test_dumper.py +++ b/modules/core/dependency/python-ihm/test/test_dumper.py @@ -2663,6 +2663,26 @@ def test_entity_poly_segment_dumper(self): # """) + def test_entity_poly_segment_dumper_bad_range(self): + """Test EntityPolySegmentDumper with bad residue ranges""" + for badrng, exc in [((10, 14), IndexError), + ((-4, 1), IndexError), + ((3, 1), ValueError)]: + system = ihm.System() + e1 = ihm.Entity('AHCD') + system.entities.append(e1) + # Disable construction-time check so that we + # can see dump time check + system.orphan_features.append( + ihm.restraint.ResidueFeature([e1(*badrng, _check=False)])) + + dumper = ihm.dumper._EntityDumper() + dumper.finalize(system) # assign IDs + dumper = ihm.dumper._EntityPolySegmentDumper() + dumper.finalize(system) # assign IDs + + self.assertRaises(exc, _get_dumper_output, dumper, system) + def test_single_state(self): """Test MultiStateDumper with a single state""" system = ihm.System() diff --git a/modules/core/dependency/python-ihm/test/test_main.py b/modules/core/dependency/python-ihm/test/test_main.py index 6b549e1c72..c8de8a897a 100644 --- a/modules/core/dependency/python-ihm/test/test_main.py +++ b/modules/core/dependency/python-ihm/test/test_main.py @@ -486,6 +486,11 @@ def test_entity_range(self): self.assertEqual(hash(r), hash(samer)) self.assertNotEqual(r, otherr) self.assertNotEqual(r, e) # entity_range != entity + # Cannot create reversed range + self.assertRaises(ValueError, e.__call__, 3, 1) + # Cannot create out-of-range range + self.assertRaises(IndexError, e.__call__, -3, 1) + self.assertRaises(IndexError, e.__call__, 1, 10) def test_asym_range(self): """Test AsymUnitRange class""" @@ -518,6 +523,11 @@ def test_asym_range(self): self.assertNotEqual(r, a) # asym_range != asym self.assertNotEqual(r, e(3, 4)) # asym_range != entity_range self.assertNotEqual(r, e) # asym_range != entity + # Cannot create reversed range + self.assertRaises(ValueError, a.__call__, 3, 1) + # Cannot create out-of-range range + self.assertRaises(IndexError, a.__call__, -3, 1) + self.assertRaises(IndexError, a.__call__, 1, 10) def test_asym_segment(self): """Test AsymUnitSegment class"""