From 445faffd769117b883938c35fbca078f0d0ad25e Mon Sep 17 00:00:00 2001 From: dhrubchatterjee16-lang Date: Wed, 21 Oct 2026 21:41:25 +0530 Subject: [PATCH 1/4] fix(core): add data type validation when setting topology attributes (#2764) --- package/AUTHORS | 1 + package/CHANGELOG | 4 + package/MDAnalysis/core/topologyattrs.py | 78 ++++++++++++++++- .../core/test_topologyattrs.py | 86 +++++++++++++++++-- 4 files changed, 163 insertions(+), 6 deletions(-) diff --git a/package/AUTHORS b/package/AUTHORS index 3d87a0651c..3a4ac88f42 100644 --- a/package/AUTHORS +++ b/package/AUTHORS @@ -286,6 +286,7 @@ Chronological list of authors - Aryaman Chaudhri - Francesco Siciliani - Akshit Boora + - Dhrub Chatterjee External code diff --git a/package/CHANGELOG b/package/CHANGELOG index 72b880c821..adc9f5c211 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -23,6 +23,10 @@ The rules for this file: * 2.11.0 Fixes + * Setting topology attributes (such as atom names, residue IDs, charges) + now validates that assigned values match the attribute's expected data type + and raises a clear TypeError rather than failing with confusing internal + errors or silently coercing invalid types (Issue #2764) * Fixed PBC handling in triclinic boxes in `_triclinic_pbc()` (used by `distances.apply_PBC()`) and `lib.nsgrid.NSGrid` (used by `capped_distance` and `self_capped_distance`): diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index 6ad9dc5198..6be50ba01d 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -85,6 +85,75 @@ from .. import _TOPOLOGY_ATTRS, _TOPOLOGY_TRANSPLANTS, _TOPOLOGY_ATTRNAMES +def _check_dtype(attr, group, values): + """Check that values match the expected dtype of the TopologyAttr.""" + is_component = isinstance(group, ComponentBase) + is_iter = iterable(values) + attr_name = attr.singular if is_component else attr.attrname + + # String attributes (either _StringInternerMixin or dtype == "U1" or object) + if isinstance(attr, _StringInternerMixin) or getattr(attr, "dtype", None) == "U1": + if is_iter: + if len(values) == 0: + return + if isinstance(values, np.ndarray) and np.issubdtype(values.dtype, np.str_): + return + if not all(isinstance(v, str) for v in values): + raise TypeError(f"All values for {attr_name} must be strings") + else: + if not isinstance(values, str): + raise TypeError(f"{attr_name} must be a string") + return + + dtype = getattr(attr, "dtype", None) + if dtype is None: + return + + try: + is_int = np.issubdtype(dtype, np.integer) + is_float = np.issubdtype(dtype, np.floating) + is_bool = np.issubdtype(dtype, np.bool_) + except TypeError: + return + + if is_int: + if is_iter: + if len(values) == 0: + return + if isinstance(values, np.ndarray): + if not (np.issubdtype(values.dtype, np.integer) and not np.issubdtype(values.dtype, np.bool_)): + raise TypeError(f"All values for {attr_name} must be integers") + elif not all(isinstance(v, (int, np.integer)) and not isinstance(v, (bool, np.bool_)) for v in values): + raise TypeError(f"All values for {attr_name} must be integers") + else: + if not (isinstance(values, (int, np.integer)) and not isinstance(values, (bool, np.bool_))): + raise TypeError(f"{attr_name} must be an integer") + elif is_float: + if is_iter: + if len(values) == 0: + return + if isinstance(values, np.ndarray): + if not (np.issubdtype(values.dtype, np.number) and not np.issubdtype(values.dtype, np.bool_)): + raise TypeError(f"All values for {attr_name} must be numbers") + elif not all(isinstance(v, (int, float, np.number)) and not isinstance(v, (bool, np.bool_)) for v in values): + raise TypeError(f"All values for {attr_name} must be numbers") + else: + if not (isinstance(values, (int, float, np.number)) and not isinstance(values, (bool, np.bool_))): + raise TypeError(f"{attr_name} must be a number") + elif is_bool: + if is_iter: + if len(values) == 0: + return + if isinstance(values, np.ndarray): + if not np.issubdtype(values.dtype, np.bool_): + raise TypeError(f"All values for {attr_name} must be booleans") + elif not all(isinstance(v, (bool, np.bool_)) for v in values): + raise TypeError(f"All values for {attr_name} must be booleans") + else: + if not isinstance(values, (bool, np.bool_)): + raise TypeError(f"{attr_name} must be a boolean") + + def _check_length(func): """Wrapper which checks the length of inputs to set_X @@ -151,6 +220,7 @@ def wrapper(attr, group, values): lenvalues=val_len, ) ) + _check_dtype(attr, group, values) # if everything went OK, continue with the function return func(attr, group, values) @@ -842,9 +912,11 @@ def _set_X(self, ag, values): newidx = len(self.namedict) self.namedict[values] = newidx newnames.append(values) - else: + elif iterable(values): newidx = np.zeros_like(values, dtype=int) for i, val in enumerate(values): + if not isinstance(val, str): + raise TypeError("All values must be strings") try: newidx[i] = self.namedict[val] except KeyError: @@ -852,6 +924,10 @@ def _set_X(self, ag, values): self.namedict[val] = nextidx newnames.append(val) newidx[i] = nextidx + else: + raise TypeError( + f"Values must be a string or iterable of strings, got {type(values).__name__}" + ) self.nmidx[ag.ix] = ( newidx # newidx either single value or same size array diff --git a/testsuite/MDAnalysisTests/core/test_topologyattrs.py b/testsuite/MDAnalysisTests/core/test_topologyattrs.py index 4d8b3349aa..b21b70a4a1 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyattrs.py +++ b/testsuite/MDAnalysisTests/core/test_topologyattrs.py @@ -125,8 +125,8 @@ def test_set_atoms_singular(self, attr): def test_set_atoms_plural(self, attr): # set len 2 Group to len 2 values dg = DummyGroup([3, 7]) - attr.set_atoms(dg, np.array([23, 504])) - assert_equal(attr.get_atoms(dg), np.array([23, 504])) + attr.set_atoms(dg, self.values[:2]) + assert_equal(attr.get_atoms(dg), self.values[:2]) def test_set_atoms_VE(self, attr): # set len 2 Group to wrong length values @@ -305,9 +305,9 @@ def test_set_residues_singular(self, attr): ) def test_set_residues_plural(self, attr): - attr.set_residues(DummyGroup([3, 0, 1]), np.array([23, 504, 2])) - assert_almost_equal( - attr.get_residues(DummyGroup([3, 0, 1])), np.array([23, 504, 2]) + attr.set_residues(DummyGroup([3, 0, 1]), self.values[:3]) + assert_equal( + attr.get_residues(DummyGroup([3, 0, 1])), self.values[:3] ) def test_set_residues_VE(self, attr): @@ -786,3 +786,79 @@ def test_coc_unwrap(self, u, compound): u.atoms.wrap coc = u.atoms[:2].center_of_charge(compound=compound, unwrap=True) assert_equal(coc.flatten(), [0, -0.5, 0]) + + +class TestDtypeChecks: + @pytest.fixture + def u(self): + universe = mda.Universe.empty( + n_atoms=6, + n_residues=3, + n_segments=2, + atom_resindex=[0, 0, 1, 1, 2, 2], + residue_segindex=[0, 0, 1], + ) + universe.add_TopologyAttr("names", ["C", "CA", "N", "O", "CB", "CG"]) + universe.add_TopologyAttr("resnames", ["ALA", "GLU", "VAL"]) + universe.add_TopologyAttr("segids", ["SEG1", "SEG2"]) + universe.add_TopologyAttr("resids", [1, 2, 3]) + universe.add_TopologyAttr("charges", [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) + universe.add_TopologyAttr("masses", [12.0, 12.0, 14.0, 16.0, 12.0, 12.0]) + return universe + + @pytest.mark.parametrize("invalid", [1, 2.5, True, None]) + def test_string_attr_atom_invalid(self, u, invalid): + with pytest.raises(TypeError, match="must be a string"): + u.atoms[0].name = invalid + + @pytest.mark.parametrize("invalid", [[1, 2, 3], ["C", 1, "O"], np.array([1, 2, 3]), 1]) + def test_string_attr_group_invalid(self, u, invalid): + with pytest.raises(TypeError, match="must be (a string|strings)"): + u.atoms[:3].names = invalid + + @pytest.mark.parametrize("invalid", [2.5, "1", True, None]) + def test_int_attr_residue_invalid(self, u, invalid): + with pytest.raises(TypeError, match="must be an integer"): + u.residues[0].resid = invalid + + @pytest.mark.parametrize("invalid", [[1.5, 2.5], np.array([1.5, 2.5]), [1, "2"], [True, False], 2.5]) + def test_int_attr_group_invalid(self, u, invalid): + with pytest.raises(TypeError, match="must be (an integer|integers)"): + u.residues[:2].resids = invalid + + @pytest.mark.parametrize("invalid", ["positive", True, None]) + def test_float_attr_atom_invalid(self, u, invalid): + with pytest.raises(TypeError, match="must be a number"): + u.atoms[0].charge = invalid + + @pytest.mark.parametrize("invalid", [["a", "b", "c"], [True, False, True], "positive"]) + def test_float_attr_group_invalid(self, u, invalid): + with pytest.raises(TypeError, match="must be (a number|numbers)"): + u.atoms[:3].charges = invalid + + def test_valid_assignments(self, u): + u.atoms[0].name = "N" + assert u.atoms[0].name == "N" + + u.atoms[:3].names = ["C1", "C2", "C3"] + assert list(u.atoms[:3].names) == ["C1", "C2", "C3"] + + u.atoms.names = "CA" + assert all(name == "CA" for name in u.atoms.names) + + u.residues[0].resid = 10 + assert u.residues[0].resid == 10 + + u.residues[:2].resids = [100, 200] + assert list(u.residues[:2].resids) == [100, 200] + + u.residues.resids = 1 + assert all(r == 1 for r in u.residues.resids) + + # ints should be allowed for float attributes + u.atoms[0].charge = 1 + assert u.atoms[0].charge == 1.0 + + u.atoms[:2].charges = [-0.5, 0.5] + assert_equal(u.atoms[:2].charges, [-0.5, 0.5]) + From e3264a5d9543fa0b1b298a59e37385ae96d47273 Mon Sep 17 00:00:00 2001 From: dhrubchatterjee16-lang Date: Thu, 22 Oct 2026 03:12:18 +0530 Subject: [PATCH 2/4] fix(core): format with black and achieve 100% test coverage for dtype validation --- package/MDAnalysis/core/topologyattrs.py | 243 ++++++++---------- .../core/test_topologyattrs.py | 109 ++++++-- 2 files changed, 185 insertions(+), 167 deletions(-) diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index 6be50ba01d..b67e8dec0f 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -91,67 +91,79 @@ def _check_dtype(attr, group, values): is_iter = iterable(values) attr_name = attr.singular if is_component else attr.attrname - # String attributes (either _StringInternerMixin or dtype == "U1" or object) if isinstance(attr, _StringInternerMixin) or getattr(attr, "dtype", None) == "U1": - if is_iter: - if len(values) == 0: - return - if isinstance(values, np.ndarray) and np.issubdtype(values.dtype, np.str_): - return - if not all(isinstance(v, str) for v in values): - raise TypeError(f"All values for {attr_name} must be strings") - else: - if not isinstance(values, str): - raise TypeError(f"{attr_name} must be a string") - return - - dtype = getattr(attr, "dtype", None) - if dtype is None: - return - - try: - is_int = np.issubdtype(dtype, np.integer) - is_float = np.issubdtype(dtype, np.floating) - is_bool = np.issubdtype(dtype, np.bool_) - except TypeError: - return - - if is_int: - if is_iter: - if len(values) == 0: - return - if isinstance(values, np.ndarray): - if not (np.issubdtype(values.dtype, np.integer) and not np.issubdtype(values.dtype, np.bool_)): - raise TypeError(f"All values for {attr_name} must be integers") - elif not all(isinstance(v, (int, np.integer)) and not isinstance(v, (bool, np.bool_)) for v in values): - raise TypeError(f"All values for {attr_name} must be integers") - else: - if not (isinstance(values, (int, np.integer)) and not isinstance(values, (bool, np.bool_))): - raise TypeError(f"{attr_name} must be an integer") - elif is_float: - if is_iter: - if len(values) == 0: - return - if isinstance(values, np.ndarray): - if not (np.issubdtype(values.dtype, np.number) and not np.issubdtype(values.dtype, np.bool_)): - raise TypeError(f"All values for {attr_name} must be numbers") - elif not all(isinstance(v, (int, float, np.number)) and not isinstance(v, (bool, np.bool_)) for v in values): - raise TypeError(f"All values for {attr_name} must be numbers") + kind = "str" + type_desc = "a string" + plural_desc = "strings" + else: + dtype = getattr(attr, "dtype", None) + if dtype is None: + return + if np.issubdtype(dtype, np.integer): + kind = "int" + type_desc = "an integer" + plural_desc = "integers" + elif np.issubdtype(dtype, np.floating): + kind = "float" + type_desc = "a number" + plural_desc = "numbers" + elif np.issubdtype(dtype, np.bool_): + kind = "bool" + type_desc = "a boolean" + plural_desc = "booleans" else: - if not (isinstance(values, (int, float, np.number)) and not isinstance(values, (bool, np.bool_))): - raise TypeError(f"{attr_name} must be a number") - elif is_bool: - if is_iter: - if len(values) == 0: - return - if isinstance(values, np.ndarray): - if not np.issubdtype(values.dtype, np.bool_): - raise TypeError(f"All values for {attr_name} must be booleans") - elif not all(isinstance(v, (bool, np.bool_)) for v in values): - raise TypeError(f"All values for {attr_name} must be booleans") + return + + if is_iter: + if isinstance(values, np.ndarray) and values.dtype != object: + if kind == "str": + valid = np.issubdtype(values.dtype, np.str_) + elif kind == "int": + valid = np.issubdtype(values.dtype, np.integer) and not np.issubdtype( + values.dtype, np.bool_ + ) + elif kind == "float": + valid = np.issubdtype(values.dtype, np.number) and not np.issubdtype( + values.dtype, np.bool_ + ) + else: # bool + valid = np.issubdtype(values.dtype, np.bool_) else: - if not isinstance(values, (bool, np.bool_)): - raise TypeError(f"{attr_name} must be a boolean") + if kind == "str": + valid = all(isinstance(v, str) for v in values) + elif kind == "int": + valid = all( + isinstance(v, (int, np.integer)) + and not isinstance(v, (bool, np.bool_)) + for v in values + ) + elif kind == "float": + valid = all( + isinstance(v, (int, float, np.number)) + and not isinstance(v, (bool, np.bool_)) + for v in values + ) + else: # bool + valid = all(isinstance(v, (bool, np.bool_)) for v in values) + + if not valid: + raise TypeError(f"All values for {attr_name} must be {plural_desc}") + else: + if kind == "str": + valid = isinstance(values, str) + elif kind == "int": + valid = isinstance(values, (int, np.integer)) and not isinstance( + values, (bool, np.bool_) + ) + elif kind == "float": + valid = isinstance(values, (int, float, np.number)) and not isinstance( + values, (bool, np.bool_) + ) + else: # bool + valid = isinstance(values, (bool, np.bool_)) + + if not valid: + raise TypeError(f"{attr_name} must be {type_desc}") def _check_length(func): @@ -317,18 +329,14 @@ def stub_method(self, *args, **kwargs): ) raise NoDataError(message) - annotation = textwrap.dedent( - """\ + annotation = textwrap.dedent("""\ .. note:: This requires the underlying topology to have {}. Otherwise, a :exc:`~MDAnalysis.exceptions.NoDataError` is raised. - """.format( - attribute_name - ) - ) + """.format(attribute_name)) # The original docstring is assumed to be formatted with black # (i.e., *all text* lines are already indented, the first line # is a triple quote followed by newline) @@ -466,9 +474,7 @@ def __init__(cls, name, bases, classdict): if dtype is not None: per_obj = classdict.get("per_object", bases[0].per_object) try: - selection.gen_selection_class( - singular, attrname, dtype, per_obj - ) + selection.gen_selection_class(singular, attrname, dtype, per_obj) except ValueError: msg = ( "A selection keyword could not be " @@ -546,9 +552,7 @@ def _gen_initial_values(n_atoms, n_residues, n_segments): raise NotImplementedError("No default values") @classmethod - def from_blank( - cls, n_atoms=None, n_residues=None, n_segments=None, values=None - ): + def from_blank(cls, n_atoms=None, n_residues=None, n_segments=None, values=None): """Create a blank version of this TopologyAttribute Parameters @@ -912,11 +916,9 @@ def _set_X(self, ag, values): newidx = len(self.namedict) self.namedict[values] = newidx newnames.append(values) - elif iterable(values): + else: newidx = np.zeros_like(values, dtype=int) for i, val in enumerate(values): - if not isinstance(val, str): - raise TypeError("All values must be strings") try: newidx[i] = self.namedict[val] except KeyError: @@ -924,14 +926,8 @@ def _set_X(self, ag, values): self.namedict[val] = nextidx newnames.append(val) newidx[i] = nextidx - else: - raise TypeError( - f"Values must be a string or iterable of strings, got {type(values).__name__}" - ) - self.nmidx[ag.ix] = ( - newidx # newidx either single value or same size array - ) + self.nmidx[ag.ix] = newidx # newidx either single value or same size array if newnames: self.name_lookup = np.concatenate([self.name_lookup, newnames]) self.values = self.name_lookup[self.nmidx] @@ -1054,8 +1050,7 @@ def phi_selections(residues, c_name="C", n_name="N", ca_name="CA"): keep_prev = [sum(r.atoms.names == c_name) == 1 for r in prev] keep_res = [ - all(sum(r.atoms.names == n) == 1 for n in ncac_names) - for r in residues + all(sum(r.atoms.names == n) == 1 for n in ncac_names) for r in residues ] keep = np.array(keep_prev) & np.array(keep_res) keep[invalid] = False @@ -1246,8 +1241,7 @@ def psi_selections(residues, c_name="C", n_name="N", ca_name="CA"): keep_nxt = [sum(r.atoms.names == n_name) == 1 for r in nxt] keep_res = [ - all(sum(r.atoms.names == n) == 1 for n in ncac_names) - for r in residues + all(sum(r.atoms.names == n) == 1 for n in ncac_names) for r in residues ] keep = np.array(keep_nxt) & np.array(keep_res) nxt = nxt[keep] @@ -1359,12 +1353,9 @@ def omega_selections(residues, c_name="C", n_name="N", ca_name="CA"): nxtatoms = [ca_name, n_name] resatoms = [ca_name, c_name] - keep_nxt = [ - all(sum(r.atoms.names == n) == 1 for n in nxtatoms) for r in nxt - ] + keep_nxt = [all(sum(r.atoms.names == n) == 1 for n in nxtatoms) for r in nxt] keep_res = [ - all(sum(r.atoms.names == n) == 1 for n in resatoms) - for r in residues + all(sum(r.atoms.names == n) == 1 for n in resatoms) for r in residues ] keep = np.array(keep_nxt) & np.array(keep_res) nxt = nxt[keep] @@ -1866,13 +1857,9 @@ def moment_of_inertia(group, wrap=False, unwrap=False, compound="group"): """ atomgroup = group.atoms - com = atomgroup.center_of_mass( - wrap=wrap, unwrap=unwrap, compound=compound - ) + com = atomgroup.center_of_mass(wrap=wrap, unwrap=unwrap, compound=compound) if compound != "group": - com = (com * group.masses[:, None]).sum( - axis=0 - ) / group.masses.sum() + com = (com * group.masses[:, None]).sum(axis=0) / group.masses.sum() if wrap: pos = atomgroup.pack_into_box(inplace=False) - com @@ -2003,9 +1990,7 @@ def _gyration(recenteredpos, masses): atomgroup = group.atoms masses = atomgroup.masses - com = atomgroup.center_of_mass( - wrap=wrap, unwrap=unwrap, compound=compound - ) + com = atomgroup.center_of_mass(wrap=wrap, unwrap=unwrap, compound=compound) if compound == "group": if wrap: @@ -2023,7 +2008,7 @@ def _gyration(recenteredpos, masses): recenteredpos = atomgroup.positions - com eig_vals = _gyration(recenteredpos, masses) else: - (atom_masks, compound_masks, n_compounds) = ( + atom_masks, compound_masks, n_compounds = ( atomgroup._split_by_compound_indices(compound) ) @@ -2140,8 +2125,7 @@ def asphericity(group, wrap=False, unwrap=False, compound="group"): ) else: shape = (3.0 / 2.0) * ( - np.sum((eig_vals - np.mean(eig_vals)) ** 2) - / np.sum(eig_vals) ** 2 + np.sum((eig_vals - np.mean(eig_vals)) ** 2) / np.sum(eig_vals) ** 2 ) return shape @@ -2225,9 +2209,7 @@ def align_principal_axis(group, axis, vector): # print "axis = %r, angle = %f deg" % (ax, angle) return group.rotateby(angle, ax) - transplants[GroupBase].append( - ("align_principal_axis", align_principal_axis) - ) + transplants[GroupBase].append(("align_principal_axis", align_principal_axis)) # TODO: update docs to property doc @@ -2384,9 +2366,7 @@ def total_charge(group, compound="group"): @warn_if_not_unique @_pbc_to_wrap @check_wrap_and_unwrap - def dipole_vector( - group, wrap=False, unwrap=False, compound="group", center="mass" - ): + def dipole_vector(group, wrap=False, unwrap=False, compound="group", center="mass"): r"""Dipole vector of the group. .. math:: @@ -2452,9 +2432,7 @@ def dipole_vector( if center == "mass": masses = atomgroup.masses - ref = atomgroup.center_of_mass( - wrap=wrap, unwrap=unwrap, compound=compound - ) + ref = atomgroup.center_of_mass(wrap=wrap, unwrap=unwrap, compound=compound) elif center == "charge": ref = atomgroup.center_of_charge( wrap=wrap, unwrap=unwrap, compound=compound @@ -2480,11 +2458,9 @@ def dipole_vector( ) else: recenteredpos = atomgroup.positions - ref - dipole_vector = np.einsum( - "ij,ij->j", recenteredpos, charges[:, np.newaxis] - ) + dipole_vector = np.einsum("ij,ij->j", recenteredpos, charges[:, np.newaxis]) else: - (atom_masks, compound_masks, n_compounds) = ( + atom_masks, compound_masks, n_compounds = ( atomgroup._split_by_compound_indices(compound) ) @@ -2566,13 +2542,9 @@ def dipole_moment(group, **kwargs): dipole_vector = atomgroup.dipole_vector(**kwargs) if len(dipole_vector.shape) > 1: - dipole_moment = np.sqrt( - np.einsum("ij,ij->i", dipole_vector, dipole_vector) - ) + dipole_moment = np.sqrt(np.einsum("ij,ij->i", dipole_vector, dipole_vector)) else: - dipole_moment = np.sqrt( - np.einsum("i,i->", dipole_vector, dipole_vector) - ) + dipole_moment = np.sqrt(np.einsum("i,i->", dipole_vector, dipole_vector)) return dipole_moment @@ -2665,9 +2637,7 @@ def __quadrupole_tensor(recenteredpos, charges): if center == "mass": masses = atomgroup.masses - ref = atomgroup.center_of_mass( - wrap=wrap, unwrap=unwrap, compound=compound - ) + ref = atomgroup.center_of_mass(wrap=wrap, unwrap=unwrap, compound=compound) elif center == "charge": ref = atomgroup.center_of_charge( wrap=wrap, unwrap=unwrap, compound=compound @@ -2695,7 +2665,7 @@ def __quadrupole_tensor(recenteredpos, charges): recenteredpos = atomgroup.positions - ref quad_tensor = __quadrupole_tensor(recenteredpos, charges) else: - (atom_masks, compound_masks, n_compounds) = ( + atom_masks, compound_masks, n_compounds = ( atomgroup._split_by_compound_indices(compound) ) @@ -2785,9 +2755,7 @@ def __quadrupole_moment(tensor): if len(quad_tensor.shape) == 2: quad_moment = __quadrupole_moment(quad_tensor) else: - quad_moment = np.array( - [__quadrupole_moment(x) for x in quad_tensor] - ) + quad_moment = np.array([__quadrupole_moment(x) for x in quad_tensor]) return quad_moment @@ -3111,9 +3079,7 @@ def sequence(self, **kwargs): ) ) try: - sequence = "".join( - [convert_aa_code(r) for r in self.residues.resnames] - ) + sequence = "".join([convert_aa_code(r) for r in self.residues.resnames]) except KeyError as err: errmsg = ( f"AtomGroup contains a residue name '{err.message}' that" @@ -3246,14 +3212,13 @@ def _check_connection_values(func): @functools.wraps(func) def wrapper(self, values, *args, **kwargs): if not all( - len(x) == self._n_atoms - and all(isinstance(y, (int, np.integer)) for y in x) + len(x) == self._n_atoms and all(isinstance(y, (int, np.integer)) for y in x) for x in values ): raise ValueError( - ( - "{} must be an iterable of tuples with {}" " atom indices" - ).format(self.attrname, self._n_atoms) + ("{} must be an iterable of tuples with {}" " atom indices").format( + self.attrname, self._n_atoms + ) ) clean = [] for v in values: @@ -3331,9 +3296,7 @@ def _bondDict(self): """Lazily built mapping of atoms:bonds""" bd = defaultdict(list) - for b, t, g, o in zip( - self.values, self.types, self._guessed, self.order - ): + for b, t, g, o in zip(self.values, self.types, self._guessed, self.order): for a in b: bd[a].append((b, t, g, o)) return bd @@ -3352,9 +3315,7 @@ def get_atoms(self, ag): """ try: - unique_bonds = set( - itertools.chain(*[self._bondDict[a] for a in ag.ix]) - ) + unique_bonds = set(itertools.chain(*[self._bondDict[a] for a in ag.ix])) except TypeError: # maybe we got passed an Atom unique_bonds = self._bondDict[ag.ix] diff --git a/testsuite/MDAnalysisTests/core/test_topologyattrs.py b/testsuite/MDAnalysisTests/core/test_topologyattrs.py index b21b70a4a1..31f235bdb3 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyattrs.py +++ b/testsuite/MDAnalysisTests/core/test_topologyattrs.py @@ -20,9 +20,8 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -"""Tests for MDAnalysis.core.topologyattrs objects. +"""Tests for MDAnalysis.core.topologyattrs objects.""" -""" import numpy as np from numpy.testing import ( @@ -233,9 +232,7 @@ class AggregationMixin(TestAtomAttr): def test_get_residues(self, attr): assert_equal( attr.get_residues(DummyGroup([2, 1])), - np.array( - [self.values[[2, 3, 9]].sum(), self.values[[4, 5, 8]].sum()] - ), + np.array([self.values[[2, 3, 9]].sum(), self.values[[4, 5, 8]].sum()]), ) def test_get_segments(self, attr): @@ -281,9 +278,7 @@ def test_set_residue_VE(self, universe): setattr(res, self.attrclass.singular, self.values[:2]) def test_get_atoms(self, attr): - assert_equal( - attr.get_atoms(DummyGroup([7, 3, 9])), self.values[[3, 2, 2]] - ) + assert_equal(attr.get_atoms(DummyGroup([7, 3, 9])), self.values[[3, 2, 2]]) def test_get_atom(self, universe): attr = getattr(universe.atoms[0], self.attrclass.singular) @@ -306,9 +301,7 @@ def test_set_residues_singular(self, attr): def test_set_residues_plural(self, attr): attr.set_residues(DummyGroup([3, 0, 1]), self.values[:3]) - assert_equal( - attr.get_residues(DummyGroup([3, 0, 1])), self.values[:3] - ) + assert_equal(attr.get_residues(DummyGroup([3, 0, 1])), self.values[:3]) def test_set_residues_VE(self, attr): dg = DummyGroup([3, 0, 1]) @@ -375,9 +368,7 @@ def test_set_segment_VE(self): setattr(seg, "segid", [1, 2, 3]) def test_get_atoms(self, attr): - assert_equal( - attr.get_atoms(DummyGroup([2, 4, 1])), self.values[[1, 1, 0]] - ) + assert_equal(attr.get_atoms(DummyGroup([2, 4, 1])), self.values[[1, 1, 0]]) def test_get_residues(self, attr): assert_equal( @@ -390,9 +381,7 @@ def test_get_segments(self, attr): atoms in segments. """ - assert_equal( - attr.get_segments(DummyGroup([1, 0, 0])), self.values[[1, 0, 0]] - ) + assert_equal(attr.get_segments(DummyGroup([1, 0, 0])), self.values[[1, 0, 0]]) def test_set_segments_singular(self, attr): dg = DummyGroup([0, 1]) @@ -439,9 +428,7 @@ def universe_pa(self): def test_principal_axes_handedness(self, universe_pa): e_vec = universe_pa.atoms.principal_axes() - assert_almost_equal( - np.dot(np.cross(e_vec[0], e_vec[1]), e_vec[2]), 1.0 - ) + assert_almost_equal(np.dot(np.cross(e_vec[0], e_vec[1]), e_vec[2]), 1.0) def test_align_principal_axes_with_self(self, ag): pa = ag.principal_axes() @@ -722,9 +709,7 @@ def test_from_lookup(self): assert_equal(attr.values, ref.values) assert attr.values.dtype == ref.values.dtype - assert all( - attr.name_lookup[i] == nm for nm, i in attr.namedict.items() - ) + assert all(attr.name_lookup[i] == nm for nm, i in attr.namedict.items()) u = mda.Universe.empty(n_atoms=5) u.add_TopologyAttr(attr) @@ -804,6 +789,9 @@ def u(self): universe.add_TopologyAttr("resids", [1, 2, 3]) universe.add_TopologyAttr("charges", [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) universe.add_TopologyAttr("masses", [12.0, 12.0, 14.0, 16.0, 12.0, 12.0]) + universe.add_TopologyAttr( + "aromaticities", [False, False, False, False, False, False] + ) return universe @pytest.mark.parametrize("invalid", [1, 2.5, True, None]) @@ -811,7 +799,9 @@ def test_string_attr_atom_invalid(self, u, invalid): with pytest.raises(TypeError, match="must be a string"): u.atoms[0].name = invalid - @pytest.mark.parametrize("invalid", [[1, 2, 3], ["C", 1, "O"], np.array([1, 2, 3]), 1]) + @pytest.mark.parametrize( + "invalid", [[1, 2, 3], ["C", 1, "O"], np.array([1, 2, 3]), 1] + ) def test_string_attr_group_invalid(self, u, invalid): with pytest.raises(TypeError, match="must be (a string|strings)"): u.atoms[:3].names = invalid @@ -821,7 +811,10 @@ def test_int_attr_residue_invalid(self, u, invalid): with pytest.raises(TypeError, match="must be an integer"): u.residues[0].resid = invalid - @pytest.mark.parametrize("invalid", [[1.5, 2.5], np.array([1.5, 2.5]), [1, "2"], [True, False], 2.5]) + @pytest.mark.parametrize( + "invalid", + [[1.5, 2.5], np.array([1.5, 2.5]), [1, "2"], [True, False], 2.5], + ) def test_int_attr_group_invalid(self, u, invalid): with pytest.raises(TypeError, match="must be (an integer|integers)"): u.residues[:2].resids = invalid @@ -831,11 +824,32 @@ def test_float_attr_atom_invalid(self, u, invalid): with pytest.raises(TypeError, match="must be a number"): u.atoms[0].charge = invalid - @pytest.mark.parametrize("invalid", [["a", "b", "c"], [True, False, True], "positive"]) + @pytest.mark.parametrize( + "invalid", + [ + ["a", "b", "c"], + [True, False, True], + np.array([True, False, True]), + "positive", + ], + ) def test_float_attr_group_invalid(self, u, invalid): with pytest.raises(TypeError, match="must be (a number|numbers)"): u.atoms[:3].charges = invalid + @pytest.mark.parametrize("invalid", [1, 0, "true", None]) + def test_bool_attr_atom_invalid(self, u, invalid): + with pytest.raises(TypeError, match="must be a boolean"): + u.atoms[0].aromaticity = invalid + + @pytest.mark.parametrize( + "invalid", + [[1, 0, 1], ["a", "b", "c"], np.array([1, 0, 1]), 1], + ) + def test_bool_attr_group_invalid(self, u, invalid): + with pytest.raises(TypeError, match="must be (a boolean|booleans)"): + u.atoms[:3].aromaticities = invalid + def test_valid_assignments(self, u): u.atoms[0].name = "N" assert u.atoms[0].name == "N" @@ -843,6 +857,9 @@ def test_valid_assignments(self, u): u.atoms[:3].names = ["C1", "C2", "C3"] assert list(u.atoms[:3].names) == ["C1", "C2", "C3"] + u.atoms[:3].names = np.array(["C1", "C2", "C3"]) + assert list(u.atoms[:3].names) == ["C1", "C2", "C3"] + u.atoms.names = "CA" assert all(name == "CA" for name in u.atoms.names) @@ -852,6 +869,9 @@ def test_valid_assignments(self, u): u.residues[:2].resids = [100, 200] assert list(u.residues[:2].resids) == [100, 200] + u.residues[:2].resids = np.array([100, 200]) + assert list(u.residues[:2].resids) == [100, 200] + u.residues.resids = 1 assert all(r == 1 for r in u.residues.resids) @@ -862,3 +882,40 @@ def test_valid_assignments(self, u): u.atoms[:2].charges = [-0.5, 0.5] assert_equal(u.atoms[:2].charges, [-0.5, 0.5]) + u.atoms[:2].charges = np.array([-0.5, 0.5]) + assert_equal(u.atoms[:2].charges, [-0.5, 0.5]) + + u.atoms.charges = 0.0 + assert all(c == 0.0 for c in u.atoms.charges) + + u.atoms[0].aromaticity = True + assert u.atoms[0].aromaticity == True + + u.atoms[:3].aromaticities = [True, False, True] + assert list(u.atoms[:3].aromaticities) == [True, False, True] + + u.atoms[:3].aromaticities = np.array([True, False, True]) + assert list(u.atoms[:3].aromaticities) == [True, False, True] + + u.atoms.aromaticities = True + assert all(a == True for a in u.atoms.aromaticities) + + def test_unsupported_or_none_dtype_bypass(self, u): + from MDAnalysis.core.topologyattrs import _check_dtype, TopologyAttr + + class DNone(TopologyAttr): + attrname = "dn" + singular = "d" + + def __init__(self): + self.dtype = None + + class DObj(TopologyAttr): + attrname = "do" + singular = "d" + + def __init__(self): + self.dtype = object + + _check_dtype(DNone(), u.atoms, [1, 2]) + _check_dtype(DObj(), u.atoms, [1, 2]) From ff1de73191f1fad28046f3c51d8747d21030b278 Mon Sep 17 00:00:00 2001 From: dhrubchatterjee16-lang Date: Thu, 22 Oct 2026 03:16:12 +0530 Subject: [PATCH 3/4] style: format topologyattrs with black 24 --- package/MDAnalysis/core/topologyattrs.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index b67e8dec0f..4584deda0f 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -329,14 +329,18 @@ def stub_method(self, *args, **kwargs): ) raise NoDataError(message) - annotation = textwrap.dedent("""\ + annotation = textwrap.dedent( + """\ .. note:: This requires the underlying topology to have {}. Otherwise, a :exc:`~MDAnalysis.exceptions.NoDataError` is raised. - """.format(attribute_name)) + """.format( + attribute_name + ) + ) # The original docstring is assumed to be formatted with black # (i.e., *all text* lines are already indented, the first line # is a triple quote followed by newline) From 18a64f0e998dd5e4a029b0534873f866895969c4 Mon Sep 17 00:00:00 2001 From: dhrubchatterjee16-lang Date: Thu, 22 Oct 2026 03:21:45 +0530 Subject: [PATCH 4/4] style: format with black (line-length 79) --- package/MDAnalysis/core/topologyattrs.py | 120 ++++++++++++------ .../core/test_topologyattrs.py | 28 +++- 2 files changed, 104 insertions(+), 44 deletions(-) diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index 4584deda0f..a86651738e 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -91,7 +91,10 @@ def _check_dtype(attr, group, values): is_iter = iterable(values) attr_name = attr.singular if is_component else attr.attrname - if isinstance(attr, _StringInternerMixin) or getattr(attr, "dtype", None) == "U1": + if ( + isinstance(attr, _StringInternerMixin) + or getattr(attr, "dtype", None) == "U1" + ): kind = "str" type_desc = "a string" plural_desc = "strings" @@ -119,13 +122,13 @@ def _check_dtype(attr, group, values): if kind == "str": valid = np.issubdtype(values.dtype, np.str_) elif kind == "int": - valid = np.issubdtype(values.dtype, np.integer) and not np.issubdtype( - values.dtype, np.bool_ - ) + valid = np.issubdtype( + values.dtype, np.integer + ) and not np.issubdtype(values.dtype, np.bool_) elif kind == "float": - valid = np.issubdtype(values.dtype, np.number) and not np.issubdtype( - values.dtype, np.bool_ - ) + valid = np.issubdtype( + values.dtype, np.number + ) and not np.issubdtype(values.dtype, np.bool_) else: # bool valid = np.issubdtype(values.dtype, np.bool_) else: @@ -147,7 +150,9 @@ def _check_dtype(attr, group, values): valid = all(isinstance(v, (bool, np.bool_)) for v in values) if not valid: - raise TypeError(f"All values for {attr_name} must be {plural_desc}") + raise TypeError( + f"All values for {attr_name} must be {plural_desc}" + ) else: if kind == "str": valid = isinstance(values, str) @@ -156,9 +161,9 @@ def _check_dtype(attr, group, values): values, (bool, np.bool_) ) elif kind == "float": - valid = isinstance(values, (int, float, np.number)) and not isinstance( - values, (bool, np.bool_) - ) + valid = isinstance( + values, (int, float, np.number) + ) and not isinstance(values, (bool, np.bool_)) else: # bool valid = isinstance(values, (bool, np.bool_)) @@ -478,7 +483,9 @@ def __init__(cls, name, bases, classdict): if dtype is not None: per_obj = classdict.get("per_object", bases[0].per_object) try: - selection.gen_selection_class(singular, attrname, dtype, per_obj) + selection.gen_selection_class( + singular, attrname, dtype, per_obj + ) except ValueError: msg = ( "A selection keyword could not be " @@ -556,7 +563,9 @@ def _gen_initial_values(n_atoms, n_residues, n_segments): raise NotImplementedError("No default values") @classmethod - def from_blank(cls, n_atoms=None, n_residues=None, n_segments=None, values=None): + def from_blank( + cls, n_atoms=None, n_residues=None, n_segments=None, values=None + ): """Create a blank version of this TopologyAttribute Parameters @@ -931,7 +940,9 @@ def _set_X(self, ag, values): newnames.append(val) newidx[i] = nextidx - self.nmidx[ag.ix] = newidx # newidx either single value or same size array + self.nmidx[ag.ix] = ( + newidx # newidx either single value or same size array + ) if newnames: self.name_lookup = np.concatenate([self.name_lookup, newnames]) self.values = self.name_lookup[self.nmidx] @@ -1054,7 +1065,8 @@ def phi_selections(residues, c_name="C", n_name="N", ca_name="CA"): keep_prev = [sum(r.atoms.names == c_name) == 1 for r in prev] keep_res = [ - all(sum(r.atoms.names == n) == 1 for n in ncac_names) for r in residues + all(sum(r.atoms.names == n) == 1 for n in ncac_names) + for r in residues ] keep = np.array(keep_prev) & np.array(keep_res) keep[invalid] = False @@ -1245,7 +1257,8 @@ def psi_selections(residues, c_name="C", n_name="N", ca_name="CA"): keep_nxt = [sum(r.atoms.names == n_name) == 1 for r in nxt] keep_res = [ - all(sum(r.atoms.names == n) == 1 for n in ncac_names) for r in residues + all(sum(r.atoms.names == n) == 1 for n in ncac_names) + for r in residues ] keep = np.array(keep_nxt) & np.array(keep_res) nxt = nxt[keep] @@ -1357,9 +1370,12 @@ def omega_selections(residues, c_name="C", n_name="N", ca_name="CA"): nxtatoms = [ca_name, n_name] resatoms = [ca_name, c_name] - keep_nxt = [all(sum(r.atoms.names == n) == 1 for n in nxtatoms) for r in nxt] + keep_nxt = [ + all(sum(r.atoms.names == n) == 1 for n in nxtatoms) for r in nxt + ] keep_res = [ - all(sum(r.atoms.names == n) == 1 for n in resatoms) for r in residues + all(sum(r.atoms.names == n) == 1 for n in resatoms) + for r in residues ] keep = np.array(keep_nxt) & np.array(keep_res) nxt = nxt[keep] @@ -1861,9 +1877,13 @@ def moment_of_inertia(group, wrap=False, unwrap=False, compound="group"): """ atomgroup = group.atoms - com = atomgroup.center_of_mass(wrap=wrap, unwrap=unwrap, compound=compound) + com = atomgroup.center_of_mass( + wrap=wrap, unwrap=unwrap, compound=compound + ) if compound != "group": - com = (com * group.masses[:, None]).sum(axis=0) / group.masses.sum() + com = (com * group.masses[:, None]).sum( + axis=0 + ) / group.masses.sum() if wrap: pos = atomgroup.pack_into_box(inplace=False) - com @@ -1994,7 +2014,9 @@ def _gyration(recenteredpos, masses): atomgroup = group.atoms masses = atomgroup.masses - com = atomgroup.center_of_mass(wrap=wrap, unwrap=unwrap, compound=compound) + com = atomgroup.center_of_mass( + wrap=wrap, unwrap=unwrap, compound=compound + ) if compound == "group": if wrap: @@ -2129,7 +2151,8 @@ def asphericity(group, wrap=False, unwrap=False, compound="group"): ) else: shape = (3.0 / 2.0) * ( - np.sum((eig_vals - np.mean(eig_vals)) ** 2) / np.sum(eig_vals) ** 2 + np.sum((eig_vals - np.mean(eig_vals)) ** 2) + / np.sum(eig_vals) ** 2 ) return shape @@ -2213,7 +2236,9 @@ def align_principal_axis(group, axis, vector): # print "axis = %r, angle = %f deg" % (ax, angle) return group.rotateby(angle, ax) - transplants[GroupBase].append(("align_principal_axis", align_principal_axis)) + transplants[GroupBase].append( + ("align_principal_axis", align_principal_axis) + ) # TODO: update docs to property doc @@ -2370,7 +2395,9 @@ def total_charge(group, compound="group"): @warn_if_not_unique @_pbc_to_wrap @check_wrap_and_unwrap - def dipole_vector(group, wrap=False, unwrap=False, compound="group", center="mass"): + def dipole_vector( + group, wrap=False, unwrap=False, compound="group", center="mass" + ): r"""Dipole vector of the group. .. math:: @@ -2436,7 +2463,9 @@ def dipole_vector(group, wrap=False, unwrap=False, compound="group", center="mas if center == "mass": masses = atomgroup.masses - ref = atomgroup.center_of_mass(wrap=wrap, unwrap=unwrap, compound=compound) + ref = atomgroup.center_of_mass( + wrap=wrap, unwrap=unwrap, compound=compound + ) elif center == "charge": ref = atomgroup.center_of_charge( wrap=wrap, unwrap=unwrap, compound=compound @@ -2462,7 +2491,9 @@ def dipole_vector(group, wrap=False, unwrap=False, compound="group", center="mas ) else: recenteredpos = atomgroup.positions - ref - dipole_vector = np.einsum("ij,ij->j", recenteredpos, charges[:, np.newaxis]) + dipole_vector = np.einsum( + "ij,ij->j", recenteredpos, charges[:, np.newaxis] + ) else: atom_masks, compound_masks, n_compounds = ( atomgroup._split_by_compound_indices(compound) @@ -2546,9 +2577,13 @@ def dipole_moment(group, **kwargs): dipole_vector = atomgroup.dipole_vector(**kwargs) if len(dipole_vector.shape) > 1: - dipole_moment = np.sqrt(np.einsum("ij,ij->i", dipole_vector, dipole_vector)) + dipole_moment = np.sqrt( + np.einsum("ij,ij->i", dipole_vector, dipole_vector) + ) else: - dipole_moment = np.sqrt(np.einsum("i,i->", dipole_vector, dipole_vector)) + dipole_moment = np.sqrt( + np.einsum("i,i->", dipole_vector, dipole_vector) + ) return dipole_moment @@ -2641,7 +2676,9 @@ def __quadrupole_tensor(recenteredpos, charges): if center == "mass": masses = atomgroup.masses - ref = atomgroup.center_of_mass(wrap=wrap, unwrap=unwrap, compound=compound) + ref = atomgroup.center_of_mass( + wrap=wrap, unwrap=unwrap, compound=compound + ) elif center == "charge": ref = atomgroup.center_of_charge( wrap=wrap, unwrap=unwrap, compound=compound @@ -2759,7 +2796,9 @@ def __quadrupole_moment(tensor): if len(quad_tensor.shape) == 2: quad_moment = __quadrupole_moment(quad_tensor) else: - quad_moment = np.array([__quadrupole_moment(x) for x in quad_tensor]) + quad_moment = np.array( + [__quadrupole_moment(x) for x in quad_tensor] + ) return quad_moment @@ -3083,7 +3122,9 @@ def sequence(self, **kwargs): ) ) try: - sequence = "".join([convert_aa_code(r) for r in self.residues.resnames]) + sequence = "".join( + [convert_aa_code(r) for r in self.residues.resnames] + ) except KeyError as err: errmsg = ( f"AtomGroup contains a residue name '{err.message}' that" @@ -3216,13 +3257,14 @@ def _check_connection_values(func): @functools.wraps(func) def wrapper(self, values, *args, **kwargs): if not all( - len(x) == self._n_atoms and all(isinstance(y, (int, np.integer)) for y in x) + len(x) == self._n_atoms + and all(isinstance(y, (int, np.integer)) for y in x) for x in values ): raise ValueError( - ("{} must be an iterable of tuples with {}" " atom indices").format( - self.attrname, self._n_atoms - ) + ( + "{} must be an iterable of tuples with {}" " atom indices" + ).format(self.attrname, self._n_atoms) ) clean = [] for v in values: @@ -3300,7 +3342,9 @@ def _bondDict(self): """Lazily built mapping of atoms:bonds""" bd = defaultdict(list) - for b, t, g, o in zip(self.values, self.types, self._guessed, self.order): + for b, t, g, o in zip( + self.values, self.types, self._guessed, self.order + ): for a in b: bd[a].append((b, t, g, o)) return bd @@ -3319,7 +3363,9 @@ def get_atoms(self, ag): """ try: - unique_bonds = set(itertools.chain(*[self._bondDict[a] for a in ag.ix])) + unique_bonds = set( + itertools.chain(*[self._bondDict[a] for a in ag.ix]) + ) except TypeError: # maybe we got passed an Atom unique_bonds = self._bondDict[ag.ix] diff --git a/testsuite/MDAnalysisTests/core/test_topologyattrs.py b/testsuite/MDAnalysisTests/core/test_topologyattrs.py index 31f235bdb3..41fe111bb2 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyattrs.py +++ b/testsuite/MDAnalysisTests/core/test_topologyattrs.py @@ -232,7 +232,9 @@ class AggregationMixin(TestAtomAttr): def test_get_residues(self, attr): assert_equal( attr.get_residues(DummyGroup([2, 1])), - np.array([self.values[[2, 3, 9]].sum(), self.values[[4, 5, 8]].sum()]), + np.array( + [self.values[[2, 3, 9]].sum(), self.values[[4, 5, 8]].sum()] + ), ) def test_get_segments(self, attr): @@ -278,7 +280,9 @@ def test_set_residue_VE(self, universe): setattr(res, self.attrclass.singular, self.values[:2]) def test_get_atoms(self, attr): - assert_equal(attr.get_atoms(DummyGroup([7, 3, 9])), self.values[[3, 2, 2]]) + assert_equal( + attr.get_atoms(DummyGroup([7, 3, 9])), self.values[[3, 2, 2]] + ) def test_get_atom(self, universe): attr = getattr(universe.atoms[0], self.attrclass.singular) @@ -368,7 +372,9 @@ def test_set_segment_VE(self): setattr(seg, "segid", [1, 2, 3]) def test_get_atoms(self, attr): - assert_equal(attr.get_atoms(DummyGroup([2, 4, 1])), self.values[[1, 1, 0]]) + assert_equal( + attr.get_atoms(DummyGroup([2, 4, 1])), self.values[[1, 1, 0]] + ) def test_get_residues(self, attr): assert_equal( @@ -381,7 +387,9 @@ def test_get_segments(self, attr): atoms in segments. """ - assert_equal(attr.get_segments(DummyGroup([1, 0, 0])), self.values[[1, 0, 0]]) + assert_equal( + attr.get_segments(DummyGroup([1, 0, 0])), self.values[[1, 0, 0]] + ) def test_set_segments_singular(self, attr): dg = DummyGroup([0, 1]) @@ -428,7 +436,9 @@ def universe_pa(self): def test_principal_axes_handedness(self, universe_pa): e_vec = universe_pa.atoms.principal_axes() - assert_almost_equal(np.dot(np.cross(e_vec[0], e_vec[1]), e_vec[2]), 1.0) + assert_almost_equal( + np.dot(np.cross(e_vec[0], e_vec[1]), e_vec[2]), 1.0 + ) def test_align_principal_axes_with_self(self, ag): pa = ag.principal_axes() @@ -709,7 +719,9 @@ def test_from_lookup(self): assert_equal(attr.values, ref.values) assert attr.values.dtype == ref.values.dtype - assert all(attr.name_lookup[i] == nm for nm, i in attr.namedict.items()) + assert all( + attr.name_lookup[i] == nm for nm, i in attr.namedict.items() + ) u = mda.Universe.empty(n_atoms=5) u.add_TopologyAttr(attr) @@ -788,7 +800,9 @@ def u(self): universe.add_TopologyAttr("segids", ["SEG1", "SEG2"]) universe.add_TopologyAttr("resids", [1, 2, 3]) universe.add_TopologyAttr("charges", [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]) - universe.add_TopologyAttr("masses", [12.0, 12.0, 14.0, 16.0, 12.0, 12.0]) + universe.add_TopologyAttr( + "masses", [12.0, 12.0, 14.0, 16.0, 12.0, 12.0] + ) universe.add_TopologyAttr( "aromaticities", [False, False, False, False, False, False] )