From b1199ed2c4829157f25bc938e1d82e5a5f41a103 Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Mon, 28 Oct 2024 15:59:12 -0400 Subject: [PATCH] Csse pyd2 warnings (#354) * further equalize return btwn v1 and v2 * molecule.extras default * avoid pubchem error * add docs, fix warnings * ok move back a few * suppress warnings --- .github/workflows/CI.yaml | 2 +- docs/changelog.rst | 3 + docs/models.rst | 154 ++++++++++++++++-- qcelemental/models/v1/molecule.py | 8 +- qcelemental/models/v2/basemodels.py | 9 +- qcelemental/models/v2/molecule.py | 10 +- qcelemental/models/v2/results.py | 5 +- qcelemental/tests/addons.py | 12 +- qcelemental/tests/test_model_results.py | 73 ++++++--- qcelemental/tests/test_molecule.py | 59 ++++--- .../tests/test_molparse_from_string.py | 9 +- qcelemental/tests/test_molutil.py | 11 +- qcelemental/util/gph_uno_bipartite.py | 6 +- qcelemental/util/serialization.py | 37 +++-- 14 files changed, 310 insertions(+), 88 deletions(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 6a258a01..f5c6ee21 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -39,7 +39,7 @@ jobs: if: matrix.python-version == '3.9' run: poetry install --no-interaction --no-ansi --extras test - name: Run tests - run: poetry run pytest -rws -v --cov=qcelemental --color=yes --cov-report=xml + run: poetry run pytest -rws -v --cov=qcelemental --color=yes --cov-report=xml #-k "not pubchem_multiout_g" - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 # NEEDS UPDATE TO v3 https://github.com/codecov/codecov-action - name: QCSchema Examples Deploy diff --git a/docs/changelog.rst b/docs/changelog.rst index 7a981fa0..f322a9fb 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -27,6 +27,7 @@ Breaking Changes ++++++++++++++++ * The very old model names `ResultInput`, `Result`, `ResultProperties`, `Optimization` deprecated in 2019 are now only available through `qcelelemental.models.v1` * ``models.v2`` do not support AutoDoc. The AutoDoc routines have been left at pydantic v1 syntax. Use autodoc-pydantic for Sphinx instead. +* Unlike Levi's pyd v2, this doesn't forward define dict, copy, json to v2 models. Instead it backwards-defines model_dump, model_dump_json, model_copy to v1. This will impede upgrading but be cleaner in the long run. See commented-out functions to temporarily restore this functionality. v2.Molecule retains its dict for now New Features ++++++++++++ @@ -35,6 +36,8 @@ New Features Enhancements ++++++++++++ +* Fix a lot of warnings originating in this project. +* `Molecule.extras` now defaults to `{}` rather than None in both v1 and v2. Input None converts to {} upon instantiation. * ``v2.FailedOperation`` field `id` is becoming `Optional[str]` instead of plain `str` so that the default validates. * v1.ProtoModel learned `model_copy`, `model_dump`, `model_dump_json` methods (all w/o warnings) so downstream can unify on newer syntax. Levi's work alternately/additionally taught v2 `copy`, `dict`, `json` (all w/warning) but dict has an alternate use in Pydantic v2. * ``AtomicInput`` and ``AtomicResult`` ``OptimizationInput``, ``OptimizationResult``, ``TorsionDriveInput``, ``TorsionDriveResult``, ``FailedOperation`` (both versions) learned a ``.convert_v(ver)`` function that returns self or the other version. diff --git a/docs/models.rst b/docs/models.rst index 64e38bb8..d989f319 100644 --- a/docs/models.rst +++ b/docs/models.rst @@ -7,7 +7,7 @@ as their base to provide serialization, validation, and manipluation. Basics --------- +------ Model creation occurs with a ``kwargs`` constructor as shown by equivalent operations below: @@ -16,11 +16,27 @@ Model creation occurs with a ``kwargs`` constructor as shown by equivalent opera >>> mol = qcel.models.Molecule(symbols=["He"], geometry=[0, 0, 0]) >>> mol = qcel.models.Molecule(**{"symbols":["He"], "geometry": [0, 0, 0]}) -A list of all available fields can be found by querying the ``fields`` attribute: +Certain models (Molecule in particular) have additional convenience instantiation functions, like +the below for hydroxide ion: .. code-block:: python - >>> mol.fields.keys() + >>> mol = qcel.models.Molecule.from_data(""" + -1 1 + O 0 0 0 + H 0 0 1.2 + """) + +A list of all available fields can be found by querying for fields: + +.. code-block:: python + + # QCSchema v1 / Pydantic v1 + >>> mol.__fields__.keys() + dict_keys(['symbols', 'geometry', ..., 'id', 'extras']) + + # QCSchema v2 / Pydantic v2 + >>> mol.model_fields.keys() dict_keys(['symbols', 'geometry', ..., 'id', 'extras']) These attributes can be accessed as shown: @@ -37,11 +53,13 @@ Note that these models are typically immutable: >>> mol.symbols = ["Ne"] TypeError: "Molecule" is immutable and does not support item assignment -To update or alter a model the ``copy`` command can be used with the ``update`` kwargs: +To update or alter a model the ``model_copy`` command can be used with the ``update`` kwargs. +Note that ``model_copy`` is Pydantic v2 syntax, but it will work on QCSchema v1 and v2 models. +The older Pydantic v1 syntax, ``copy``, will only work on QCSchema v1 models. .. code-block:: python - >>> mol.copy(update={"symbols": ["Ne"]}) + >>> mol.model_copy(update={"symbols": ["Ne"]}) < Geometry (in Angstrom), charge = 0.0, multiplicity = 1: Center X Y Z @@ -53,26 +71,30 @@ To update or alter a model the ``copy`` command can be used with the ``update`` Serialization ------------- -All models can be serialized back to their dictionary counterparts through the ``dict`` function: +All models can be serialized back to their dictionary counterparts through the ``model_dump`` function: +Note that ``model_dump`` is Pydantic v2 syntax, but it will work on QCSchema v1 and v2 models. +The older Pydantic v1 syntax, ``dict``, will only work on QCSchema v1 models. It has a different effect on v2 models. .. code-block:: python - >>> mol.dict() + >>> mol.model_dump() {'symbols': ['He'], 'geometry': array([[0., 0., 0.]])} JSON representations are supported out of the box for all models: +Note that ``model_dump_json`` is Pydantic v2 syntax, but it will work on QCSchema v1 and v2 models. +The older Pydantic v1 syntax, ``json``, will only work on QCSchema v1 models. .. code-block:: python - >>> mol.json() + >>> mol.model_dump_json() '{"symbols": ["He"], "geometry": [0.0, 0.0, 0.0]}' Raw JSON can also be parsed back into a model: .. code-block:: python - >>> mol.parse_raw(mol.json()) + >>> mol.parse_raw(mol.model_dump_json()) < Geometry (in Angstrom), charge = 0.0, multiplicity = 1: Center X Y Z @@ -82,10 +104,120 @@ Raw JSON can also be parsed back into a model: > The standard ``dict`` operation returns all internal representations which may be classes or other complex structures. -To return a JSON-like dictionary the ``dict`` function can be used: +To return a JSON-like dictionary the ``model_dump`` function can be used: .. code-block:: python - >>> mol.dict(encoding='json') + >>> mol.model_dump(encoding='json') {'symbols': ['He'], 'geometry': [0.0, 0.0, 0.0]} + +QCSchema v2 +----------- + +Starting with QCElemental v0.50.0, a new "v2" version of QCSchema is accessible. In particular: + +* QCSchema v2 is written in Pydantic v2 syntax. (Note that a model with submodels may not mix Pydantic v1 and v2 models.) +* Major QCSchema v2 models have field ``schema_version=2``. Note that Molecule has long had ``schema_version=2``, but this belongs to QCSchema v1. The QCSchema v2 Molecule has ``schema_version=3``. +* QCSchema v2 has certain field rearrangements that make procedure models more composable. They also make v1 and v2 distinguishable in dictionary form. +* QCSchema v2 does not include new features. It is purely a technical upgrade. + +Also see https://github.com/MolSSI/QCElemental/issues/323 for details and progress. The changelog contains details. + +The anticipated timeline is: + +* v0.50 — QCSchema v2 available. QCSchema v1 unchanged (files moved but imports will work w/o change). There will be beta releases. +* v0.70 — QCSchema v2 will become the default. QCSchema v1 will remain available, but it will require specific import paths (available as soon as v0.50). +* v1.0 — QCSchema v2 unchanged. QCSchema v1 dropped. Earliest 1 Jan 2026. + +Both QCSchema v1 and v2 will be available for quite awhile to allow downstream projects time to adjust. + +To make sure you're using QCSchema v1: + +.. code-block:: python + + # replace + >>> from qcelemental.models import AtomicResult, OptimizationInput + # by + >>> from qcelemental.models.v1 import AtomicResult, OptimizationInput + +To try out QCSchema v2: + +.. code-block:: python + + # replace + >>> from qcelemental.models import AtomicResult, OptimizationInput + # by + >>> from qcelemental.models.v2 import AtomicResult, OptimizationInput + +To figure out what model you're working with, you can look at its Pydantic base or its QCElemental base: + +.. code-block:: python + + # make molecules + >>> mol1 = qcel.models.v1.Molecule(symbols=["O", "H"], molecular_charge=-1, geometry=[0, 0, 0, 0, 0, 1.2]) + >>> mol2 = qcel.models.v2.Molecule(symbols=["O", "H"], molecular_charge=-1, geometry=[0, 0, 0, 0, 0, 1.2]) + >>> print(mol1, mol2) + Molecule(name='HO', formula='HO', hash='6b7a42f') Molecule(name='HO', formula='HO', hash='6b7a42f') + + # query v1 molecule + >>> isinstance(mol1, pydantic.v1.BaseModel) + True + >>> isinstance(mol1, pydantic.BaseModel) + False + >>> isinstance(mol1, qcel.models.v1.ProtoModel) + True + >>> isinstance(mol1, qcel.models.v2.ProtoModel) + False + + # query v2 molecule + >>> isinstance(mol2, pydantic.v1.BaseModel) + False + >>> isinstance(mol2, pydantic.BaseModel) + True + >>> isinstance(mol2, qcel.models.v1.ProtoModel) + False + >>> isinstance(mol2, qcel.models.v2.ProtoModel) + True + +Most high-level models (e.g., ``AtomicInput``, not ``Provenance``) have a ``convert_v`` function to convert between QCSchema versions. It returns the input object if called with the current version. + +.. code-block:: python + + >>> inp1 = qcel.models.v1.AtomicInput(driver='energy', model={'method': 'pbe', 'basis': 'pvdz'}, molecule=mol1) + >>> print(inp1) + AtomicInput(driver='energy', model={'method': 'pbe', 'basis': 'pvdz'}, molecule_hash='6b7a42f') + >>> inp1.schema_version + 1 + >>> inp2 = qcel.models.v2.AtomicInput(driver='energy', model={'method': 'pbe', 'basis': 'pvdz'}, molecule=mol2) + >>> print(inp2) + AtomicInput(driver='energy', model={'method': 'pbe', 'basis': 'pvdz'}, molecule_hash='6b7a42f') + >>> inp2.schema_version + 2 + + # now convert + >>> inp1_now2 = inp1.convert_v(2) + >>> print(inp1_now2.schema_version) + 2 + >>> inp2_now1 = inp1.convert_v(1) + >>> print(inp2_now1.schema_version) + 1 + +Error messages aren't necessarily helpful in the upgrade process. + +.. code-block:: python + + # This usually means you're calling Pydantic v1 functions (dict, json, copy) on a Pydantic v2 model. + # There are dict and copy functions commented out in qcelemental/models/v2/basemodels.py that you + # can uncomment and use temporarily to ease the upgrade, but the preferred route is to switch to + # model_dump, model_dump_json, model_copy that work on QCSchema v1 and v2 models. + >>> TypeError: ProtoModel.serialize() got an unexpected keyword argument 'by_alias' + + # This usually means you're mixing a v1 model into a v2 model. Check all the imports from + # qcelemental.models for version specificity. If the import can't be updated, run `convert_v` + # on the model. + >>> pydantic_core._pydantic_core.ValidationError: 1 validation error for AtomicInput + >>> molecule + >>> Input should be a valid dictionary or instance of Molecule [type=model_type, input_value=Molecule(name='HO', formula='HO', hash='6b7a42f'), input_type=Molecule] + >>> For further information visit https://errors.pydantic.dev/2.5/v/model_type + diff --git a/qcelemental/models/v1/molecule.py b/qcelemental/models/v1/molecule.py index e533b832..bc873adb 100644 --- a/qcelemental/models/v1/molecule.py +++ b/qcelemental/models/v1/molecule.py @@ -290,7 +290,7 @@ class Molecule(ProtoModel): "never need to be manually set.", ) extras: Dict[str, Any] = Field( # type: ignore - None, + {}, description="Additional information to bundle with the molecule. Use for schema development and scratch space.", ) @@ -350,7 +350,7 @@ def __init__(self, orient: bool = False, validate: Optional[bool] = None, **kwar kwargs = {**kwargs, **schema} # Allow any extra fields validate = True - if "extras" not in kwargs: + if "extras" not in kwargs or kwargs["extras"] is None: # latter re-defaults to empty dict kwargs["extras"] = {} super().__init__(**kwargs) @@ -552,10 +552,12 @@ def __eq__(self, other): by scientific terms, and not programing terms, so it's less rigorous than a programmatic equality or a memory equivalent `is`. """ + import qcelemental if isinstance(other, dict): other = Molecule(orient=False, **other) - elif isinstance(other, Molecule): + elif isinstance(other, (qcelemental.models.v2.Molecule, Molecule)): + # allow v2 on grounds of "scientific, not programming terms" pass else: raise TypeError("Comparison molecule not understood of type '{}'.".format(type(other))) diff --git a/qcelemental/models/v2/basemodels.py b/qcelemental/models/v2/basemodels.py index e87079d2..8e4b5d9c 100644 --- a/qcelemental/models/v2/basemodels.py +++ b/qcelemental/models/v2/basemodels.py @@ -133,9 +133,11 @@ def parse_file(cls, path: Union[str, Path], *, encoding: Optional[str] = None) - return cls.parse_raw(path.read_bytes(), encoding=encoding) - def dict(self, **kwargs) -> Dict[str, Any]: - warnings.warn("The `dict` method is deprecated; use `model_dump` instead.", DeprecationWarning) - return self.model_dump(**kwargs) + # UNCOMMENT IF NEEDED FOR UPGRADE + # defining this is maybe bad idea as dict(v2) does non-recursive dictionary, whereas model_dump does nested + # def dict(self, **kwargs) -> Dict[str, Any]: + # warnings.warn("The `dict` method is deprecated; use `model_dump` instead.", DeprecationWarning) + # return self.model_dump(**kwargs) @model_serializer(mode="wrap") def _serialize_model(self, handler) -> Dict[str, Any]: @@ -235,6 +237,7 @@ def serialize( return serialize(data, encoding=encoding) + # UNCOMMENT IF NEEDED FOR UPGRADE REDO!!! def json(self, **kwargs): # Alias JSON here from BaseModel to reflect dict changes warnings.warn("The `json` method is deprecated; use `model_dump_json` instead.", DeprecationWarning) diff --git a/qcelemental/models/v2/molecule.py b/qcelemental/models/v2/molecule.py index 9e721403..5dc3c58f 100644 --- a/qcelemental/models/v2/molecule.py +++ b/qcelemental/models/v2/molecule.py @@ -334,7 +334,7 @@ class Molecule(ProtoModel): "never need to be manually set.", ) extras: Dict[str, Any] = Field( # type: ignore - None, + {}, description="Additional information to bundle with the molecule. Use for schema development and scratch space.", ) @@ -382,7 +382,7 @@ def __init__(self, orient: bool = False, validate: Optional[bool] = None, **kwar kwargs = {**kwargs, **schema} # Allow any extra fields validate = True - if "extras" not in kwargs: + if "extras" not in kwargs or kwargs["extras"] is None: # latter re-defaults to empty dict kwargs["extras"] = {} super().__init__(**kwargs) @@ -588,19 +588,23 @@ def __eq__(self, other): by scientific terms, and not programing terms, so it's less rigorous than a programmatic equality or a memory equivalent `is`. """ + import qcelemental if isinstance(other, dict): other = Molecule(orient=False, **other) - elif isinstance(other, Molecule): + elif isinstance(other, (Molecule, qcelemental.models.v1.Molecule)): + # allow v2 on grounds of "scientific, not programming terms" pass else: raise TypeError("Comparison molecule not understood of type '{}'.".format(type(other))) return self.get_hash() == other.get_hash() + # UNCOMMENT IF NEEDED FOR UPGRADE REDO?? def dict(self, **kwargs): warnings.warn("The `dict` method is deprecated; use `model_dump` instead.", DeprecationWarning) return self.model_dump(**kwargs) + # TODO maybe bad idea as dict(v2) does non-recursive dictionary, whereas model_dump does nested @model_serializer(mode="wrap") def _serialize_molecule(self, handler) -> Dict[str, Any]: diff --git a/qcelemental/models/v2/results.py b/qcelemental/models/v2/results.py index 87c70f98..fe11258d 100644 --- a/qcelemental/models/v2/results.py +++ b/qcelemental/models/v2/results.py @@ -773,7 +773,10 @@ def _version_stamp(cls, v): @field_validator("return_result") @classmethod def _validate_return_result(cls, v, info): - if info.data["driver"] == "gradient": + if info.data["driver"] == "energy": + if isinstance(v, np.ndarray) and v.size == 1: + v = v.item(0) + elif info.data["driver"] == "gradient": v = np.asarray(v).reshape(-1, 3) elif info.data["driver"] == "hessian": v = np.asarray(v) diff --git a/qcelemental/tests/addons.py b/qcelemental/tests/addons.py index f54151fc..743e70a7 100644 --- a/qcelemental/tests/addons.py +++ b/qcelemental/tests/addons.py @@ -11,10 +11,13 @@ def internet_connection(): try: - socket.create_connection(("www.google.com", 80)) - return True + scc = socket.create_connection(("www.google.com", 80)) except OSError: + scc.close() return False + else: + scc.close() + return True using_web = pytest.mark.skipif(internet_connection() is False, reason="Could not connect to the internet") @@ -62,7 +65,8 @@ def xfail_on_pubchem_busy(): def drop_qcsk(instance, tnm: str, schema_name: str = None): - is_model = isinstance(instance, (qcelemental.models.v1.ProtoModel, qcelemental.models.v2.ProtoModel)) + # order matters for isinstance. a __fields__ warning is thrown if v1 before v2. + is_model = isinstance(instance, (qcelemental.models.v2.ProtoModel, qcelemental.models.v1.ProtoModel)) if is_model and schema_name is None: schema_name = type(instance).__name__ drop = (_data_path / schema_name / tnm).with_suffix(".json") @@ -70,7 +74,7 @@ def drop_qcsk(instance, tnm: str, schema_name: str = None): with open(drop, "w") as fp: if is_model: # fp.write(instance.json(exclude_unset=True, exclude_none=True)) # works but file is one-line - instance = json.loads(instance.json(exclude_unset=True, exclude_none=True)) + instance = json.loads(instance.model_dump_json(exclude_unset=True, exclude_none=True)) elif isinstance(instance, dict): pass else: diff --git a/qcelemental/tests/test_model_results.py b/qcelemental/tests/test_model_results.py index f02928cf..17f4e995 100644 --- a/qcelemental/tests/test_model_results.py +++ b/qcelemental/tests/test_model_results.py @@ -1,4 +1,5 @@ import copy +import json import warnings import numpy as np @@ -376,7 +377,7 @@ def test_wavefunction_protocols( assert wfn.wavefunction is None else: expected_keys = set(expected) | {"scf_" + x for x in expected} | {"basis", "restricted"} - assert wfn.wavefunction.dict().keys() == expected_keys + assert wfn.wavefunction.model_dump().keys() == expected_keys @pytest.mark.parametrize( @@ -510,10 +511,15 @@ def test_failed_operation(result_data_fixture, request, schema_versions): error={"error_type": "expected_testing_error", "error_message": "If you see this, its all good"}, ) assert isinstance(failed.error, ComputeError) - assert isinstance(failed.dict(), dict) - failed_json = failed.json() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + assert isinstance(failed.dict(), dict) + assert isinstance(failed.model_dump(), dict) + + failed_json = failed.model_dump_json() assert isinstance(failed_json, str) assert "its all good" in failed_json + assert isinstance(failed_json, str) def test_result_properties_array(request, schema_versions): @@ -530,10 +536,12 @@ def test_result_properties_array(request, schema_versions): assert obj.scf_dipole_moment.shape == (3,) assert obj.scf_quadrupole_moment.shape == (3, 3) - assert obj.dict().keys() == {"scf_one_electron_energy", "scf_dipole_moment", "scf_quadrupole_moment"} + assert obj.model_dump().keys() == {"scf_one_electron_energy", "scf_dipole_moment", "scf_quadrupole_moment"} assert np.array_equal(obj.scf_quadrupole_moment, np.array(lquad).reshape(3, 3)) # assert obj.dict()["scf_quadrupole_moment"] == lquad # when properties.dict() was forced json - assert np.array_equal(obj.dict()["scf_quadrupole_moment"], np.array(lquad).reshape(3, 3)) # now remains ndarray + assert np.array_equal( + obj.model_dump()["scf_quadrupole_moment"], np.array(lquad).reshape(3, 3) + ) # now remains ndarray def test_result_derivatives_array(request, schema_versions): @@ -549,7 +557,7 @@ def test_result_derivatives_array(request, schema_versions): assert obj.calcinfo_natom == 4 assert obj.return_gradient.shape == (4, 3) assert obj.scf_total_hessian.shape == (12, 12) - assert obj.dict().keys() == {"calcinfo_natom", "return_gradient", "scf_total_hessian"} + assert obj.model_dump().keys() == {"calcinfo_natom", "return_gradient", "scf_total_hessian"} @pytest.mark.parametrize( @@ -560,7 +568,7 @@ def test_model_dictable(result_data_fixture, optimization_data_fixture, smodel, if smodel == "molecule": model = schema_versions.Molecule - data = result_data_fixture["molecule"].dict() + data = result_data_fixture["molecule"].model_dump() sver = (2, 2) # TODO , 3) elif smodel == "atomicresultproperties": @@ -584,7 +592,7 @@ def test_model_dictable(result_data_fixture, optimization_data_fixture, smodel, sver = (1, 2) elif smodel == "basisset": - model = schema_versions.basis.BasisSet + model = schema_versions.BasisSet data = {"name": "custom", "center_data": center_data, "atom_map": ["bs_sto3g_o", "bs_sto3g_h", "bs_sto3g_h"]} sver = (1, 2) @@ -600,10 +608,18 @@ def ver_tests(qcsk_ver): instance = model(**data) ver_tests(qcsk_ver) - instance = model(**instance.dict()) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + instance = model(**instance.dict()) assert instance ver_tests(qcsk_ver) + instance2 = model(**data) + ver_tests(qcsk_ver) + instance2 = model(**instance2.model_dump()) + assert instance2 + ver_tests(qcsk_ver) + def test_result_model_deprecations(result_data_fixture, optimization_data_fixture, request): if "v1" not in request.node.name: @@ -624,19 +640,20 @@ def test_result_model_deprecations(result_data_fixture, optimization_data_fixtur @pytest.mark.parametrize( - "retres,atprop,rettyp", + "retres,atprop,rettyp,jsntyp", [ - (15, "mp2_correlation_energy", float), - (15.0, "mp2_correlation_energy", float), - ([1.0, -2.5, 3, 0, 0, 0, 0, 0, 0], "return_gradient", np.ndarray), - (np.array([1.0, -2.5, 3, 0, 0, 0, 0, 0, 0]), "return_gradient", np.ndarray), - ({"cat1": "tail", "cat2": "whiskers"}, None, str), - ({"float1": 4.4, "float2": -9.9}, None, float), - ({"list1": [-1.0, 4.4], "list2": [-9.9, 2]}, None, list), - ({"arr1": np.array([-1.0, 4.4]), "arr2": np.array([-9.9, 2])}, None, np.ndarray), + (15, "mp2_correlation_energy", float, float), + (15.0, "mp2_correlation_energy", float, float), + (np.array([15.3]), "ccsd_total_energy", float, float), + ([1.0, -2.5, 3, 0, 0, 0, 0, 0, 0], "return_gradient", np.ndarray, list), + (np.array([1.0, -2.5, 3, 0, 0, 0, 0, 0, 0]), "return_gradient", np.ndarray, list), + ({"cat1": "tail", "cat2": "whiskers"}, None, str, str), + ({"float1": 4.4, "float2": -9.9}, None, float, float), + ({"list1": [-1.0, 4.4], "list2": [-9.9, 2]}, None, list, list), + ({"arr1": np.array([-1.0, 4.4]), "arr2": np.array([-9.9, 2])}, None, np.ndarray, list), ], ) -def test_return_result_types(result_data_fixture, retres, atprop, rettyp, request, schema_versions): +def test_return_result_types(result_data_fixture, retres, atprop, rettyp, jsntyp, request, schema_versions): AtomicResult = schema_versions.AtomicResult working_res = copy.deepcopy(result_data_fixture) @@ -653,3 +670,21 @@ def test_return_result_types(result_data_fixture, retres, atprop, rettyp, reques if atprop: assert isinstance(getattr(atres.properties, atprop), rettyp) assert isinstance(atres.return_result, rettyp) + + datres = atres.model_dump() + if isinstance(retres, dict): + for v in datres["return_result"].values(): + assert isinstance(v, rettyp) + else: + if atprop: + assert isinstance(datres["properties"][atprop], rettyp) + assert isinstance(datres["return_result"], rettyp) + + jatres = json.loads(atres.model_dump_json()) + if isinstance(retres, dict): + for v in jatres["return_result"].values(): + assert isinstance(v, jsntyp) + else: + if atprop: + assert isinstance(jatres["properties"][atprop], jsntyp) + assert isinstance(jatres["return_result"], jsntyp) diff --git a/qcelemental/tests/test_molecule.py b/qcelemental/tests/test_molecule.py index 308781f6..06024218 100644 --- a/qcelemental/tests/test_molecule.py +++ b/qcelemental/tests/test_molecule.py @@ -2,7 +2,10 @@ Tests the imports and exports of the Molecule object. """ +import warnings + import numpy as np +import pydantic import pytest import qcelemental as qcel @@ -40,7 +43,7 @@ def water_dimer_minima_data(): def test_molecule_data_constructor_numpy(Molecule, water_dimer_minima_data): water_dimer_minima = Molecule.from_data(**water_dimer_minima_data) - water_psi = water_dimer_minima.copy() + water_psi = water_dimer_minima.model_copy() ele = np.array(water_psi.atomic_numbers).reshape(-1, 1) npwater = np.hstack((ele, water_psi.geometry * qcel.constants.conversion_factor("Bohr", "angstrom"))) @@ -57,13 +60,13 @@ def test_molecule_data_constructor_numpy(Molecule, water_dimer_minima_data): def test_molecule_data_constructor_dict(Molecule, water_dimer_minima_data): water_dimer_minima = Molecule.from_data(**water_dimer_minima_data) - water_psi = water_dimer_minima.copy() + water_psi = water_dimer_minima.model_copy() # Check the JSON construct/deconstruct - water_from_json = Molecule.from_data(water_psi.dict()) + water_from_json = Molecule.from_data(water_psi.model_dump()) assert water_psi == water_from_json - water_from_json = Molecule.from_data(water_psi.json(), "json") + water_from_json = Molecule.from_data(water_psi.model_dump_json(), "json") assert water_psi == water_from_json assert water_psi == Molecule.from_data(water_psi.to_string("psi4"), dtype="psi4") @@ -132,7 +135,9 @@ def test_molecule_np_constructors(Molecule): assert neon_from_psi == neon_from_np # Check the JSON construct/deconstruct - neon_from_json = Molecule.from_data(neon_from_psi.json(), dtype="json") + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + neon_from_json = Molecule.from_data(neon_from_psi.json(), dtype="json") assert neon_from_psi == neon_from_json assert neon_from_json.get_molecular_formula() == "Ne4" @@ -140,10 +145,12 @@ def test_molecule_np_constructors(Molecule): def test_molecule_compare(Molecule, water_molecule_data): water_molecule = Molecule.from_data(water_molecule_data) - water_molecule2 = water_molecule.copy() + water_molecule2 = water_molecule.model_copy() assert water_molecule2 == water_molecule - water_molecule3 = water_molecule.copy(update={"geometry": (water_molecule.geometry + np.array([0.1, 0, 0]))}) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + water_molecule3 = water_molecule.copy(update={"geometry": (water_molecule.geometry + np.array([0.1, 0, 0]))}) assert water_molecule != water_molecule3 @@ -151,7 +158,9 @@ def test_water_minima_data(Molecule, water_dimer_minima_data): water_dimer_minima = Molecule.from_data(**water_dimer_minima_data) # Give it a name - mol_dict = water_dimer_minima.dict() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + mol_dict = water_dimer_minima.dict() mol_dict["name"] = "water dimer" mol = Molecule(orient=True, **mol_dict) @@ -184,7 +193,7 @@ def test_water_minima_data(Molecule, water_dimer_minima_data): def test_water_minima_fragment(Molecule, water_dimer_minima_data): water_dimer_minima = Molecule.from_data(**water_dimer_minima_data) - mol = water_dimer_minima.copy() + mol = water_dimer_minima.model_copy() frag_0 = mol.get_fragment(0, orient=True) frag_1 = mol.get_fragment(1, orient=True) assert frag_0.get_hash() == "5f31757232a9a594c46073082534ca8a6806d367" # pragma: allowlist secret @@ -206,14 +215,14 @@ def test_water_minima_fragment(Molecule, water_dimer_minima_data): def test_pretty_print(Molecule, water_dimer_minima_data): water_dimer_minima = Molecule.from_data(**water_dimer_minima_data) - mol = water_dimer_minima.copy() + mol = water_dimer_minima.model_copy() assert isinstance(mol.pretty_print(), str) def test_to_string(Molecule, water_dimer_minima_data): water_dimer_minima = Molecule.from_data(**water_dimer_minima_data) - mol = water_dimer_minima.copy() + mol = water_dimer_minima.model_copy() assert isinstance(mol.to_string("psi4"), str) @@ -383,7 +392,9 @@ def test_water_orient(Molecule): def test_molecule_errors_extra(Molecule, water_dimer_minima_data): water_dimer_minima = Molecule.from_data(**water_dimer_minima_data) - data = water_dimer_minima.dict(exclude_unset=True) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + data = water_dimer_minima.dict(exclude_unset=True) data["whatever"] = 5 with pytest.raises(Exception): Molecule(**data, validate=False) @@ -392,7 +403,7 @@ def test_molecule_errors_extra(Molecule, water_dimer_minima_data): def test_molecule_errors_connectivity(Molecule, water_molecule_data): water_molecule = Molecule.from_data(water_molecule_data) - data = water_molecule.dict() + data = water_molecule.model_dump() data["connectivity"] = [(-1, 5, 5)] with pytest.raises(Exception): Molecule(**data) @@ -401,7 +412,7 @@ def test_molecule_errors_connectivity(Molecule, water_molecule_data): def test_molecule_errors_shape(Molecule, water_molecule_data): water_molecule = Molecule.from_data(water_molecule_data) - data = water_molecule.dict() + data = water_molecule.model_dump() data["geometry"] = list(range(8)) with pytest.raises(Exception): Molecule(**data) @@ -410,11 +421,13 @@ def test_molecule_errors_shape(Molecule, water_molecule_data): def test_molecule_json_serialization(Molecule, water_dimer_minima_data): water_dimer_minima = Molecule.from_data(**water_dimer_minima_data) - assert isinstance(water_dimer_minima.json(), str) + assert isinstance(water_dimer_minima.model_dump_json(), str) - assert isinstance(water_dimer_minima.dict(encoding="json")["geometry"], list) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + assert isinstance(water_dimer_minima.dict(encoding="json")["geometry"], list) - assert water_dimer_minima == Molecule.from_data(water_dimer_minima.json(), dtype="json") + assert water_dimer_minima == Molecule.from_data(water_dimer_minima.model_dump_json(), dtype="json") @pytest.mark.parametrize("encoding", serialize_extensions) @@ -547,10 +560,10 @@ def test_molecule_repeated_hashing(Molecule): h1 = mol.get_hash() assert mol.get_molecular_formula() == "H2O2" - mol2 = Molecule(orient=False, **mol.dict()) + mol2 = Molecule(orient=False, **mol.model_dump()) assert h1 == mol2.get_hash() - mol3 = Molecule(orient=False, **mol2.dict()) + mol3 = Molecule(orient=False, **mol2.model_dump()) assert h1 == mol3.get_hash() @@ -724,7 +737,7 @@ def test_sparse_molecule_fields(mol_string, extra_keys, Molecule): if extra_keys is not None: expected_keys |= extra_keys - diff_keys = mol.dict().keys() ^ expected_keys + diff_keys = mol.model_dump().keys() ^ expected_keys assert len(diff_keys) == 0, f"Diff Keys {diff_keys}" @@ -733,11 +746,11 @@ def test_sparse_molecule_connectivity(Molecule): A bit of a weird test, but because we set connectivity it should carry through. """ mol = Molecule(symbols=["He", "He"], geometry=[0, 0, -2, 0, 0, 2], connectivity=None) - assert "connectivity" in mol.dict() - assert mol.dict()["connectivity"] is None + assert "connectivity" in mol.model_dump() + assert mol.model_dump()["connectivity"] is None mol = Molecule(symbols=["He", "He"], geometry=[0, 0, -2, 0, 0, 2]) - assert "connectivity" not in mol.dict() + assert "connectivity" not in mol.model_dump() def test_bad_isotope_spec(Molecule): diff --git a/qcelemental/tests/test_molparse_from_string.py b/qcelemental/tests/test_molparse_from_string.py index 83c10afd..e35b88d9 100644 --- a/qcelemental/tests/test_molparse_from_string.py +++ b/qcelemental/tests/test_molparse_from_string.py @@ -1,4 +1,5 @@ import copy +import warnings import numpy as np import pytest @@ -110,7 +111,9 @@ def test_psi4_qm_1a(Molecule): assert compare_molrecs(fullans, final["qm"], tnm() + ": full") kmol = Molecule.from_data(subject) - _check_eq_molrec_minimal_model([], kmol.dict(), fullans) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + _check_eq_molrec_minimal_model([], kmol.dict(), fullans) def test_psi4_qm_1ab(): @@ -151,7 +154,7 @@ def test_psi4_qm_1c(Molecule): assert compare_molrecs(fullans, final["qm"], tnm() + ": full") kmol = Molecule.from_data(subject) - _check_eq_molrec_minimal_model([], kmol.dict(), fullans) + _check_eq_molrec_minimal_model([], kmol.model_dump(), fullans) def test_psi4_qm_1d(): @@ -347,7 +350,7 @@ def test_psi4_qm_2a(Molecule): kmol = Molecule.from_data(subject) _check_eq_molrec_minimal_model( ["fragments", "fragment_charges", "fragment_multiplicities", "mass_numbers", "masses", "atom_labels", "real"], - kmol.dict(), + kmol.model_dump(), fullans, ) diff --git a/qcelemental/tests/test_molutil.py b/qcelemental/tests/test_molutil.py index 3cd78f5b..3a4af03e 100644 --- a/qcelemental/tests/test_molutil.py +++ b/qcelemental/tests/test_molutil.py @@ -1,5 +1,6 @@ import math import pprint +import warnings import numpy as np import pydantic @@ -53,8 +54,8 @@ def test_relative_geoms_align_free(request, Molecule): do_shift=True, do_rotate=True, do_resort=False, do_plot=False, verbose=2, do_test=True ) - rmolrec = qcel.molparse.from_schema(s22_12.dict()) - cmolrec = qcel.molparse.from_schema(cmol.dict()) + rmolrec = qcel.molparse.from_schema(s22_12.model_dump()) + cmolrec = qcel.molparse.from_schema(cmol.model_dump()) assert compare_molrecs(rmolrec, cmolrec, atol=1.0e-4, relative_geoms="align") @@ -67,8 +68,10 @@ def test_relative_geoms_align_fixed(request, Molecule): do_shift=False, do_rotate=False, do_resort=False, do_plot=False, verbose=2, do_test=True ) - rmolrec = qcel.molparse.from_schema(s22_12.dict()) - cmolrec = qcel.molparse.from_schema(cmol.dict()) + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + rmolrec = qcel.molparse.from_schema(s22_12.dict()) + cmolrec = qcel.molparse.from_schema(cmol.dict()) assert compare_molrecs(rmolrec, cmolrec, atol=1.0e-4, relative_geoms="align") diff --git a/qcelemental/util/gph_uno_bipartite.py b/qcelemental/util/gph_uno_bipartite.py index 7301b291..0144f8f2 100644 --- a/qcelemental/util/gph_uno_bipartite.py +++ b/qcelemental/util/gph_uno_bipartite.py @@ -13,6 +13,8 @@ Updated Dec 2017 LAB for pep8, py3, more tests, starter_match, and simpler interface """ +import warnings + import numpy as np # commented as untested [Apr 2019] @@ -367,7 +369,9 @@ def _enumMaximumMatchingIter2(adj, matchadj, all_matches, n1, add_e=None, check_ # -------------------Find cycles------------------- if check_cycle: d = matchadj.multiply(adj) - d[n1:, :] = adj[n1:, :] - matchadj[n1:, :].multiply(adj[n1:, :]) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", sparse.SparseEfficiencyWarning) + d[n1:, :] = adj[n1:, :] - matchadj[n1:, :].multiply(adj[n1:, :]) dg = nx.from_numpy_array(d.toarray(), create_using=nx.DiGraph()) cycles = list(nx.simple_cycles(dg)) diff --git a/qcelemental/util/serialization.py b/qcelemental/util/serialization.py index 171e21b7..bfdc9335 100644 --- a/qcelemental/util/serialization.py +++ b/qcelemental/util/serialization.py @@ -46,10 +46,13 @@ def msgpackext_encode(obj: Any) -> Any: # below pydatnic_encoder is for Pydantic v1 API models # tentative whether handling both together will work beyond tests # or if separate files called by models.v1 and .v2 will be req'd - try: - return pydantic_encoder(obj) - except TypeError: + if isinstance(obj, pydantic.BaseModel): pass + else: + try: + return pydantic_encoder(obj) + except TypeError: + pass if isinstance(obj, np.ndarray): if obj.shape: @@ -135,10 +138,13 @@ def default(self, obj: Any) -> Any: try: return to_jsonable_python(obj) except ValueError: - try: - return pydantic_encoder(obj) - except TypeError: + if isinstance(obj, pydantic.BaseModel): pass + else: + try: + return pydantic_encoder(obj) + except TypeError: + pass if isinstance(obj, np.ndarray): if obj.shape: @@ -210,10 +216,14 @@ def default(self, obj: Any) -> Any: try: return to_jsonable_python(obj) except ValueError: - try: - return pydantic_encoder(obj) - except TypeError: + if isinstance(obj, pydantic.BaseModel): + # this block bypasses the else below for pyd v2 models to supress a warning pass + else: + try: + return pydantic_encoder(obj) + except TypeError: + pass # See if pydantic model can be just serialized if the above couldn't be dumped if isinstance(obj, pydantic.BaseModel): @@ -287,10 +297,13 @@ def msgpack_encode(obj: Any) -> Any: try: return to_jsonable_python(obj) except ValueError: - try: - return pydantic_encoder(obj) - except TypeError: + if isinstance(obj, pydantic.BaseModel): pass + else: + try: + return pydantic_encoder(obj) + except TypeError: + pass if isinstance(obj, np.ndarray): if obj.shape: