From c5bcfdc16a676a7fc7eec0a3905fbddb0d3dea0b Mon Sep 17 00:00:00 2001 From: Toon Verstraelen Date: Sun, 12 May 2019 23:51:15 +0200 Subject: [PATCH] Use attrs for IOData class Fixes #73 Not all pylint exclusions can be removed, essentially due to pylint bugs resulting in many false negatives, see https://stackoverflow.com/questions/47972143/using-attr-with-pylint https://github.com/PyCQA/pylint/issues/1694 The good solution is to disable all type-checking warnings of pylint and to use a proper type checker like mypy instead. Related changes: - reaction_coordinate, ipoint, npoint, istep and nstep move to extra. - many getattr and hasattr calls are replaced by nicer code. - ArrayTypeCheckDescriptor is replaced by two simple functions. - Document IOData.charge - Bug got fixed in poscar format (gvecs) --- iodata/formats/cube.py | 2 +- iodata/formats/fchk.py | 16 +- iodata/formats/molden.py | 6 +- iodata/formats/molpro.py | 13 +- iodata/formats/poscar.py | 5 +- iodata/formats/xyz.py | 2 +- iodata/iodata.py | 342 ++++++++++++++------------------ iodata/test/common.py | 6 +- iodata/test/test_chgcar.py | 2 +- iodata/test/test_cp2k.py | 1 - iodata/test/test_cube.py | 2 +- iodata/test/test_fchk.py | 22 +- iodata/test/test_gaussianlog.py | 1 - iodata/test/test_iodata.py | 40 ++-- iodata/test/test_locpot.py | 2 +- iodata/test/test_molden.py | 2 +- iodata/test/test_molekel.py | 2 +- iodata/test/test_molpro.py | 1 - iodata/test/test_orca.py | 2 +- iodata/test/test_overlap.py | 1 - iodata/test/test_poscar.py | 3 +- iodata/test/test_wfn.py | 1 - iodata/test/test_wfx.py | 1 - iodata/test/test_xyz.py | 1 - setup.py | 2 +- tools/conda.recipe/meta.yaml | 1 + 26 files changed, 216 insertions(+), 263 deletions(-) diff --git a/iodata/formats/cube.py b/iodata/formats/cube.py index a9566d170..25a00c549 100644 --- a/iodata/formats/cube.py +++ b/iodata/formats/cube.py @@ -187,6 +187,6 @@ def dump_one(f: TextIO, data: IOData): attributes. """ - title = getattr(data, 'title', 'Created with IOData') + title = data.title or 'Created with IOData' _write_cube_header(f, title, data.atcoords, data.atnums, data.cube, data.atcorenums) _write_cube_data(f, data.cube.data) diff --git a/iodata/formats/fchk.py b/iodata/formats/fchk.py index 6825291af..74215f59b 100644 --- a/iodata/formats/fchk.py +++ b/iodata/formats/fchk.py @@ -250,8 +250,8 @@ def load_many(lit: LineIterator) -> Iterator[dict]: ------ out Output dictionary containing ``title``, ``atcoords``, ``atnums``, - ``atcorenums``, ``ipoint``, ``npoint``, ``istep``, ``nstep``, - ``gradient``, ``reaction_coordinate``, and ``energy``. + ``atcorenums``, ``extra`` (with ``ipoint``, ``npoint``, ``istep``, + ``nstep``), ``gradient``, ``reaction_coordinate``, and ``energy``. Trajectories from a Gaussian optimization, relaxed scan or IRC calculation are written in groups of frames, called "points" in the Gaussian world, e.g. @@ -294,16 +294,18 @@ def load_many(lit: LineIterator) -> Iterator[dict]: 'title': fchk['title'], 'atnums': fchk["Atomic numbers"], 'atcorenums': fchk["Nuclear charges"], - 'ipoint': ipoint, - 'npoint': len(nsteps), - 'istep': istep, - 'nstep': nstep, 'energy': energy, 'atcoords': atcoords, 'atgradient': gradients, + 'extra': { + 'ipoint': ipoint, + 'npoint': len(nsteps), + 'istep': istep, + 'nstep': nstep, + }, } if prefix == "IRC point": - data['reaction_coordinate'] = recor + data['extra']['reaction_coordinate'] = recor yield data diff --git a/iodata/formats/molden.py b/iodata/formats/molden.py index 9e9340025..196b592b6 100644 --- a/iodata/formats/molden.py +++ b/iodata/formats/molden.py @@ -595,7 +595,7 @@ def dump_one(f: TextIO, data: IOData): """ # Print the header f.write('[Molden Format]\n') - if hasattr(data, 'title'): + if data.title is not None: f.write('[Title]\n') f.write(' {}\n'.format(data.title)) @@ -611,8 +611,8 @@ def dump_one(f: TextIO, data: IOData): f.write('\n') # Print the basis set - if not hasattr(data, 'obasis'): - raise IOError('A Gaussian orbital basis is required to write a molden input file.') + if data.obasis is None: + raise IOError('A Gaussian orbital basis is required to write a molden file.') obasis = data.obasis # Figure out the pure/Cartesian situation. Note that the Molden diff --git a/iodata/formats/molpro.py b/iodata/formats/molpro.py index fccbeebc2..73babf1cd 100644 --- a/iodata/formats/molpro.py +++ b/iodata/formats/molpro.py @@ -135,19 +135,18 @@ def dump_one(f: TextIO, data: IOData): """ one_mo = data.one_ints['core_mo'] - two_mo = data.two_ints['two_mo'] - nactive = one_mo.shape[0] - core_energy = getattr(data, 'core_energy', 0.0) - nelec = getattr(data, 'nelec', 0) - spinpol = getattr(data, 'spinpol', 0) # Write header + nactive = one_mo.shape[0] + nelec = data.nelec or 0 + spinpol = data.spinpol or 0 print(f' &FCI NORB={nactive:d},NELEC={nelec:d},MS2={spinpol:d},', file=f) print(f" ORBSYM= {','.join('1' for v in range(nactive))},", file=f) print(' ISYM=1', file=f) print(' &END', file=f) # Write integrals and core energy + two_mo = data.two_ints['two_mo'] for i in range(nactive): # pylint: disable=too-many-nested-blocks for j in range(i + 1): for k in range(nactive): @@ -161,5 +160,5 @@ def dump_one(f: TextIO, data: IOData): value = one_mo[i, j] if value != 0.0: print(f'{value:23.16e} {i+1:4d} {j+1:4d} {0:4d} {0:4d}', file=f) - if core_energy != 0.0: - print(f'{core_energy:23.16e} {0:4d} {0:4d} {0:4d} {0:4d}', file=f) + if data.core_energy is not None: + print(f'{data.core_energy:23.16e} {0:4d} {0:4d} {0:4d} {0:4d}', file=f) diff --git a/iodata/formats/poscar.py b/iodata/formats/poscar.py index a0e67db1e..10c230e21 100644 --- a/iodata/formats/poscar.py +++ b/iodata/formats/poscar.py @@ -72,7 +72,7 @@ def dump_one(f: TextIO, data: IOData): ``cellvecs`` attributes. It may contain ``title`` attribute. """ - print(getattr(data, 'title', 'Created with HORTON'), file=f) + print(data.title or 'Created with IOData', file=f) print(' 1.00000000000000', file=f) # Write cell vectors, each row is one vector in angstrom: @@ -90,8 +90,9 @@ def dump_one(f: TextIO, data: IOData): print('Direct', file=f) # Write the coordinates + gvecs = np.linalg.inv(data.cellvecs).T for uatnum in uatnums: indexes = (data.atnums == uatnum).nonzero()[0] for index in indexes: - row = np.dot(data.gvecs, data.atcoords[index]) + row = np.dot(gvecs, data.atcoords[index]) print(f' {row[0]: 21.16f} {row[1]: 21.16f} {row[2]: 21.16f} F F F', file=f) diff --git a/iodata/formats/xyz.py b/iodata/formats/xyz.py index 817b28554..4e3ededd0 100644 --- a/iodata/formats/xyz.py +++ b/iodata/formats/xyz.py @@ -109,7 +109,7 @@ def dump_one(f: TextIO, data: IOData): """ print(data.natom, file=f) - print(getattr(data, 'title', 'Created with IODATA module'), file=f) + print(data.title or 'Created with IOData', file=f) for i in range(data.natom): n = num2sym[data.atnums[i]] x, y, z = data.atcoords[i] / angstrom diff --git a/iodata/iodata.py b/iodata/iodata.py index f4da0cd3c..e49cc4dd3 100644 --- a/iodata/iodata.py +++ b/iodata/iodata.py @@ -19,101 +19,46 @@ """Module for handling input/output from different file formats.""" -from typing import List, Tuple, Type - +import attr import numpy as np +from .basis import MolecularBasis +from .orbitals import MolecularOrbitals +from .utils import Cube -__all__ = ['IOData'] - - -class ArrayTypeCheckDescriptor: - """A type checker for IOData attributes.""" - - def __init__(self, name: str, ndim: int = None, shape: Tuple = None, dtype: Type = None, - matching: List[str] = None, default: str = None, doc=None): - """Initialize decorator to perform type and shape checking of np.ndarray attributes. - - Parameters - ---------- - name - Name of the attribute (without leading underscores). - ndim - The number of dimensions of the array. - shape - The shape of the array. Use -1 for dimensions where the shape is - not fixed a priori. - dtype - The datatype of the array. - matching - A list of names of other attributes that must have consistent - shapes. This argument requires that the shape is specified. - All dimensions for which the shape tuple equals -1 are must be - the same in this attribute and the matching attributes. - default - The name of another (type-checked) attribute to return as default - when this attribute is not set - - """ - if matching is not None and shape is None: - raise TypeError('The matching argument requires the shape to be specified.') - - self._name = name - self._ndim = ndim - self._shape = shape - if dtype is None: - self._dtype = None - else: - self._dtype = np.dtype(dtype) - self._matching = matching - self._default = default - self.__doc__ = doc or 'A type-checked attribute' - - def __get__(self, instance, owner): - if instance is None: - return self - if self._default is not None and not hasattr(instance, '_' + self._name): - # When the attribute is not present, we assign it first with the - # default value. The return statement can then remain completely - # general. - default = (getattr(instance, '_' + self._default).astype(self._dtype)) - setattr(instance, '_' + self._name, default) - return getattr(instance, '_' + self._name) - - def __set__(self, obj, value): - # try casting to proper dtype: - value = np.array(value, dtype=self._dtype, copy=False) - # if not isinstance(value, np.ndarray): - # raise TypeError('Attribute \'%s\' of \'%s\' must be a numpy ' - # 'array.' % (self._name, type(obj))) - if self._ndim is not None and value.ndim != self._ndim: - raise TypeError(f"Attribute '{self._name}' of '{type(obj)}' must be a numpy array " - f"with {self._ndim} dimension(s).") - if self._shape is not None: - for i in range(len(self._shape)): - if self._shape[i] >= 0 and self._shape[i] != value.shape[i]: - raise TypeError(f"Attribute '{self._name}' of '{type(obj)}' must be a numpy" - f" array {self._shape[i]} elements in dimension {i}.") - if self._dtype is not None: - if not issubclass(value.dtype.type, self._dtype.type): - raise TypeError(f"Attribute '{self._name}' of '{type(obj)}' must be a numpy " - f"array with dtype '{self._dtype.type}'.") - if self._matching is not None: - for othername in self._matching: - other = getattr(obj, '_' + othername, None) - if other is not None: - for i in range(len(self._shape)): - if self._shape[i] == -1 and \ - other.shape[i] != value.shape[i]: - raise TypeError(f"shape[{i}] of attribute '{self._name}' of " - f"'{type(obj)}' in is incompatible with " - f"that of '{othername}'.") - setattr(obj, '_' + self._name, value) - def __delete__(self, obj): - delattr(obj, '_' + self._name) +__all__ = ['IOData'] +def convert_array_to(dtype): + """Return a function to convert arrays to the given type.""" + def converter(array): + if array is None: + return None + return np.array(array, copy=False, dtype=dtype) + return converter + + +def validate_shape(*shape): + """Return a function to validate the shape of an array.""" + def validator(obj, attrname, value): + if value is None: + return + myshape = tuple([obj.natom if size == 'natom' else size for size in shape]) + if len(myshape) != len(value.shape): + raise TypeError('Expect ndim {} for attribute {}, got {}'.format( + len(myshape), attrname, len(value.shape))) + for axis, size in enumerate(myshape): + if size is None: + continue + if size != value.shape[axis]: + raise TypeError( + 'Expect size {} for axis {} of attribute {}, got {}'.format( + size, axis, attrname, value.shape[axis])) + return validator + + +@attr.s(auto_attribs=True, slots=True) class IOData: """A container class for data loaded from (or to be written to) a file. @@ -122,41 +67,35 @@ class IOData: set are removed after the IOData instance is constructed. The following attributes are supported by at least one of the io formats: - Type checked array attributes (if present) - ------------------------------------------ - + Attributes + ---------- + atcharges + A dictionary where keys are names of charge definitions and values are + arrays with atomic charges (size N). atcoords A (N, 3) float array with Cartesian coordinates of the atoms. atcorenums A (N,) float array with pseudo-potential core charges. The matrix elements corresponding to ghost atoms are zero. + atffparams + A dictionary with arrays of atomic force field parameters (typically + non-bonded). Keys include 'charges', 'vdw_radii', 'sigmas', 'epsilons', + 'alphas' (atomic polarizabilities), 'c6s', 'c8s', 'c10s', 'buck_as', + 'buck_bs', 'lj_as', 'core_charges', 'valence_charges', 'valence_widths', + etc. Not all of them have to be present, depending on the use case. atfrozen A (N,) bool array with frozen atoms. (All atoms are free if this attribute is not set.) atgradient A (N, 3) float array with the first derivatives of the energy w.r.t. Cartesian atomic displacements. + athessian + A (3*N, 3*N) array containing the energy Hessian w.r.t Cartesian atomic + displacements. atmasses A (N,) float array with atomic masses - atnums A (N,) int vector with the atomic numbers. - - Attributes without type checking - -------------------------------- - - atcharges - A dictionary where keys are names of charge definitions and values are - arrays with atomic charges (size N). - atffparams - A dictionary with arrays of atomic force field parameters (typically - non-bonded). Keys include 'charges', 'vdw_radii', 'sigmas', 'epsilons', - 'alphas' (atomic polarizabilities), 'c6s', 'c8s', 'c10s', 'buck_as', - 'buck_bs', 'lj_as', 'core_charges', 'valence_charges', 'valence_widths', - etc. Not all of them have to be present, depending on the use case. - athessian - A (3*N, 3*N) array containing the energy Hessian w.r.t Cartesian atomic - displacements. basisdef A basis set definition, i.e. a dictionary whose keys are symbols (of chemical elements), atomic numbers (similar to previous, str to make @@ -173,6 +112,8 @@ class IOData: periodic boundary conditions. A single vector corresponds to a 1D cell, e.g. for a wire. Two vectors describe a 2D cell, e.g. for a membrane. Three vectors describe a 3D cell, e.g. a crystalline solid. + charge + The net charge of the system. core_energy The Hartree-Fock energy due to the core orbitals cube @@ -252,96 +193,124 @@ class IOData: """ - def __init__(self, **kwargs): - """Initialize an IOData instance. + atcharges: dict = {} + atcoords: np.ndarray = attr.ib( + default=None, converter=convert_array_to(float), + validator=validate_shape('natom', 3)) + _atcorenums: np.ndarray = attr.ib( + default=None, converter=convert_array_to(float), + validator=validate_shape('natom')) + atffparams: dict = {} + atfrozen: np.ndarray = attr.ib( + default=None, converter=convert_array_to(bool), + validator=validate_shape('natom')) + atgradient: np.ndarray = attr.ib( + default=None, converter=convert_array_to(float), + validator=validate_shape('natom', 3)) + athessian: np.ndarray = attr.ib( + default=None, converter=convert_array_to(float), + validator=validate_shape(None, None)) + atmasses: np.ndarray = attr.ib( + default=None, converter=convert_array_to(float), + validator=validate_shape('natom')) + atnums: np.ndarray = attr.ib( + default=None, converter=convert_array_to(int), + validator=validate_shape('natom')) + basisdef: str = None + bonds: np.ndarray = attr.ib( + default=None, converter=convert_array_to(int), + validator=validate_shape(None, 3)) + cellvecs: np.ndarray = attr.ib( + default=None, converter=convert_array_to(float), + validator=validate_shape(None, 3)) + _charge: float = None + core_energy: float = None + cube: Cube = None + energy: float = None + extcharges: np.ndarray = attr.ib( + default=None, converter=convert_array_to(float), + validator=validate_shape(None, 4)) + extra: dict = {} + g_rot: float = None + lot: str = None + mo: MolecularOrbitals = None + moments: dict = {} + _nelec: float = None + obasis: MolecularBasis = None + obasis_name: str = None + one_ints: dict = {} + one_rdms: dict = {} + run_type: str = None + _spinpol: float = None + title: str = None + two_ints: dict = {} + two_rdms: dict = {} + + # Public interfaces to private attributes - All keyword arguments will be turned into corresponding attributes. - """ - for key, value in kwargs.items(): - setattr(self, key, value) + @property + def atcorenums(self) -> np.ndarray: + """Return effective core charges.""" + result = self._atcorenums + if result is None and self.atnums is not None: + # Known bug in pylint. See + # https://stackoverflow.com/questions/47972143/using-attr-with-pylint + # https://github.com/PyCQA/pylint/issues/1694 + # pylint: disable=no-member + result = self.atnums.astype(float) + self._atcorenums = result + return result + + @atcorenums.setter + def atcorenums(self, atcorenums): + self._atcorenums = atcorenums - # only perform type checking on some attributes - atcoords = ArrayTypeCheckDescriptor( - 'atcoords', 2, (-1, 3), float, - ['atcorenums', 'atgradient', 'atfrozen', 'atmasses', 'atnums'], - doc="A (N, 3) float array with Cartesian coordinates of the atoms.") - atcorenums = ArrayTypeCheckDescriptor( - 'atcorenums', 1, (-1,), float, - ['atcoords', 'atgradient', 'atfrozen', 'atmasses', 'atnums'], - 'atnums', - doc="A (N,) float array with pseudo-potential core charges.") - atgradient = ArrayTypeCheckDescriptor( - 'atgradient', 2, (-1, 3), float, - ['atcoords', 'atcorenums', 'atfrozen', 'atmasses', 'atnums'], - doc="A (N, 3) float array with Cartesian atomic forces.") - atfrozen = ArrayTypeCheckDescriptor( - 'atfrozen', 1, (-1,), bool, - ['atcoords', 'atcorenums', 'atgradient', 'atmasses', 'atnums'], - doc="A (N,) boolean array flagging fixed atoms.") - atmasses = ArrayTypeCheckDescriptor( - 'atmasses', 1, (-1,), float, - ['atcoords', 'atcorenums', 'atgradient', 'atfrozen', 'atnums'], - doc="A (N,) float array with atomic masses.") - atnums = ArrayTypeCheckDescriptor( - 'atnums', 1, (-1,), int, - ['atcoords', 'atcorenums', 'atgradient', 'atfrozen', 'atmasses'], - doc="A (N,) int vector with the atomic numbers.") + @property + def charge(self) -> float: + """Return the net charge of the system.""" + if self.atcorenums is None: + return self._charge + return self.atcorenums.sum() - self.nelec + + @charge.setter + def charge(self, charge: float): + if self.atcorenums is None: + self._charge = charge + else: + self.nelec = self.atcorenums.sum() - charge @property def natom(self) -> int: """Return the number of atoms.""" - if hasattr(self, 'atcoords'): - return len(self.atcoords) - if hasattr(self, 'atcorenums'): - return len(self.atcorenums) - if hasattr(self, 'atgradient'): - return len(self.atgradient) - if hasattr(self, 'atfrozen'): - return len(self.atfrozen) - if hasattr(self, 'atmasses'): - return len(self.atmasses) - if hasattr(self, 'atnums'): - return len(self.atnums) - raise AttributeError("Cannot determine the number of atoms.") + natom = None + if self.atcoords is not None: + natom = len(self.atcoords) + elif self.atcorenums is not None: + natom = len(self.atcorenums) + elif self.atgradient is not None: + natom = len(self.atgradient) + elif self.atfrozen is not None: + natom = len(self.atfrozen) + elif self.atmasses is not None: + natom = len(self.atmasses) + elif self.atnums is not None: + natom = len(self.atnums) + return natom @property def nelec(self) -> float: """Return the number of electrons.""" - mo = getattr(self, 'mo', None) - if mo is None: + if self.mo is None: return self._nelec - return mo.nelec + return self.mo.nelec @nelec.setter def nelec(self, nelec: float): - mo = getattr(self, 'mo', None) - if mo is None: - # We need to fix the following together with all the no-member - # warnings, see https://github.com/theochem/iodata/issues/73 - # pylint: disable=attribute-defined-outside-init + if self.mo is None: self._nelec = nelec else: raise TypeError("nelec cannot be set when orbitals are present.") - @property - def charge(self) -> float: - """Return the net charge of the system.""" - atcorenums = getattr(self, 'atcorenums', None) - if atcorenums is None: - return self._charge - return atcorenums.sum() - self.nelec - - @charge.setter - def charge(self, charge: float): - atcorenums = getattr(self, 'atcorenums', None) - if atcorenums is None: - # We need to fix the following together with all the no-member - # warnings, see https://github.com/theochem/iodata/issues/73 - # pylint: disable=attribute-defined-outside-init - self._charge = charge - else: - self.nelec = atcorenums.sum() - charge - @property def spinpol(self) -> float: """Return the spin polarization. @@ -350,18 +319,13 @@ def spinpol(self) -> float: number in ]0, 2[ implies spin polarizaiton, which may not always be a valid assumption. """ - mo = getattr(self, 'mo', None) - if mo is None: + if self.mo is None: return self._spinpol - return mo.spinpol + return self.mo.spinpol @spinpol.setter def spinpol(self, spinpol: float): - mo = getattr(self, 'mo', None) - if mo is None: - # We need to fix the following together with all the no-member - # warnings, see https://github.com/theochem/iodata/issues/73 - # pylint: disable=attribute-defined-outside-init + if self.mo is None: self._spinpol = spinpol else: raise TypeError("spinpol cannot be set when orbitals are present.") diff --git a/iodata/test/common.py b/iodata/test/common.py index c1fcee7a5..4111becb8 100644 --- a/iodata/test/common.py +++ b/iodata/test/common.py @@ -85,7 +85,7 @@ def truncated_file(fn_orig, nline, nadd, tmpdir): def compare_mols(mol1, mol2): """Compare two IOData objects.""" - assert getattr(mol1, 'title', None) == getattr(mol2, 'title', None) + assert mol1.title == mol2.title assert_equal(mol1.atnums, mol2.atnums) assert_equal(mol1.atcorenums, mol2.atcorenums) assert_allclose(mol1.atcoords, mol2.atcoords) @@ -121,8 +121,8 @@ def compare_mols(mol1, mol2): ('one_rdms', ['scf', 'scf_spin', 'post_scf', 'post_scf_spin']), ] for attrname, keys in cases: - d1 = getattr(mol1, attrname, {}) - d2 = getattr(mol2, attrname, {}) + d1 = getattr(mol1, attrname) + d2 = getattr(mol2, attrname) for key in keys: if key in d1: assert key in d2 diff --git a/iodata/test/test_chgcar.py b/iodata/test/test_chgcar.py index 00a29986b..8ff38e8be 100644 --- a/iodata/test/test_chgcar.py +++ b/iodata/test/test_chgcar.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member +# pylint: disable=unsubscriptable-object """Test iodata.formats.chgcar module.""" import numpy as np diff --git a/iodata/test/test_cp2k.py b/iodata/test/test_cp2k.py index f11c850e2..141566182 100644 --- a/iodata/test/test_cp2k.py +++ b/iodata/test/test_cp2k.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member """Test iodata.formats.cp2k module.""" import pytest diff --git a/iodata/test/test_cube.py b/iodata/test/test_cube.py index 4cb7be678..32ff716b5 100644 --- a/iodata/test/test_cube.py +++ b/iodata/test/test_cube.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member +# pylint: disable=unsubscriptable-object """Test iodata.formats.cube module.""" diff --git a/iodata/test/test_fchk.py b/iodata/test/test_fchk.py index 3201d65af..6ce6f9abb 100644 --- a/iodata/test/test_fchk.py +++ b/iodata/test/test_fchk.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member +# pylint: disable=unsubscriptable-object,no-member """Test iodata.formats.fchk module.""" @@ -334,18 +334,18 @@ def check_trj_basics(trj, nsteps, title, irc): for ipoint, nstep in enumerate(nsteps): for istep in range(nstep): mol = trj.pop(0) - assert mol.ipoint == ipoint - assert mol.npoint == len(nsteps) - assert mol.istep == istep - assert mol.nstep == nstep + assert mol.extra['ipoint'] == ipoint + assert mol.extra['npoint'] == len(nsteps) + assert mol.extra['istep'] == istep + assert mol.extra['nstep'] == nstep assert mol.natom == natom assert mol.atnums.shape == (natom, ) assert mol.atcorenums.shape == (natom, ) assert mol.atcoords.shape == (natom, 3) assert mol.atgradient.shape == (natom, 3) assert mol.title == title - assert hasattr(mol, 'energy') - assert hasattr(mol, 'reaction_coordinate') ^ (not irc) + assert mol.energy is not None + assert ('reaction_coordinate' in mol.extra) ^ (not irc) def test_peroxide_opt(): @@ -402,10 +402,10 @@ def test_peroxide_irc(): assert_allclose(trj[0].energy, -1.48750432E+02) assert_allclose(trj[5].energy, -1.48752713E+02) assert_allclose(trj[-1].energy, -1.48757803E+02) - assert trj[0].reaction_coordinate == 0.0 - assert_allclose(trj[1].reaction_coordinate, 1.05689581E-01) - assert_allclose(trj[10].reaction_coordinate, 1.05686037E+00) - assert_allclose(trj[-1].reaction_coordinate, -1.05685760E+00) + assert trj[0].extra['reaction_coordinate'] == 0.0 + assert_allclose(trj[1].extra['reaction_coordinate'], 1.05689581E-01) + assert_allclose(trj[10].extra['reaction_coordinate'], 1.05686037E+00) + assert_allclose(trj[-1].extra['reaction_coordinate'], -1.05685760E+00) assert_allclose(trj[0].atcoords[2], [-1.94749866E+00, -5.22905491E-01, -1.47814774E+00]) assert_allclose(trj[10].atcoords[1], diff --git a/iodata/test/test_gaussianlog.py b/iodata/test/test_gaussianlog.py index 7fabbded8..4c0850e81 100644 --- a/iodata/test/test_gaussianlog.py +++ b/iodata/test/test_gaussianlog.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member """Test iodata.formats.log module.""" from numpy.testing import assert_equal, assert_allclose diff --git a/iodata/test/test_iodata.py b/iodata/test/test_iodata.py index 811d93845..4d48a7d13 100644 --- a/iodata/test/test_iodata.py +++ b/iodata/test/test_iodata.py @@ -16,12 +16,12 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member +# pylint: disable=unsubscriptable-object,no-member """Test iodata.iodata module.""" import numpy as np -from numpy.testing import assert_raises, assert_allclose +from numpy.testing import assert_allclose import pytest from .common import compute_1rdm @@ -36,30 +36,30 @@ def test_typecheck(): m = IOData(atcoords=np.array([[1, 2, 3], [2, 3, 1]])) assert np.issubdtype(m.atcoords.dtype, np.floating) - assert not hasattr(m, 'atnums') + assert m.atnums is None m = IOData(atnums=np.array([2.0, 3.0]), atcorenums=np.array([1, 1]), atcoords=np.array([[1, 2, 3], [2, 3, 1]])) assert np.issubdtype(m.atnums.dtype, np.integer) assert np.issubdtype(m.atcorenums.dtype, np.floating) - assert hasattr(m, 'atnums') - del m.atnums - assert not hasattr(m, 'atnums') + assert m.atnums is not None + m.atnums = None + assert m.atnums is None def test_typecheck_raises(): # check attribute type - assert_raises(TypeError, IOData, atcoords=np.array([[1, 2], [2, 3]])) - assert_raises(TypeError, IOData, atnums=np.array([[1, 2], [2, 3]])) + pytest.raises(TypeError, IOData, atcoords=np.array([[1, 2], [2, 3]])) + pytest.raises(TypeError, IOData, atnums=np.array([[1, 2], [2, 3]])) # check inconsistency between various attributes atnums, atcorenums, atcoords = np.array( [2, 3]), np.array([1]), np.array([[1, 2, 3]]) - assert_raises(TypeError, IOData, atnums=atnums, + pytest.raises(TypeError, IOData, atnums=atnums, atcorenums=atcorenums) - assert_raises(TypeError, IOData, atnums=atnums, atcoords=atcoords) + pytest.raises(TypeError, IOData, atnums=atnums, atcoords=atcoords) def test_unknown_format(): - assert_raises(ValueError, load_one, 'foo.unknown_file_extension') + pytest.raises(ValueError, load_one, 'foo.unknown_file_extension') def test_dm_water_sto3g_hf(): @@ -129,21 +129,15 @@ def test_undefined(): # One a blank IOData object, accessing undefined charge and nelec should raise # an AttributeError. mol = IOData() - with pytest.raises(AttributeError): - _ = mol.charge - with pytest.raises(AttributeError): - _ = mol.nelec - with pytest.raises(AttributeError): - _ = mol.spinpol - with pytest.raises(AttributeError): - _ = mol.natom + assert mol.charge is None + assert mol.nelec is None + assert mol.spinpol is None + assert mol.natom is None mol.nelec = 5 - with pytest.raises(AttributeError): - _ = mol.charge + assert mol.charge is None mol = IOData() mol.charge = 1 - with pytest.raises(AttributeError): - _ = mol.nelec + assert mol.nelec is None def test_natom(): diff --git a/iodata/test/test_locpot.py b/iodata/test/test_locpot.py index dc7781898..7a4de1530 100644 --- a/iodata/test/test_locpot.py +++ b/iodata/test/test_locpot.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member +# pylint: disable=unsubscriptable-object """Test iodata.formats.locpot module.""" from numpy.testing import assert_equal, assert_allclose diff --git a/iodata/test/test_molden.py b/iodata/test/test_molden.py index adc6eccaa..064f07cb7 100644 --- a/iodata/test/test_molden.py +++ b/iodata/test/test_molden.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member +# pylint: disable=unsubscriptable-object """Test iodata.formats.molden module.""" import os diff --git a/iodata/test/test_molekel.py b/iodata/test/test_molekel.py index f9e22cfe1..43340226e 100644 --- a/iodata/test/test_molekel.py +++ b/iodata/test/test_molekel.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member +# pylint: disable=unsubscriptable-object,no-member """Test iodata.formats.molekel module.""" from numpy.testing import assert_equal, assert_allclose diff --git a/iodata/test/test_molpro.py b/iodata/test/test_molpro.py index c8a658c92..c442abb1b 100644 --- a/iodata/test/test_molpro.py +++ b/iodata/test/test_molpro.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member """Test iodata.formats.molpro module.""" import os diff --git a/iodata/test/test_orca.py b/iodata/test/test_orca.py index 38c48992e..a226dcceb 100644 --- a/iodata/test/test_orca.py +++ b/iodata/test/test_orca.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member +# pylint: disable=unsubscriptable-object """Test iodata.formats.orca module.""" import numpy as np diff --git a/iodata/test/test_overlap.py b/iodata/test/test_overlap.py index 5b32046d6..f18298869 100644 --- a/iodata/test/test_overlap.py +++ b/iodata/test/test_overlap.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member """Test iodata.overlap & iodata.overlap_accel modules.""" import numpy as np diff --git a/iodata/test/test_poscar.py b/iodata/test/test_poscar.py index 43687c217..0c79496ab 100644 --- a/iodata/test/test_poscar.py +++ b/iodata/test/test_poscar.py @@ -16,7 +16,7 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member +# pylint: disable=unsubscriptable-object """Test iodata.formats.poscar module.""" import os @@ -70,7 +70,6 @@ def test_load_dump_consistency(tmpdir): mol0.cellvecs = np.array([[2.05278155, 0.23284023, 1.59024118], [4.96430141, 4.73044423, 4.67590975], [3.48374425, 0.67931228, 0.66281160]]) - mol0.gvecs = np.linalg.inv(mol0.cellvecs).T fn_tmp = os.path.join(tmpdir, 'POSCAR') dump_one(mol0, fn_tmp) diff --git a/iodata/test/test_wfn.py b/iodata/test/test_wfn.py index d6d817c4f..074a20f2c 100644 --- a/iodata/test/test_wfn.py +++ b/iodata/test/test_wfn.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member """Test iodata.formats.wfn module.""" diff --git a/iodata/test/test_wfx.py b/iodata/test/test_wfx.py index d29bdc215..4749b3227 100644 --- a/iodata/test/test_wfx.py +++ b/iodata/test/test_wfx.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member """Test iodata.formats.wfn module.""" import pytest diff --git a/iodata/test/test_xyz.py b/iodata/test/test_xyz.py index 478316831..f5cc3687f 100644 --- a/iodata/test/test_xyz.py +++ b/iodata/test/test_xyz.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program; if not, see # -- -# pylint: disable=no-member """Test iodata.formats.xyz module.""" import os diff --git a/setup.py b/setup.py index cf011c320..3b8b43537 100755 --- a/setup.py +++ b/setup.py @@ -77,6 +77,6 @@ def get_readme(): 'Intended Audience :: Science/Research', ], setup_requires=['numpy>=1.0', 'cython>=0.24.1'], - install_requires=['numpy>=1.0', 'cython>=0.24.1', 'scipy', + install_requires=['numpy>=1.0', 'cython>=0.24.1', 'scipy', 'attrs>=19.1.0', 'importlib_resources; python_version < "3.7"'], ) diff --git a/tools/conda.recipe/meta.yaml b/tools/conda.recipe/meta.yaml index e3534120a..60b54b04f 100644 --- a/tools/conda.recipe/meta.yaml +++ b/tools/conda.recipe/meta.yaml @@ -26,6 +26,7 @@ requirements: run: - python - scipy + - attrs >=19.1.0 - importlib_resources # [py<37] test: