From cf87140a7b6c163093d7750d7fcebf483abc0c5f Mon Sep 17 00:00:00 2001 From: "Lori A. Burns" Date: Fri, 6 Sep 2024 14:13:11 -0400 Subject: [PATCH] fix import, tests, etc --- .github/workflows/CI.yaml | 19 ++---- docs/api.rst | 3 +- docs/changelog.rst | 5 ++ docs/conf.py | 13 ++-- docs/model_common.rst | 16 ++--- docs/model_molecule.rst | 2 +- docs/model_result.rst | 10 +-- pyproject.toml | 39 ++++++----- qcelemental/info/cpu_info.py | 5 +- qcelemental/info/dft_info.py | 5 +- qcelemental/models/__init__.py | 9 +++ qcelemental/models/v1/__init__.py | 8 --- qcelemental/models/v1/align.py | 6 +- qcelemental/models/v1/basemodels.py | 9 +-- qcelemental/models/v1/basis.py | 5 +- qcelemental/models/v1/common_models.py | 11 +-- qcelemental/models/v1/molecule.py | 11 +-- qcelemental/models/v1/procedures.py | 10 +-- qcelemental/models/v1/results.py | 11 +-- qcelemental/models/v2/__init__.py | 19 ++++++ qcelemental/models/v2/basemodels.py | 10 +-- qcelemental/models/v2/basis.py | 2 +- qcelemental/models/v2/molecule.py | 6 ++ qcelemental/models/v2/procedures.py | 25 ++----- qcelemental/models/v2/results.py | 89 ++++++------------------- qcelemental/tests/addons.py | 9 +-- qcelemental/tests/test_model_results.py | 52 +++++++++++---- qcelemental/tests/test_molutil.py | 11 +-- qcelemental/tests/test_utils.py | 8 +-- qcelemental/util/autodocs.py | 9 +-- 30 files changed, 184 insertions(+), 253 deletions(-) create mode 100644 qcelemental/models/v2/__init__.py diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index 55f641f4..7c799e43 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -17,8 +17,8 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.9", "3.11", "3.12"] - pydantic-version: ["1", "2"] + python-version: ["3.12"] + pydantic-version: ["2"] runs-on: [ubuntu-latest, windows-latest] exclude: - runs-on: windows-latest @@ -35,15 +35,6 @@ jobs: uses: actions/checkout@v3 - name: Install poetry run: pip install poetry - # Force pydantic 1.0 by modifying poetry dep "pydantic" string with in-place sed - # -i is zero-length extension which does effectively in-place sub. - # Can't do -i '' because Ubuntu sed is -i{suffix} whereas OSX sed is -i {suffix}... ugh - # ^ start of line, pydantic, optional spaces and > sign, capture the version, replace with ^{version} - # Should avoid also replacing the autodoc-pydantic spec later on. - - name: Sed replace pydantic on repo - if: matrix.pydantic-version == '1' - run: | - sed -i 's/^pydantic *= *">*= *\([0-9.]*\)"/pydantic = "^\1"/' pyproject.toml - name: Install repo with poetry (full deps) if: matrix.python-version != '3.9' run: poetry install --no-interaction --no-ansi --all-extras @@ -74,12 +65,10 @@ jobs: name: Set up Python with: python-version: "3.10" + - name: Setup Graphviz + uses: ts-graphviz/setup-graphviz@v2 - name: Install poetry run: pip install poetry - # Force pydantic 1.0 by modifying poetry dep "pydantic" string with in-place sed (see above for details) - - name: Sed replace pydantic on repo - run: | - sed -i 's/^pydantic *= *">*= *\([0-9.]*\)"/pydantic = "^\1"/' pyproject.toml - name: Install repo run: poetry install --no-interaction --no-ansi - name: Build Documentation diff --git a/docs/api.rst b/docs/api.rst index 09bd0bd2..27e23c73 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -12,7 +12,6 @@ QCElemental API .. automodapi:: qcelemental.testing :skip:tnm -.. automodapi:: qcelemental.models - :skip:Optimization +.. automodapi:: qcelemental.models.v2 :skip:qcschema_models diff --git a/docs/changelog.rst b/docs/changelog.rst index 3da28c88..1148f544 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -26,13 +26,18 @@ Changelog 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. New Features ++++++++++++ * Downstream code should ``from qcelemental.models.v1 import Molecule, AtomicResult`` etc. to assure medium-term availability of existing models. +* New pydantic v2 models available as ``from qcelemental.models.v2 import Molecule, AtomicResult`` etc. Enhancements ++++++++++++ +* The ``models.v2`` have had their `schema_version` bumped for ``BasisSet``, ``AtomicInput``, ``OptimizationInput`` (implicit for ``AtomicResult`` and ``OptimizationResult``), ``TorsionDriveInput`` , and ``TorsionDriveResult``. +* The ``models.v2`` ``AtomicResultProperties`` has been given a ``schema_name`` and ``schema_version`` (2) for the first time. +* Note that ``models.v2`` ``QCInputSpecification`` and ``OptimizationSpecification`` have *not* had schema_version bumped. Bug Fixes +++++++++ diff --git a/docs/conf.py b/docs/conf.py index d420f73a..9633b9a3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -88,7 +88,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -115,7 +115,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] +html_static_path = [] # "_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -187,8 +187,8 @@ # -- Extension configuration ------------------------------------------------- extlinks = { - "issue": ("https://github.com/MolSSI/QCElemental/issues/%s", "GH#"), - "pr": ("https://github.com/MolSSI/QCElemental/pull/%s", "GH#"), + "issue": ("https://github.com/MolSSI/QCElemental/issues/%s", "GH#%s"), + "pr": ("https://github.com/MolSSI/QCElemental/pull/%s", "GH#%s"), } @@ -200,8 +200,9 @@ "numpy": ("https://numpy.org/doc/stable/", None), "scipy": ("https://docs.scipy.org/doc/scipy/", None), "matplotlib": ("https://matplotlib.org/stable/", None), - "qcengine": ("http://docs.qcarchive.molssi.org/projects/QCEngine/en/latest/", None), - "qcfractal": ("http://docs.qcarchive.molssi.org/projects/QCFractal/en/latest/", None), + "qcengine": ("https://molssi.github.io/QCEngine/", None), + "qcfractal": ("https://molssi.github.io/QCFractal/", None), + "nglview": ("https://nglviewer.org/nglview/release/v2.7.7", None), } # -- Options for todo extension ---------------------------------------------- diff --git a/docs/model_common.rst b/docs/model_common.rst index d02946ca..6de905e9 100644 --- a/docs/model_common.rst +++ b/docs/model_common.rst @@ -6,40 +6,40 @@ Common Models used throughout the QCArchive ecosystem. BasisSet -------- -.. autopydantic_model:: qcelemental.models.BasisSet +.. autopydantic_model:: qcelemental.models.v2.BasisSet :noindex: -.. autopydantic_model:: qcelemental.models.basis.BasisCenter +.. autopydantic_model:: qcelemental.models.v2.basis.BasisCenter :noindex: -.. autopydantic_model:: qcelemental.models.basis.ElectronShell +.. autopydantic_model:: qcelemental.models.v2.basis.ElectronShell :noindex: -.. autopydantic_model:: qcelemental.models.basis.ECPPotential +.. autopydantic_model:: qcelemental.models.v2.basis.ECPPotential :noindex: ComputeError ------------ -.. autopydantic_model:: qcelemental.models.ComputeError +.. autopydantic_model:: qcelemental.models.v2.ComputeError :noindex: FailedOperation --------------- -.. autopydantic_model:: qcelemental.models.FailedOperation +.. autopydantic_model:: qcelemental.models.v2.FailedOperation :noindex: Provenance ---------- -.. autopydantic_model:: qcelemental.models.Provenance +.. autopydantic_model:: qcelemental.models.v2.Provenance :noindex: DriverEnum ---------- -.. autoclass:: qcelemental.models.DriverEnum +.. autoclass:: qcelemental.models.v2.DriverEnum :noindex: :members: :undoc-members: diff --git a/docs/model_molecule.rst b/docs/model_molecule.rst index 0b76e76a..06125184 100644 --- a/docs/model_molecule.rst +++ b/docs/model_molecule.rst @@ -127,6 +127,6 @@ Obtaining fragments with ghost atoms is also supported: API --- -.. autopydantic_model:: qcelemental.models.Molecule +.. autopydantic_model:: qcelemental.models.v2.Molecule :noindex: diff --git a/docs/model_result.rst b/docs/model_result.rst index ea2a8284..58c81373 100644 --- a/docs/model_result.rst +++ b/docs/model_result.rst @@ -8,25 +8,25 @@ A Python implementation of the `MolSSI QCSchema AtomicInput ----------- -.. autopydantic_model:: qcelemental.models.AtomicInput +.. autopydantic_model:: qcelemental.models.v2.AtomicInput :noindex: AtomicResult ------------ -.. autopydantic_model:: qcelemental.models.AtomicResult +.. autopydantic_model:: qcelemental.models.v2.AtomicResult :noindex: API --- -.. autopydantic_model:: qcelemental.models.results.AtomicResultProtocols +.. autopydantic_model:: qcelemental.models.v2.results.AtomicResultProtocols :noindex: -.. autopydantic_model:: qcelemental.models.results.AtomicResultProperties +.. autopydantic_model:: qcelemental.models.v2.results.AtomicResultProperties :noindex: -.. autopydantic_model:: qcelemental.models.results.WavefunctionProperties +.. autopydantic_model:: qcelemental.models.v2.results.WavefunctionProperties :noindex: diff --git a/pyproject.toml b/pyproject.toml index 5504e066..eeffd273 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Framework :: Pydantic", + "Framework :: Pydantic :: 2", ] [tool.poetry.dependencies] @@ -36,13 +38,13 @@ packaging = [ { version = ">=24.1", python = ">=3.8" }, ] # qcel is compatible with most any numpy, v1 or v2, but numpy v2 only works with pint >=0.24, which is only available for py >=3.10 -python = "^3.7" +python = "^3.7.1" pint = [ { version = ">=0.10", python = ">=3.7,<3.9" }, { version = ">=0.23", python = ">=3.9,<3.10" }, { version = ">=0.24", python = ">=3.10,<3.13" }, ] -pydantic = ">=1.8.2" +pydantic = ">=2.0" nglview = { version = "^3.0.3", optional = true } ipykernel = { version = "<6.0.0", optional = true } importlib-metadata = { version = ">=4.8", python = "<3.8" } @@ -59,23 +61,24 @@ align = ["networkx", "scipy"] test = ["pytest"] [tool.poetry.group.dev.dependencies] -black = ">=22.1.0,<23.0a0" -mypy = "^1.1.1" -isort = "5.11.5" -flake8 = "<6.0.0" -pre-commit = "<3.2.0" -pytest-cov = "^4.0.0" -autoflake = "^2.0.2" -jsonschema = "^4.17.3" -msgpack = "^1.0.5" -numpydoc = "^1.5.0" -docutils = "<0.19" -sphinx = "<6.0.0" +black = ">=22.1.0,<23.0a0" # unk +mypy = "^1.1.1" # unk +isort = "5.11.5" # unk +flake8 = { version = "6.0.0", python = ">=3.8.1,<4.0.0" } +pre-commit = { version = "^3.8.0", python = ">=3.9,<4.0.0" } +pytest-cov = "^4.0.0" # unk +autoflake = "^2.0.2" # unk +jsonschema = { version = "^4.23.0", python = ">=3.8,<4.0.0" } +msgpack = { version = "^1.0.8", python = ">=3.8,<4.0.0" } +numpydoc = { version = "^1.8.0", python = ">=3.9,<4.0.0" } +docutils = "0.20.1" +sphinx = { version = "^7.0.0", python = ">=3.9,<4.0.0" } sphinxcontrib-napoleon = "^0.7" -sphinx-rtd-theme = "^1.2.0" -autodoc-pydantic = "^1.8.0" -sphinx-automodapi = "^0.15.0" -sphinx-autodoc-typehints = "^1.22" +sphinx-rtd-theme = "^2.0.0" +autodoc-pydantic = { version = "^2.1.0", python = ">=3.8,<4.0" } +sphinx-automodapi = { version = "^0.17.0", python = ">=3.8,<4.0.0" } +sphinx-autodoc-typehints = { version = "^2.3", python = ">=3.10,<4.0.0" } +graphviz = "^0.20.0" # insufficient on pypi as also need `dot`. python-graphviz sufficient in conda. [tool.black] line-length = 120 diff --git a/qcelemental/info/cpu_info.py b/qcelemental/info/cpu_info.py index 55f109de..4fe35689 100644 --- a/qcelemental/info/cpu_info.py +++ b/qcelemental/info/cpu_info.py @@ -8,10 +8,7 @@ from functools import lru_cache from typing import List, Optional -try: - from pydantic.v1 import Field -except ImportError: # Will also trap ModuleNotFoundError - from pydantic import Field +from pydantic.v1 import Field from ..models import ProtoModel diff --git a/qcelemental/info/dft_info.py b/qcelemental/info/dft_info.py index ce82c763..073e40d3 100644 --- a/qcelemental/info/dft_info.py +++ b/qcelemental/info/dft_info.py @@ -4,10 +4,7 @@ from typing import Dict -try: - from pydantic.v1 import Field -except ImportError: # Will also trap ModuleNotFoundError - from pydantic import Field +from pydantic.v1 import Field from ..models import ProtoModel diff --git a/qcelemental/models/__init__.py b/qcelemental/models/__init__.py index e7e2ac4b..829a3482 100644 --- a/qcelemental/models/__init__.py +++ b/qcelemental/models/__init__.py @@ -1 +1,10 @@ +try: + import pydantic +except ImportError: # pragma: no cover + raise ImportError( + "Python module pydantic not found. Solve by installing it: " + "`conda install pydantic -c conda-forge` or `pip install pydantic`" + ) + +from . import v1, v2 from .v1 import * diff --git a/qcelemental/models/v1/__init__.py b/qcelemental/models/v1/__init__.py index c17f2cdc..e61f458b 100644 --- a/qcelemental/models/v1/__init__.py +++ b/qcelemental/models/v1/__init__.py @@ -1,11 +1,3 @@ -try: - import pydantic -except ImportError: # pragma: no cover - raise ImportError( - "Python module pydantic not found. Solve by installing it: " - "`conda install pydantic -c conda-forge` or `pip install pydantic`" - ) - from . import types from .align import AlignmentMill from .basemodels import AutodocBaseSettings # remove when QCFractal merges `next` diff --git a/qcelemental/models/v1/align.py b/qcelemental/models/v1/align.py index ca09504f..2a6c0a23 100644 --- a/qcelemental/models/v1/align.py +++ b/qcelemental/models/v1/align.py @@ -1,11 +1,7 @@ from typing import Optional import numpy as np - -try: - from pydantic.v1 import Field, validator -except ImportError: # Will also trap ModuleNotFoundError - from pydantic import Field, validator +from pydantic.v1 import Field, validator from ...util import blockwise_contract, blockwise_expand from .basemodels import ProtoModel diff --git a/qcelemental/models/v1/basemodels.py b/qcelemental/models/v1/basemodels.py index 2fecef26..229b1588 100644 --- a/qcelemental/models/v1/basemodels.py +++ b/qcelemental/models/v1/basemodels.py @@ -3,13 +3,8 @@ from typing import Any, Dict, Optional, Set, Union import numpy as np - -try: - from pydantic.v1 import BaseSettings # remove when QCFractal merges `next` - from pydantic.v1 import BaseModel -except ImportError: # Will also trap ModuleNotFoundError - from pydantic import BaseSettings # remove when QCFractal merges `next` - from pydantic import BaseModel +from pydantic.v1 import BaseSettings # remove when QCFractal merges `next` +from pydantic.v1 import BaseModel from qcelemental.util import deserialize, serialize from qcelemental.util.autodocs import AutoPydanticDocGenerator # remove when QCFractal merges `next` diff --git a/qcelemental/models/v1/basis.py b/qcelemental/models/v1/basis.py index 2a4b2c88..c7d1c4b8 100644 --- a/qcelemental/models/v1/basis.py +++ b/qcelemental/models/v1/basis.py @@ -1,10 +1,7 @@ from enum import Enum from typing import Dict, List, Optional -try: - from pydantic.v1 import ConstrainedInt, Field, constr, validator -except ImportError: # Will also trap ModuleNotFoundError - from pydantic import ConstrainedInt, Field, constr, validator +from pydantic.v1 import ConstrainedInt, Field, constr, validator from ...exceptions import ValidationError from .basemodels import ProtoModel, qcschema_draft diff --git a/qcelemental/models/v1/common_models.py b/qcelemental/models/v1/common_models.py index f848449d..7f822798 100644 --- a/qcelemental/models/v1/common_models.py +++ b/qcelemental/models/v1/common_models.py @@ -2,20 +2,13 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Union import numpy as np - -try: - from pydantic.v1 import Field -except ImportError: # Will also trap ModuleNotFoundError - from pydantic import Field +from pydantic.v1 import Field from .basemodels import ProtoModel, qcschema_draft from .basis import BasisSet if TYPE_CHECKING: - try: - from pydantic.v1.typing import ReprArgs - except ImportError: # Will also trap ModuleNotFoundError - from pydantic.typing import ReprArgs + from pydantic.v1.typing import ReprArgs # Encoders, to be deprecated diff --git a/qcelemental/models/v1/molecule.py b/qcelemental/models/v1/molecule.py index d2261f63..e533b832 100644 --- a/qcelemental/models/v1/molecule.py +++ b/qcelemental/models/v1/molecule.py @@ -10,11 +10,7 @@ from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple, Union, cast import numpy as np - -try: - from pydantic.v1 import ConstrainedFloat, ConstrainedInt, Field, constr, validator -except ImportError: # Will also trap ModuleNotFoundError - from pydantic import ConstrainedFloat, ConstrainedInt, Field, constr, validator +from pydantic.v1 import ConstrainedFloat, ConstrainedInt, Field, constr, validator # molparse imports separated b/c https://github.com/python/mypy/issues/7203 from ...molparse.from_arrays import from_arrays @@ -31,10 +27,7 @@ from .types import Array if TYPE_CHECKING: - try: - from pydantic.v1.typing import ReprArgs - except ImportError: # Will also trap ModuleNotFoundError - from pydantic.typing import ReprArgs + from pydantic.v1.typing import ReprArgs # Rounding quantities for hashing GEOMETRY_NOISE = 8 diff --git a/qcelemental/models/v1/procedures.py b/qcelemental/models/v1/procedures.py index 90f3c7cf..5a0ce95b 100644 --- a/qcelemental/models/v1/procedures.py +++ b/qcelemental/models/v1/procedures.py @@ -1,10 +1,7 @@ from enum import Enum from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple -try: - from pydantic.v1 import Field, conlist, constr, validator -except ImportError: # Will also trap ModuleNotFoundError - from pydantic import Field, conlist, constr, validator +from pydantic.v1 import Field, conlist, constr, validator from ...util import provenance_stamp from .basemodels import ProtoModel @@ -23,10 +20,7 @@ from .results import AtomicResult if TYPE_CHECKING: - try: - from pydantic.v1.typing import ReprArgs - except ImportError: # Will also trap ModuleNotFoundError - from pydantic.typing import ReprArgs + from pydantic.v1.typing import ReprArgs class TrajectoryProtocolEnum(str, Enum): diff --git a/qcelemental/models/v1/results.py b/qcelemental/models/v1/results.py index 44140729..ede7197a 100644 --- a/qcelemental/models/v1/results.py +++ b/qcelemental/models/v1/results.py @@ -3,11 +3,7 @@ from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Union import numpy as np - -try: - from pydantic.v1 import Field, constr, validator -except ImportError: # Will also trap ModuleNotFoundError - from pydantic import Field, constr, validator +from pydantic.v1 import Field, constr, validator from ...util import provenance_stamp from .basemodels import ProtoModel, qcschema_draft @@ -17,10 +13,7 @@ from .types import Array if TYPE_CHECKING: - try: - from pydantic.v1.typing import ReprArgs - except ImportError: # Will also trap ModuleNotFoundError - from pydantic.typing import ReprArgs + from pydantic.v1.typing import ReprArgs class AtomicResultProperties(ProtoModel): diff --git a/qcelemental/models/v2/__init__.py b/qcelemental/models/v2/__init__.py new file mode 100644 index 00000000..509cda9d --- /dev/null +++ b/qcelemental/models/v2/__init__.py @@ -0,0 +1,19 @@ +from . import types +from .align import AlignmentMill +from .basemodels import ProtoModel +from .basis import BasisSet +from .common_models import ComputeError, DriverEnum, FailedOperation, Provenance +from .molecule import Molecule +from .procedures import OptimizationInput, OptimizationResult +from .results import AtomicInput, AtomicResult, AtomicResultProperties + + +def qcschema_models(): + return [ + AtomicInput, + AtomicResult, + AtomicResultProperties, + BasisSet, + Molecule, + Provenance, + ] diff --git a/qcelemental/models/v2/basemodels.py b/qcelemental/models/v2/basemodels.py index c82109f3..e87079d2 100644 --- a/qcelemental/models/v2/basemodels.py +++ b/qcelemental/models/v2/basemodels.py @@ -5,10 +5,8 @@ import numpy as np from pydantic import BaseModel, ConfigDict, model_serializer -from pydantic_settings import BaseSettings # remove when QCFractal merges `next` from qcelemental.util import deserialize, serialize -from qcelemental.util.autodocs import AutoPydanticDocGenerator # remove when QCFractal merges `next` def _repr(self) -> str: @@ -37,6 +35,8 @@ class ExtendedConfigDict(ConfigDict, total=False): class ProtoModel(BaseModel): + """QCSchema extension of pydantic.BaseModel.""" + model_config = ExtendedConfigDict( frozen=True, extra="forbid", @@ -279,10 +279,4 @@ def _merge_config_with(cls, *args, **kwargs): return ExtendedConfigDict(**output_dict) -# remove when QCFractal merges `next` -class AutodocBaseSettings(BaseSettings): - def __init_subclass__(cls) -> None: - cls.__doc__ = AutoPydanticDocGenerator(cls, always_apply=True) - - qcschema_draft = "http://json-schema.org/draft-04/schema#" diff --git a/qcelemental/models/v2/basis.py b/qcelemental/models/v2/basis.py index ca9ad843..54ff278f 100644 --- a/qcelemental/models/v2/basis.py +++ b/qcelemental/models/v2/basis.py @@ -172,7 +172,7 @@ class BasisSet(ProtoModel): description=f"The QCSchema specification to which this model conforms. Explicitly fixed as qcschema_basis.", ) schema_version: int = Field( # type: ignore - 1, + 2, description="The version number of :attr:`~qcelemental.models.BasisSet.schema_name` " "to which this model conforms.", ) diff --git a/qcelemental/models/v2/molecule.py b/qcelemental/models/v2/molecule.py index e7c40403..9e721403 100644 --- a/qcelemental/models/v2/molecule.py +++ b/qcelemental/models/v2/molecule.py @@ -13,6 +13,12 @@ from pydantic import Field, constr, field_validator, model_serializer from typing_extensions import Annotated +try: + import nglview +except ModuleNotFoundError: + # import is purely for forward reference for docs-build. import is not required except for Molecule.show() + pass + # molparse imports separated b/c https://github.com/python/mypy/issues/7203 from ...molparse.from_arrays import from_arrays from ...molparse.from_schema import from_schema diff --git a/qcelemental/models/v2/procedures.py b/qcelemental/models/v2/procedures.py index 2b7ecb86..3e3a365f 100644 --- a/qcelemental/models/v2/procedures.py +++ b/qcelemental/models/v2/procedures.py @@ -65,12 +65,14 @@ class QCInputSpecification(ProtoModel): class OptimizationInput(ProtoModel): + """QCSchema input directive for geometry optimization.""" + id: Optional[str] = None hash_index: Optional[str] = None schema_name: constr( # type: ignore strip_whitespace=True, pattern=qcschema_optimization_input_default ) = qcschema_optimization_input_default - schema_version: int = 1 + schema_version: int = 2 keywords: Dict[str, Any] = Field({}, description="The optimization specific keywords to be used.") extras: Dict[str, Any] = Field({}, description="Extra fields that are not part of the schema.") @@ -89,6 +91,8 @@ def __repr_args__(self) -> "ReprArgs": class OptimizationResult(OptimizationInput): + """QCSchema results model for geometry optimization.""" + schema_name: constr( # type: ignore strip_whitespace=True, pattern=qcschema_optimization_output_default ) = qcschema_optimization_output_default @@ -205,7 +209,7 @@ class TorsionDriveInput(ProtoModel): schema_name: constr( strip_whitespace=True, pattern=qcschema_torsion_drive_input_default ) = qcschema_torsion_drive_input_default # type: ignore - schema_version: int = 1 + schema_version: int = 2 keywords: TDKeywords = Field(..., description="The torsion drive specific keywords to be used.") extras: Dict[str, Any] = Field({}, description="Extra fields that are not part of the schema.") @@ -239,7 +243,7 @@ class TorsionDriveResult(TorsionDriveInput): schema_name: constr( strip_whitespace=True, pattern=qcschema_torsion_drive_output_default ) = qcschema_torsion_drive_output_default # type: ignore - schema_version: int = 1 + schema_version: int = 2 final_energies: Dict[str, float] = Field( ..., description="The final energy at each angle of the TorsionDrive scan." @@ -261,18 +265,3 @@ class TorsionDriveResult(TorsionDriveInput): ) error: Optional[ComputeError] = Field(None, description=str(ComputeError.__doc__)) provenance: Provenance = Field(..., description=str(Provenance.__doc__)) - - -def Optimization(*args, **kwargs): - """QC Optimization Results Schema. - - .. deprecated:: 0.12 - Use :py:func:`qcelemental.models.OptimizationResult` instead. - - """ - from warnings import warn - - warn( - "Optimization has been renamed to OptimizationResult and will be removed as soon as v0.13.0", DeprecationWarning - ) - return OptimizationResult(*args, **kwargs) diff --git a/qcelemental/models/v2/results.py b/qcelemental/models/v2/results.py index ea0b6fcf..3c787ed1 100644 --- a/qcelemental/models/v2/results.py +++ b/qcelemental/models/v2/results.py @@ -2,6 +2,12 @@ from functools import partial from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Union +try: + from typing import Literal +except ImportError: + # remove when minimum py38 + from typing_extensions import Literal + import numpy as np from pydantic import Field, constr, field_validator @@ -26,6 +32,17 @@ class AtomicResultProperties(ProtoModel): * nmo: number of molecular orbitals = :attr:`~qcelemental.models.AtomicResultProperties.calcinfo_nmo` """ + schema_name: Literal["qcschema_atomicproperties"] = Field( + "qcschema_atomicproperties", + description=( + f"The QCSchema specification this model conforms to. Explicitly fixed as qcschema_atomicproperties." + ), + ) + schema_version: int = Field( + 2, + description="The version number of :attr:`~qcelemental.models.AtomicResultProperties.schema_name` to which this model conforms.", + ) + # Calcinfo calcinfo_nbasis: Optional[int] = Field(None, description="The number of basis functions for the computation.") calcinfo_nmo: Optional[int] = Field(None, description="The number of molecular orbitals for the computation.") @@ -635,14 +652,14 @@ class AtomicInput(ProtoModel): r"""The MolSSI Quantum Chemistry Schema""" id: Optional[str] = Field(None, description="The optional ID for the computation.") - schema_name: constr(strip_whitespace=True, pattern="^(qc_?schema_input)$") = Field( # type: ignore + schema_name: constr(strip_whitespace=True, pattern=r"^(qc\_?schema_input)$") = Field( # type: ignore qcschema_input_default, description=( f"The QCSchema specification this model conforms to. Explicitly fixed as {qcschema_input_default}." ), ) schema_version: int = Field( - 1, + 2, description="The version number of :attr:`~qcelemental.models.AtomicInput.schema_name` to which this model conforms.", ) @@ -676,7 +693,7 @@ def __repr_args__(self) -> "ReprArgs": class AtomicResult(AtomicInput): r"""Results from a CMS program execution.""" - schema_name: constr(strip_whitespace=True, pattern="^(qc_?schema_output)$") = Field( # type: ignore + schema_name: constr(strip_whitespace=True, pattern=r"^(qc\_?schema_output)$") = Field( # type: ignore qcschema_output_default, description=( f"The QCSchema specification this model conforms to. Explicitly fixed as {qcschema_output_default}." @@ -831,69 +848,3 @@ def _native_file_protocol(cls, value, info): for rk in return_keep: ret[rk] = files.get(rk, None) return ret - - -class ResultProperties(AtomicResultProperties): - """QC Result Properties Schema. - - .. deprecated:: 0.12 - Use :py:func:`qcelemental.models.AtomicResultProperties` instead. - - """ - - def __init__(self, *args, **kwargs): - from warnings import warn - - warn( - "ResultProperties has been renamed to AtomicResultProperties and will be removed as soon as v0.13.0", - DeprecationWarning, - ) - super().__init__(*args, **kwargs) - - -class ResultProtocols(AtomicResultProtocols): - """QC Result Protocols Schema. - - .. deprecated:: 0.12 - Use :py:func:`qcelemental.models.AtomicResultProtocols` instead. - - """ - - def __init__(self, *args, **kwargs): - from warnings import warn - - warn( - "ResultProtocols has been renamed to AtomicResultProtocols and will be removed as soon as v0.13.0", - DeprecationWarning, - ) - super().__init__(*args, **kwargs) - - -class ResultInput(AtomicInput): - """QC Input Schema. - - .. deprecated:: 0.12 - Use :py:func:`qcelemental.models.AtomicInput` instead. - - """ - - def __init__(self, *args, **kwargs): - from warnings import warn - - warn("ResultInput has been renamed to AtomicInput and will be removed as soon as v0.13.0", DeprecationWarning) - super().__init__(*args, **kwargs) - - -class Result(AtomicResult): - """QC Result Schema. - - .. deprecated:: 0.12 - Use :py:func:`qcelemental.models.AtomicResult` instead. - - """ - - def __init__(self, *args, **kwargs): - from warnings import warn - - warn("Result has been renamed to AtomicResult and will be removed as soon as v0.13.0", DeprecationWarning) - super().__init__(*args, **kwargs) diff --git a/qcelemental/tests/addons.py b/qcelemental/tests/addons.py index a590faf4..f54151fc 100644 --- a/qcelemental/tests/addons.py +++ b/qcelemental/tests/addons.py @@ -62,12 +62,13 @@ def xfail_on_pubchem_busy(): def drop_qcsk(instance, tnm: str, schema_name: str = None): - if isinstance(instance, qcelemental.models.ProtoModel) and schema_name is None: + is_model = isinstance(instance, (qcelemental.models.v1.ProtoModel, qcelemental.models.v2.ProtoModel)) + if is_model and schema_name is None: schema_name = type(instance).__name__ drop = (_data_path / schema_name / tnm).with_suffix(".json") with open(drop, "w") as fp: - if isinstance(instance, qcelemental.models.ProtoModel): + 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)) elif isinstance(instance, dict): @@ -83,7 +84,7 @@ def Molecule(request): if request.param == "v1": return qcelemental.models.v1.Molecule elif request.param == "v2": - return qcelemental.models.v1.Molecule # TODO v2 + return qcelemental.models.v2.Molecule else: return qcelemental.models.Molecule @@ -93,6 +94,6 @@ def schema_versions(request): if request.param == "v1": return qcelemental.models.v1 elif request.param == "v2": - return qcelemental.models.v1 # TODO v2 + return qcelemental.models.v2 else: return qcelemental.models diff --git a/qcelemental/tests/test_model_results.py b/qcelemental/tests/test_model_results.py index f089917d..395d9ce2 100644 --- a/qcelemental/tests/test_model_results.py +++ b/qcelemental/tests/test_model_results.py @@ -1,4 +1,5 @@ import numpy as np +import pydantic import pytest import qcelemental as qcel @@ -549,40 +550,63 @@ def test_result_derivatives_array(request, schema_versions): @pytest.mark.parametrize( - "smodel", ["molecule", "atomicresultproperties", "atomicinput", "atomicresult", "optimizationresult"] + "smodel", ["molecule", "atomicresultproperties", "atomicinput", "atomicresult", "optimizationresult", "basisset"] ) -def test_model_dictable(result_data_fixture, optimization_data_fixture, smodel, schema_versions): - Molecule = schema_versions.Molecule - AtomicResultProperties = schema_versions.AtomicResultProperties - AtomicInput = schema_versions.AtomicInput - AtomicResult = schema_versions.AtomicResult - OptimizationResult = schema_versions.OptimizationResult +def test_model_dictable(result_data_fixture, optimization_data_fixture, smodel, schema_versions, request): + qcsk_ver = "v2" if ("v2" in request.node.name) else "v1" if smodel == "molecule": - model = Molecule + model = schema_versions.Molecule data = result_data_fixture["molecule"].dict() + sver = (2, 2) # TODO , 3) elif smodel == "atomicresultproperties": - model = AtomicResultProperties + model = schema_versions.AtomicResultProperties data = {"scf_one_electron_energy": "-5.0", "scf_dipole_moment": [1, 2, 3], "ccsd_dipole_moment": None} + sver = (None, 2) elif smodel == "atomicinput": - model = AtomicInput + model = schema_versions.AtomicInput data = {k: result_data_fixture[k] for k in ["molecule", "model", "driver"]} + sver = (1, 2) elif smodel == "atomicresult": - model = AtomicResult + model = schema_versions.AtomicResult data = result_data_fixture + sver = (1, 2) elif smodel == "optimizationresult": - model = OptimizationResult + model = schema_versions.OptimizationResult data = optimization_data_fixture + sver = (1, 2) + + elif smodel == "basisset": + model = schema_versions.basis.BasisSet + data = {"name": "custom", "center_data": center_data, "atom_map": ["bs_sto3g_o", "bs_sto3g_h", "bs_sto3g_h"]} + sver = (1, 2) + + def ver_tests(qcsk_ver): + if qcsk_ver == "v1": + if sver[0] is not None: + assert instance.schema_version == sver[0] + assert isinstance(instance, pydantic.v1.BaseModel) + elif qcsk_ver == "v2": + if sver[1] is not None: + assert instance.schema_version == sver[1] + assert isinstance(instance, pydantic.BaseModel) instance = model(**data) - assert model(**instance.dict()) + ver_tests(qcsk_ver) + instance = model(**instance.dict()) + assert instance + ver_tests(qcsk_ver) + +def test_result_model_deprecations(result_data_fixture, optimization_data_fixture, request): + if "v1" not in request.node.name: + # schema_versions coming from fixtures despite not being explicitly present + pytest.skip("Deprecations from 2019 only available from qcel.models.v1") -def test_result_model_deprecations(result_data_fixture, optimization_data_fixture): with pytest.warns(DeprecationWarning): qcel.models.v1.ResultProperties(scf_one_electron_energy="-5.0") diff --git a/qcelemental/tests/test_molutil.py b/qcelemental/tests/test_molutil.py index b5b0b4f7..3cd78f5b 100644 --- a/qcelemental/tests/test_molutil.py +++ b/qcelemental/tests/test_molutil.py @@ -2,12 +2,7 @@ import pprint import numpy as np - -try: - import pydantic.v1 as pydantic -except ImportError: # Will also trap ModuleNotFoundError - import pydantic - +import pydantic import pytest import qcelemental as qcel @@ -166,7 +161,7 @@ def test_error_nat_b787(Molecule): def test_mill_shift_error(schema_versions): AlignmentMill = schema_versions.AlignmentMill - with pytest.raises(pydantic.ValidationError) as e: + with pytest.raises((pydantic.v1.ValidationError, pydantic.ValidationError)) as e: AlignmentMill(shift=[0, 1]) assert "Shift must be castable to shape" in str(e.value) @@ -175,7 +170,7 @@ def test_mill_shift_error(schema_versions): def test_mill_rot_error(schema_versions): AlignmentMill = schema_versions.AlignmentMill - with pytest.raises(pydantic.ValidationError) as e: + with pytest.raises((pydantic.v1.ValidationError, pydantic.ValidationError)) as e: AlignmentMill(rotation=[0, 1, 3]) assert "Rotation must be castable to shape" in str(e.value) diff --git a/qcelemental/tests/test_utils.py b/qcelemental/tests/test_utils.py index f8c70aff..43bbb285 100644 --- a/qcelemental/tests/test_utils.py +++ b/qcelemental/tests/test_utils.py @@ -2,11 +2,7 @@ import numpy as np import pytest - -try: - from pydantic.v1 import BaseModel, Field -except ImportError: # Will also trap ModuleNotFoundError - from pydantic import BaseModel, Field +from pydantic.v1 import BaseModel, Field import qcelemental as qcel from qcelemental.testing import compare_recursive, compare_values @@ -16,6 +12,8 @@ @pytest.fixture(scope="function") def doc_fixture(): + # associated with AutoDoc, so leaving at Pydantic v1 syntax + class Nest(BaseModel): """A nested model""" diff --git a/qcelemental/util/autodocs.py b/qcelemental/util/autodocs.py index b6b64232..ac57b50d 100644 --- a/qcelemental/util/autodocs.py +++ b/qcelemental/util/autodocs.py @@ -3,10 +3,11 @@ from textwrap import dedent, indent from typing import Any -try: - from pydantic.v1 import BaseModel, BaseSettings -except ImportError: # Will also trap ModuleNotFoundError - from pydantic import BaseModel, BaseSettings +from pydantic.v1 import BaseModel, BaseSettings + +# home-grown AutoDoc has been replaced autodoc-pydantic for Sphinx in QCElemental and QCEngine. +# pre-next QCFractal was the last known user. Leaving this in pydantic v1 for now until removed entirely. + __all__ = ["auto_gen_docs_on_demand", "get_base_docs", "AutoPydanticDocGenerator"]