diff --git a/modules/core/dependency/python-ihm/.github/workflows/testpy.yml b/modules/core/dependency/python-ihm/.github/workflows/testpy.yml index 7ffd447d59..7dbee7fb4a 100644 --- a/modules/core/dependency/python-ihm/.github/workflows/testpy.yml +++ b/modules/core/dependency/python-ihm/.github/workflows/testpy.yml @@ -11,8 +11,6 @@ jobs: os: [ubuntu-22.04] python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] include: - - os: ubuntu-20.04 - python-version: '2.7' - os: ubuntu-20.04 python-version: '3.6' - os: macos-latest diff --git a/modules/core/dependency/python-ihm/ChangeLog.rst b/modules/core/dependency/python-ihm/ChangeLog.rst index 12e8990877..026169454e 100644 --- a/modules/core/dependency/python-ihm/ChangeLog.rst +++ b/modules/core/dependency/python-ihm/ChangeLog.rst @@ -1,3 +1,10 @@ +HEAD +==== + - :class:`ihm.location.DatabaseLocation` no longer accepts a ``db_name`` + parameter. Derived classes (such as :class:`ihm.location.PDBLocation`) + should be used instead; the base class should only be used for "other" + databases that are not described in the IHM dictionary (#116). + 0.38 - 2023-05-26 ================= - Convenience classes are added to describe datasets stored in diff --git a/modules/core/dependency/python-ihm/docs/cross_linkers.rst b/modules/core/dependency/python-ihm/docs/cross_linkers.rst index 2804bc6569..ece8650f30 100644 --- a/modules/core/dependency/python-ihm/docs/cross_linkers.rst +++ b/modules/core/dependency/python-ihm/docs/cross_linkers.rst @@ -49,3 +49,30 @@ The :mod:`ihm.cross_linkers` Python module SDA (NHS-Diazirine) (succinimidyl 4,4′-azipentanoate) cross-linker that links primary amines with nearly any other functional group via long-wave UV-light activation. + +.. data:: photo_leucine + + L-photo-leucine. Non-canonical amino acid incorporated at leucine + positions that links leucine to any other functional group via long-wave + UV-light activation. + See `Suchanek et al, 2005 `_. + +.. data:: dsbu + + dsbu (disuccinimidyl dibutyric urea) cross-linker that links a primary + amine with another primary amine (non-water-soluble). + Cleavable in the gas phase using collision-induced dissociation. + See `Müller et al, 2011 `_. + +.. data:: phoX + + PhoX cross-linker that links a primary amine with another primary amine. + The spacer group contains a phosphonate group, making the cross-linker + IMAC-enrichable. Also known by the name DSPP. See + `Steigenberger et al, 2019 `_. + +.. data:: tbuphoX + + Tert-butyl PhoX cross-linker. Similar to PhoX, but containing a tert-butyl + group that renders the cross-linker cell permeable. + See `Jiang et al, 2021 `_. diff --git a/modules/core/dependency/python-ihm/ihm/__init__.py b/modules/core/dependency/python-ihm/ihm/__init__.py index cf3deb6dfc..e32b8125ed 100644 --- a/modules/core/dependency/python-ihm/ihm/__init__.py +++ b/modules/core/dependency/python-ihm/ihm/__init__.py @@ -575,7 +575,7 @@ class Software(object): """Software used as part of the modeling protocol. :param str name: The name of the software. - :param str classification: The major function of the sofware, for + :param str classification: The major function of the software, for example 'model building', 'sample preparation', 'data collection'. :param str description: A longer text description of the software. @@ -1086,6 +1086,8 @@ class Residue(object): def __init__(self, seq_id, entity=None, asym=None): self.entity = entity self.asym = asym + if entity is None and asym: + self.entity = asym.entity # todo: check id for validity (at property read time) self.seq_id = seq_id @@ -1106,8 +1108,7 @@ def _get_ins_code(self): "for asymmetric units") def _get_comp(self): - entity = self.entity or self.asym.entity - return entity.sequence[self.seq_id - 1] + return self.entity.sequence[self.seq_id - 1] comp = property(_get_comp, doc="Chemical component (residue type)") diff --git a/modules/core/dependency/python-ihm/ihm/cross_linkers.py b/modules/core/dependency/python-ihm/ihm/cross_linkers.py index ff1440c137..7246155968 100644 --- a/modules/core/dependency/python-ihm/ihm/cross_linkers.py +++ b/modules/core/dependency/python-ihm/ihm/cross_linkers.py @@ -64,3 +64,26 @@ inchi='1S/C9H11N3O4/c1-9(10-11-9)5-4-8(15)16-12-6(13)2-3-' '7(12)14/h2-5H2,1H3', inchi_key=' SYYLQNPWAPHRFV-UHFFFAOYSA-N') + +photo_leucine = ihm.ChemDescriptor( + 'L-Photo-Leucine', chemical_name='L-Photo-Leucine', + smiles='CC1(C[C@H](N)C(O)=O)N=N1', + inchi='1S/C5H9N3O2/c1-5(7-8-5)' + '2-3(6)4(9)10/h3H,2,6H2,1H3,(H,9,10)/t3-/m0/s1', + inchi_key='MJRDGTVDJKACQZ-VKHMYHEASA-N') + +dsbu = ihm.ChemDescriptor( + 'DSBU', chemical_name='disuccinimidyl dibutyric urea', + smiles='O=C(NCCCC(=O)ON1C(=O)CCC1=O)NCCCC(=O)ON2C(=O)CCC2=O', + inchi='S/C17H22N4O9/c22-11-5-6-12(23)20(11)29-15(26)' + '3-1-9-18-17(28)19-10-2-4-16(27)30-21-13(24)7-8-14(21)' + '25/h1-10H2,(H2,18,19,28)', + inchi_key='XZSQCCZQFXUQCY-UHFFFAOYSA-N') + +phoX = ihm.ChemDescriptor( + 'DSPP', chemical_name='(3,5-bis(((2,5-dioxopyrrolidin-1-yl)oxy)' + 'carbonyl) phenyl)phosphonic acid') + +tbuphoX = ihm.ChemDescriptor( + 'TBDSPP', chemical_name='tert-butyl disuccinimidyl' + 'phenyl phosphonate, tBu-PhoX') diff --git a/modules/core/dependency/python-ihm/ihm/dumper.py b/modules/core/dependency/python-ihm/ihm/dumper.py index 55a182eb75..0beda144c0 100644 --- a/modules/core/dependency/python-ihm/ihm/dumper.py +++ b/modules/core/dependency/python-ihm/ihm/dumper.py @@ -718,7 +718,7 @@ def get_next_id(self): self.index += 1 while self.ids[self.index] in self.seen_ids: self.index += 1 - # Note tha we don't need to add our own IDs to seen_ids since + # Note that we don't need to add our own IDs to seen_ids since # they are already guaranteed to be unique return self.ids[self.index] diff --git a/modules/core/dependency/python-ihm/ihm/format_bcif.py b/modules/core/dependency/python-ihm/ihm/format_bcif.py index 327d755d13..be5a1481a1 100644 --- a/modules/core/dependency/python-ihm/ihm/format_bcif.py +++ b/modules/core/dependency/python-ihm/ihm/format_bcif.py @@ -504,7 +504,7 @@ def _get_mask_and_type(data): mask[i] = 1 if val is None else 2 else: seen_types.add(type(val)) - # If a mix of types, coerce to that of the highest precendence + # If a mix of types, coerce to that of the highest precedence # (mixed int/float can be represented as float; mix int/float/str can # be represented as str; bool is represented as str) if not seen_types or bool in seen_types or str in seen_types: diff --git a/modules/core/dependency/python-ihm/ihm/location.py b/modules/core/dependency/python-ihm/ihm/location.py index 153f90a877..5e46d79c34 100644 --- a/modules/core/dependency/python-ihm/ihm/location.py +++ b/modules/core/dependency/python-ihm/ihm/location.py @@ -61,18 +61,21 @@ def __hash__(self): class DatabaseLocation(Location): """A dataset stored in an official database (PDB, EMDB, PRIDE, etc.). + Generally a subclass should be used specific to the database - + for example, :class:`PDBLocation`, :class:`EMDBLocation`, or + :class:`PRIDELocation`, although this base class can be used directly + for "other" databases not currently supported by the IHM dictionary. - :param str db_name: The name of the database. :param str db_code: The accession code inside the database. :param str version: The version of the dataset in the database. :param str details: Additional details about the dataset, if known. """ _eq_keys = Location._eq_keys + ['db_name', 'access_code', 'version'] + db_name = 'Other' - def __init__(self, db_name, db_code, version=None, details=None): + def __init__(self, db_code, version=None, details=None): super(DatabaseLocation, self).__init__(details) - self.db_name = db_name self.access_code = db_code self.version = version @@ -81,154 +84,98 @@ class EMDBLocation(DatabaseLocation): """Something stored in the EMDB database. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'EMDB' - - def __init__(self, db_code, version=None, details=None): - super(EMDBLocation, self).__init__(self._db_name, db_code, - version, details) + db_name = 'EMDB' class PDBLocation(DatabaseLocation): """Something stored in the PDB database. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'PDB' - - def __init__(self, db_code, version=None, details=None): - super(PDBLocation, self).__init__(self._db_name, db_code, - version, details) + db_name = 'PDB' class PDBDevLocation(DatabaseLocation): """Something stored in the PDB-Dev database. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'PDB-Dev' - - def __init__(self, db_code, version=None, details=None): - super(PDBDevLocation, self).__init__(self._db_name, db_code, - version, details) + db_name = 'PDB-Dev' class ModelArchiveLocation(DatabaseLocation): """Something stored in Model Archive. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'MODEL ARCHIVE' - - def __init__(self, db_code, version=None, details=None): - super(ModelArchiveLocation, self).__init__(self._db_name, db_code, - version, details) + db_name = 'MODEL ARCHIVE' class BMRBLocation(DatabaseLocation): """Something stored in the BMRB database. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'BMRB' - - def __init__(self, db_code, version=None, details=None): - super(BMRBLocation, self).__init__(self._db_name, db_code, - version, details) + db_name = 'BMRB' class MassIVELocation(DatabaseLocation): """Something stored in the MassIVE database. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'MASSIVE' - - def __init__(self, db_code, version=None, details=None): - super(MassIVELocation, self).__init__(self._db_name, db_code, version, - details) + db_name = 'MASSIVE' class EMPIARLocation(DatabaseLocation): """Something stored in the EMPIAR database. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'EMPIAR' - - def __init__(self, db_code, version=None, details=None): - super(EMPIARLocation, self).__init__(self._db_name, db_code, version, - details) + db_name = 'EMPIAR' class SASBDBLocation(DatabaseLocation): """Something stored in the SASBDB database. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'SASBDB' - - def __init__(self, db_code, version=None, details=None): - super(SASBDBLocation, self).__init__(self._db_name, db_code, version, - details) + db_name = 'SASBDB' class PRIDELocation(DatabaseLocation): """Something stored in the PRIDE database. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'PRIDE' - - def __init__(self, db_code, version=None, details=None): - super(PRIDELocation, self).__init__(self._db_name, db_code, version, - details) + db_name = 'PRIDE' class JPOSTLocation(DatabaseLocation): """Something stored in the JPOST database. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'jPOSTrepo' - - def __init__(self, db_code, version=None, details=None): - super(JPOSTLocation, self).__init__(self._db_name, db_code, version, - details) + db_name = 'jPOSTrepo' class BioGRIDLocation(DatabaseLocation): """Something stored in the BioGRID database. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'BioGRID' - - def __init__(self, db_code, version=None, details=None): - super(BioGRIDLocation, self).__init__(self._db_name, db_code, version, - details) + db_name = 'BioGRID' class ProXLLocation(DatabaseLocation): """Something stored in the ProXL database. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'ProXL' - - def __init__(self, db_code, version=None, details=None): - super(ProXLLocation, self).__init__(self._db_name, db_code, version, - details) + db_name = 'ProXL' class IProXLocation(DatabaseLocation): """Something stored in the iProX database. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'iProX' - - def __init__(self, db_code, version=None, details=None): - super(IProXLocation, self).__init__(self._db_name, db_code, version, - details) + db_name = 'iProX' class AlphaFoldDBLocation(DatabaseLocation): """Something stored in the AlphaFoldDB database. See :class:`DatabaseLocation` for a description of the parameters and :class:`Location` for discussion of the usage of these objects.""" - _db_name = 'AlphaFoldDB' - - def __init__(self, db_code, version=None, details=None): - super(AlphaFoldDBLocation, self).__init__( - self._db_name, db_code, version, details) + db_name = 'AlphaFoldDB' class FileLocation(Location): diff --git a/modules/core/dependency/python-ihm/ihm/reader.py b/modules/core/dependency/python-ihm/ihm/reader.py index ecd7e72ce9..bb713b604f 100644 --- a/modules/core/dependency/python-ihm/ihm/reader.py +++ b/modules/core/dependency/python-ihm/ihm/reader.py @@ -1421,11 +1421,11 @@ def __init__(self, *args): super(_DatasetDBRefHandler, self).__init__(*args) # Map data_type to corresponding # subclass of ihm.location.DatabaseLocation + # or ihm.location.DatabaseLocation itself self.type_map = dict( - (x[1]._db_name.lower(), x[1]) + (x[1].db_name.lower(), x[1]) for x in inspect.getmembers(ihm.location, inspect.isclass) - if issubclass(x[1], ihm.location.DatabaseLocation) - and x[1] is not ihm.location.DatabaseLocation) + if issubclass(x[1], ihm.location.DatabaseLocation)) def __call__(self, dataset_list_id, db_name, id, version, details, accession_code): @@ -1434,7 +1434,7 @@ def __call__(self, dataset_list_id, db_name, id, version, details, dbloc = self.sysr.db_locations.get_by_id(id, self.type_map.get(typ, None)) # Preserve user-provided name for unknown databases - if dbloc.db_name is None and db_name is not None: + if dbloc.db_name == 'Other' and db_name is not None: dbloc.db_name = db_name ds.location = dbloc self.copy_if_present( diff --git a/modules/core/dependency/python-ihm/src/ihm_format.c b/modules/core/dependency/python-ihm/src/ihm_format.c index c3bbe6d895..0ad6e4ebd0 100644 --- a/modules/core/dependency/python-ihm/src/ihm_format.c +++ b/modules/core/dependency/python-ihm/src/ihm_format.c @@ -229,7 +229,7 @@ struct ihm_mapping { ihm_destroy_callback value_destroy_func; }; -/* Make a new mapping from case-insensitive strings to arbitary pointers. +/* Make a new mapping from case-insensitive strings to arbitrary pointers. The mapping uses a simple binary search (more memory efficient than a hash table and generally faster too since the number of keys is quite small). */ diff --git a/modules/core/dependency/python-ihm/test/test_dumper.py b/modules/core/dependency/python-ihm/test/test_dumper.py index 9c2da4d362..ced0fe207b 100644 --- a/modules/core/dependency/python-ihm/test/test_dumper.py +++ b/modules/core/dependency/python-ihm/test/test_dumper.py @@ -1230,7 +1230,7 @@ def test_dataset_dumper_duplicates_details(self): def test_dataset_dumper_duplicates_samedata_sameloc(self): """DatasetDumper doesn't duplicate same datasets in same location""" system = ihm.System() - loc1 = ihm.location.DatabaseLocation("mydb", "abc", "1.0", "") + loc1 = ihm.location.DatabaseLocation("abc", "1.0", "") # Identical datasets in the same location aren't duplicated cx1 = ihm.dataset.CXMSDataset(loc1) @@ -1246,8 +1246,8 @@ def test_dataset_dumper_duplicates_samedata_sameloc(self): def test_dataset_dumper_duplicates_samedata_diffloc(self): """DatasetDumper is OK with same datasets in different locations""" system = ihm.System() - loc1 = ihm.location.DatabaseLocation("mydb", "abc", "1.0", "") - loc2 = ihm.location.DatabaseLocation("mydb", "xyz", "1.0", "") + loc1 = ihm.location.DatabaseLocation("abc", "1.0", "") + loc2 = ihm.location.DatabaseLocation("xyz", "1.0", "") cx1 = ihm.dataset.CXMSDataset(loc1) cx2 = ihm.dataset.CXMSDataset(loc2) dump = ihm.dumper._DatasetDumper() @@ -1261,7 +1261,7 @@ def test_dataset_dumper_duplicates_diffdata_sameloc(self): """DatasetDumper is OK with different datasets in same location""" system = ihm.System() # Different datasets in same location are OK (but odd) - loc2 = ihm.location.DatabaseLocation("mydb", "xyz", "1.0", "") + loc2 = ihm.location.DatabaseLocation("xyz", "1.0", "") cx2 = ihm.dataset.CXMSDataset(loc2) em3d = ihm.dataset.EMDensityDataset(loc2) dump = ihm.dumper._DatasetDumper() diff --git a/modules/core/dependency/python-ihm/test/test_flr.py b/modules/core/dependency/python-ihm/test/test_flr.py index 492c964edf..7746d811e8 100644 --- a/modules/core/dependency/python-ihm/test/test_flr.py +++ b/modules/core/dependency/python-ihm/test/test_flr.py @@ -26,7 +26,7 @@ def test_probe_eq(self): self.assertTrue(p_ref != p_unequal) def test_probe_descriptor_init(self): - """ Test initalization of ProbeDescriptor """ + """ Test initialization of ProbeDescriptor """ p = ihm.flr.ProbeDescriptor(reactive_probe_chem_descriptor='foo', chromophore_chem_descriptor='bar', chromophore_center_atom='foo2') @@ -399,7 +399,7 @@ def test_exp_condition_eq(self): self.assertTrue(e_ref != e_unequal) def test_fret_analysis_init(self): - """Test initalization of FRETAnalysis.""" + """Test initialization of FRETAnalysis.""" f = ihm.flr.FRETAnalysis( experiment='this_experiment', sample_probe_1='this_sample_probe_1', diff --git a/modules/core/dependency/python-ihm/test/test_location.py b/modules/core/dependency/python-ihm/test/test_location.py index 45b0ba382e..20ff9f4917 100644 --- a/modules/core/dependency/python-ihm/test/test_location.py +++ b/modules/core/dependency/python-ihm/test/test_location.py @@ -16,16 +16,16 @@ class Tests(unittest.TestCase): def test_database_location(self): """Test DatabaseLocation""" - dl1 = ihm.location.DatabaseLocation('mydb', 'abc', version=1) - dl2 = ihm.location.DatabaseLocation('mydb', 'abc', version=1) + dl1 = ihm.location.DatabaseLocation('abc', version=1) + dl2 = ihm.location.DatabaseLocation('abc', version=1) self.assertEqual(dl1, dl2) - dl3 = ihm.location.DatabaseLocation('mydb', 'abc', version=2) + dl3 = ihm.location.DatabaseLocation('abc', version=2) self.assertNotEqual(dl1, dl3) # details can change without affecting equality - dl4 = ihm.location.DatabaseLocation('mydb', 'abc', version=1, + dl4 = ihm.location.DatabaseLocation('abc', version=1, details='foo') self.assertEqual(dl1, dl4) - self.assertEqual(dl1.db_name, 'mydb') + self.assertEqual(dl1.db_name, 'Other') self.assertEqual(dl1.access_code, 'abc') self.assertEqual(dl1.version, 1) self.assertIsNone(dl1.details) diff --git a/modules/core/dependency/python-ihm/test/test_main.py b/modules/core/dependency/python-ihm/test/test_main.py index 640c277137..f0222d38bd 100644 --- a/modules/core/dependency/python-ihm/test/test_main.py +++ b/modules/core/dependency/python-ihm/test/test_main.py @@ -375,7 +375,7 @@ def test_asym_unit_residue(self): e = ihm.Entity('AHCDAH') a = ihm.AsymUnit(e, auth_seq_id_map=5) r = a.residue(3) - self.assertIsNone(r.entity) + self.assertEqual(r.entity, e) self.assertEqual(r.asym, a) self.assertEqual(r.seq_id, 3) self.assertEqual(r.auth_seq_id, 8) @@ -400,7 +400,7 @@ def test_atom_asym(self): a = asym.residue(3).atom('CA') self.assertEqual(a.id, 'CA') self.assertEqual(a.residue.seq_id, 3) - self.assertIsNone(a.entity) + self.assertEqual(a.entity, e) self.assertEqual(a.asym, asym) self.assertEqual(a.seq_id, 3) diff --git a/modules/core/dependency/python-ihm/test/test_make_mmcif.py b/modules/core/dependency/python-ihm/test/test_make_mmcif.py index 86c10b8b5a..d3206b071e 100644 --- a/modules/core/dependency/python-ihm/test/test_make_mmcif.py +++ b/modules/core/dependency/python-ihm/test/test_make_mmcif.py @@ -54,6 +54,13 @@ def test_bad_usage(self): ret = subprocess.call([sys.executable, MAKE_MMCIF]) self.assertEqual(ret, 1) + @unittest.skipIf(sys.version_info[0] < 3, "make-mmcif.py needs Python 3") + def test_same_file(self): + """Check that make-mmcif fails if input and output are the same""" + incif = utils.get_input_file_name(TOPDIR, 'struct_only.cif') + ret = subprocess.call([sys.executable, MAKE_MMCIF, incif, incif]) + self.assertEqual(ret, 1) + @unittest.skipIf(sys.version_info[0] < 3, "make-mmcif.py needs Python 3") def test_mini(self): """Check that make-mmcif works given only basic atom info""" diff --git a/modules/core/dependency/python-ihm/test/test_reader.py b/modules/core/dependency/python-ihm/test/test_reader.py index 3b9bb03c81..f24772281d 100644 --- a/modules/core/dependency/python-ihm/test/test_reader.py +++ b/modules/core/dependency/python-ihm/test/test_reader.py @@ -1064,7 +1064,7 @@ def test_dataset_dbref_handler(self): self.assertEqual(d3.location.access_code, 'EMD-123') self.assertIsNone(d3.location.version) self.assertIsNone(d3.location.details) - self.assertIsNone(d4.location.db_name) + self.assertEqual(d4.location.db_name, 'Other') self.assertEqual(d4.location.__class__, ihm.location.DatabaseLocation) self.assertIsNone(d4.location.access_code) diff --git a/modules/core/dependency/python-ihm/util/make-mmcif.py b/modules/core/dependency/python-ihm/util/make-mmcif.py index 2f0ac1683c..817858fae6 100644 --- a/modules/core/dependency/python-ihm/util/make-mmcif.py +++ b/modules/core/dependency/python-ihm/util/make-mmcif.py @@ -22,6 +22,7 @@ import ihm.model import ihm.protocol import sys +import os def add_ihm_info(s): @@ -62,6 +63,10 @@ def add_ihm_info(s): else: out_fname = 'output.cif' +if (os.path.exists(fname) and os.path.exists(out_fname) + and os.path.samefile(fname, out_fname)): + raise ValueError("Input and output are the same file") + with open(fname) as fh: with open(out_fname, 'w') as fhout: ihm.dumper.write(