From 8dccf55469ac30c9e94d50dd9968c6ad7b686470 Mon Sep 17 00:00:00 2001 From: Alex Hadley Date: Tue, 23 Apr 2024 16:29:09 -0700 Subject: [PATCH] #169 Add Pydantic functionality to ParamDataclass --- paramdb/_database.py | 8 +- paramdb/_param_data/_dataclasses.py | 92 +++++++++++++--- paramdb/_param_data/_param_data.py | 33 +++--- poetry.lock | 145 +++++++++++++++++++++++++- pyproject.toml | 4 + tests/_param_data/test_dataclasses.py | 3 +- 6 files changed, 242 insertions(+), 43 deletions(-) diff --git a/paramdb/_database.py b/paramdb/_database.py index 0023c65..f2de3d0 100644 --- a/paramdb/_database.py +++ b/paramdb/_database.py @@ -19,9 +19,9 @@ try: from astropy.units import Quantity # type: ignore - ASTROPY_INSTALLED = True + _ASTROPY_INSTALLED = True except ImportError: - ASTROPY_INSTALLED = False + _ASTROPY_INSTALLED = False T = TypeVar("T") SelectT = TypeVar("SelectT", bound=Select[Any]) @@ -62,7 +62,7 @@ def _to_dict(obj: Any) -> Any: class_full_name_dict = {CLASS_NAME_KEY: class_full_name} if isinstance(obj, datetime): return class_full_name_dict | {"isoformat": obj.isoformat()} - if ASTROPY_INSTALLED and isinstance(obj, Quantity): + if _ASTROPY_INSTALLED and isinstance(obj, Quantity): return class_full_name_dict | {"value": obj.value, "unit": str(obj.unit)} if isinstance(obj, ParamData): return {CLASS_NAME_KEY: type(obj).__name__} | obj.to_dict() @@ -86,7 +86,7 @@ def _from_dict(json_dict: dict[str, Any]) -> Any: class_name = json_dict.pop(CLASS_NAME_KEY) if class_name == _full_class_name(datetime): return datetime.fromisoformat(json_dict["isoformat"]).astimezone() - if ASTROPY_INSTALLED and class_name == _full_class_name(Quantity): + if _ASTROPY_INSTALLED and class_name == _full_class_name(Quantity): return Quantity(**json_dict) param_class = get_param_class(class_name) if param_class is not None: diff --git a/paramdb/_param_data/_dataclasses.py b/paramdb/_param_data/_dataclasses.py index 30dd256..d19a84c 100644 --- a/paramdb/_param_data/_dataclasses.py +++ b/paramdb/_param_data/_dataclasses.py @@ -6,6 +6,14 @@ from typing_extensions import Self, dataclass_transform from paramdb._param_data._param_data import ParamData +try: + import pydantic + import pydantic.dataclasses + + _PYDANTIC_INSTALLED = True +except ImportError: + _PYDANTIC_INSTALLED = False + @dataclass_transform() class ParamDataclass(ParamData): @@ -20,11 +28,72 @@ class CustomParam(ParamDataclass): value2: int - Any keyword arguments given when creating a subclass are passed internally to the - standard ``@dataclass()`` decorator. + Any class keyword arguments (other than those described below) given when creating a + subclass are passed internally to the ``@dataclass()`` decorator. + + If Pydantic is installed, then subclasses will have Pydantic runtime validation + enabled by default. This can be disabled using the class keyword argument + ``type_validation``. The following Pydantic configuration values are set by default: + + - extra: ``'forbid'`` (forbid extra attributes) + - validate_assignment: ``True`` (validate on assignment as well as initialization) + - arbitrary_types_allowed: ``True`` (allow arbitrary type hints) + - strict: ``True`` (disable value coercion, e.g. '2' -> 2) + + Pydantic configuration options can be updated using the class keyword argument + ``pydantic_config``, which will merge new options with the existing configuration. """ - __field_names: set[str] + __field_names: set[str] # Data class field names + __type_validation: bool = True # Whether to use Pydantic + __pydantic_config: pydantic.ConfigDict = { + "extra": "forbid", + "validate_assignment": True, + "arbitrary_types_allowed": True, + "strict": True, + } + + # Set in __init_subclass__() and used to set attributes within __setattr__() + # pylint: disable-next=unused-argument + def __base_setattr(self: Any, name: str, value: Any) -> None: ... + + def __init_subclass__( + cls, + /, + type_validation: bool | None = None, + pydantic_config: pydantic.ConfigDict | None = None, + **kwargs: Any, + ) -> None: + super().__init_subclass__() # kwargs are passed to dataclass constructor + if type_validation is not None: + cls.__type_validation = type_validation + if pydantic_config is not None: + # Merge new Pydantic config with the old one + cls.__pydantic_config |= pydantic_config + cls.__base_setattr = object.__setattr__ # type: ignore + if _PYDANTIC_INSTALLED and cls.__type_validation: + # Transform the class into a Pydantic data class, with custom handling for + # validate_assignment + pydantic.dataclasses.dataclass( + config=cls.__pydantic_config | {"validate_assignment": False}, **kwargs + )(cls) + if cls.__pydantic_config["validate_assignment"]: + pydantic_validator = ( + pydantic.dataclasses.is_pydantic_dataclass(cls) + and cls.__pydantic_validator__ # pylint: disable=no-member + ) + if pydantic_validator: + + def __base_setattr(self: Any, name: str, value: Any) -> None: + pydantic_validator.validate_assignment(self, name, value) + + cls.__base_setattr = __base_setattr # type: ignore + else: + # Transform the class into a data class + dataclass(**kwargs)(cls) + cls.__field_names = ( + {f.name for f in fields(cls)} if is_dataclass(cls) else set() + ) # pylint: disable-next=unused-argument def __new__(cls, *args: Any, **kwargs: Any) -> Self: @@ -32,20 +101,11 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self: # since __init__() will be overwritten by dataclass() if cls is ParamDataclass: raise TypeError("only subclasses of ParamDataclass can be instantiated") - self = super(ParamDataclass, cls).__new__(cls) - super(ParamDataclass, self).__init__() # pylint: disable=super-with-arguments + self = super().__new__(cls) + super().__init__(self) return self - def __init_subclass__(cls, /, **kwargs: Any) -> None: - # Convert subclasses into dataclasses - super().__init_subclass__() # kwargs are passed to dataclass constructor - dataclass(**kwargs)(cls) - cls.__field_names = ( - {f.name for f in fields(cls)} if is_dataclass(cls) else set() - ) - def __post_init__(self) -> None: - # Called by the self.__init__() generated by dataclass() for field_name in self.__field_names: self._add_child(getattr(self, field_name)) @@ -61,12 +121,12 @@ def __setattr__(self, name: str, value: Any) -> None: # If this attribute is a Data Class field, update last updated and children if name in self.__field_names: old_value = getattr(self, name) if hasattr(self, name) else None - super().__setattr__(name, value) + self.__base_setattr(name, value) self._update_last_updated() self._remove_child(old_value) self._add_child(value) return - super().__setattr__(name, value) + self.__base_setattr(name, value) def _to_json(self) -> dict[str, Any]: if is_dataclass(self): diff --git a/paramdb/_param_data/_param_data.py b/paramdb/_param_data/_param_data.py index a195811..843fbf0 100644 --- a/paramdb/_param_data/_param_data.py +++ b/paramdb/_param_data/_param_data.py @@ -24,8 +24,8 @@ def get_param_class(class_name: str) -> type[ParamData] | None: class ParamData(ABC): """Abstract base class for all parameter data.""" - __parent: ParamData | None = None - __last_updated: datetime + _parent: ParamData | None = None + _last_updated: datetime def __init_subclass__(cls, /, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) @@ -34,19 +34,17 @@ def __init_subclass__(cls, /, **kwargs: Any) -> None: _param_classes[cls.__name__] = cls def __init__(self) -> None: - self.__last_updated = datetime.now(timezone.utc).astimezone() + super().__setattr__("_last_updated", datetime.now(timezone.utc).astimezone()) def _add_child(self, child: Any) -> None: """Add the given object as a child, if it is ``ParamData``.""" if isinstance(child, ParamData): - # pylint: disable-next=protected-access,unused-private-member - child.__parent = self + super(ParamData, child).__setattr__("_parent", self) def _remove_child(self, child: Any) -> None: """Remove the given object as a child, if it is ``ParamData``.""" if isinstance(child, ParamData): - # pylint: disable-next=protected-access,unused-private-member - child.__parent = None + super(ParamData, child).__setattr__("_parent", None) def _update_last_updated(self) -> None: """Update last updated for this object and its chain of parents.""" @@ -57,10 +55,10 @@ def _update_last_updated(self) -> None: # Continue up the chain of parents, stopping if we reach a last updated # timestamp that is more recent than the new one while current and not ( - current.__last_updated and current.__last_updated >= new_last_updated + current._last_updated and current._last_updated >= new_last_updated ): - current.__last_updated = new_last_updated - current = current.__parent + super(ParamData, current).__setattr__("_last_updated", new_last_updated) + current = current._parent @abstractmethod def _to_json(self) -> Any: @@ -91,7 +89,7 @@ def to_dict(self) -> dict[str, Any]: Return a dictionary representation of this parameter data object, which can be used to reconstruct the object by passing it to :py:meth:`from_dict`. """ - return {_LAST_UPDATED_KEY: self.last_updated, _DATA_KEY: self._to_json()} + return {_LAST_UPDATED_KEY: self._last_updated, _DATA_KEY: self._to_json()} @classmethod def from_dict(cls, data_dict: dict[str, Any]) -> Self: @@ -100,14 +98,13 @@ def from_dict(cls, data_dict: dict[str, Any]) -> Self: ``json.loads()`` and originally constructed by :py:meth:`from_dict`. """ param_data = cls._from_json(data_dict[_DATA_KEY]) - # pylint: disable-next=protected-access,unused-private-member - param_data.__last_updated = data_dict[_LAST_UPDATED_KEY] + super().__setattr__(param_data, "_last_updated", data_dict[_LAST_UPDATED_KEY]) return param_data @property def last_updated(self) -> datetime: """When any parameter within this parameter data was last updated.""" - return self.__last_updated + return self._last_updated @property def parent(self) -> ParamData: @@ -119,12 +116,12 @@ def parent(self) -> ParamData: Raises a ``ValueError`` if there is currently no parent, which can occur if the parent is still being initialized. """ - if self.__parent is None: + if self._parent is None: raise ValueError( f"'{type(self).__name__}' object has no parent, or its parent has not" " been initialized yet" ) - return self.__parent + return self._parent @property def root(self) -> ParamData: @@ -134,6 +131,6 @@ def root(self) -> ParamData: """ # pylint: disable=protected-access root = self - while root.__parent is not None: - root = root.__parent + while root._parent is not None: + root = root._parent return root diff --git a/poetry.lock b/poetry.lock index 432dc42..85709d3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,6 +11,17 @@ files = [ {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = true +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + [[package]] name = "appnope" version = "0.1.4" @@ -90,13 +101,13 @@ test-all = ["astropy[test]", "coverage[toml]", "ipython (>=4.2)", "objgraph", "s [[package]] name = "astropy-iers-data" -version = "0.2024.4.15.2.45.49" +version = "0.2024.4.22.0.29.50" description = "IERS Earth Rotation and Leap Second tables for the astropy core package" optional = true python-versions = ">=3.8" files = [ - {file = "astropy_iers_data-0.2024.4.15.2.45.49-py3-none-any.whl", hash = "sha256:3f2b1944be1d71fcd88728e217f21fd01a59455b2990684f953cfac9b5e2f739"}, - {file = "astropy_iers_data-0.2024.4.15.2.45.49.tar.gz", hash = "sha256:97a36a65abd8a4723dc3353c739364f12855a2b92b0eda47ad81afd282559997"}, + {file = "astropy_iers_data-0.2024.4.22.0.29.50-py3-none-any.whl", hash = "sha256:2986299dbeb0e4a5f1703d2aa6bba7a2fc860963e34bb2adf58f29bf79336ab5"}, + {file = "astropy_iers_data-0.2024.4.22.0.29.50.tar.gz", hash = "sha256:f44afb60448016d9ebfc29c8212cf3a50724073a9a79530cc1be782f8db4d125"}, ] [package.extras] @@ -601,6 +612,20 @@ files = [ {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] +[[package]] +name = "eval-type-backport" +version = "0.2.0" +description = "Like `typing._eval_type`, but lets older Python versions use newer typing features." +optional = true +python-versions = ">=3.8" +files = [ + {file = "eval_type_backport-0.2.0-py3-none-any.whl", hash = "sha256:ac2f73d30d40c5a30a80b8739a789d6bb5e49fdffa66d7912667e2015d9c9933"}, + {file = "eval_type_backport-0.2.0.tar.gz", hash = "sha256:68796cfbc7371ebf923f03bdf7bef415f3ec098aeced24e054b253a0e78f7b37"}, +] + +[package.extras] +tests = ["pytest"] + [[package]] name = "exceptiongroup" version = "1.2.1" @@ -1624,6 +1649,116 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +[[package]] +name = "pydantic" +version = "2.7.0" +description = "Data validation using Python type hints" +optional = true +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"}, + {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.18.1" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.18.1" +description = "Core functionality for Pydantic validation and serialization" +optional = true +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"}, + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"}, + {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"}, + {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"}, + {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"}, + {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"}, + {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"}, + {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"}, + {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"}, + {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"}, + {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"}, + {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"}, + {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + [[package]] name = "pyerfa" version = "2.0.1.4" @@ -2605,9 +2740,11 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\ cffi = ["cffi (>=1.11)"] [extras] +all = ["astropy", "pydantic"] astropy = ["astropy"] +pydantic = ["eval-type-backport", "pydantic"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "04116a506de3dc9c681f0d63eeb47e265ef2a28b05bbf88e4a2861a00c301836" +content-hash = "1a77add5a2ab25667d6572d1ee6f217fe960f318cb907c34b37bef4215b3ed9a" diff --git a/pyproject.toml b/pyproject.toml index 7c49b44..87beac9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,9 +17,13 @@ typing-extensions = "^4.11.0" sqlalchemy = "^2.0.29" zstandard = "^0.22.0" astropy = { version = "^6.0.1", optional = true } +pydantic = { version = "^2.7.0", optional = true } +eval-type-backport = { version = "^0.2.0", optional = true } [tool.poetry.extras] +all = ["astropy", "pydantic"] astropy = ["astropy"] +pydantic = ["pydantic", "eval-type-backport"] [tool.poetry.group.dev.dependencies] mypy = "^1.9.0" diff --git a/tests/_param_data/test_dataclasses.py b/tests/_param_data/test_dataclasses.py index 07d913d..5965df4 100644 --- a/tests/_param_data/test_dataclasses.py +++ b/tests/_param_data/test_dataclasses.py @@ -80,7 +80,8 @@ def test_param_dataclass_set_last_updated_non_field( parameter is set. """ with capture_start_end_times() as times: - param_dataclass_object.non_field = 1.23 + # Use ParamData's setattr function to bypass Pydantic validation + ParamData.__setattr__(param_dataclass_object, "non_field", 1.23) assert param_dataclass_object.last_updated.timestamp() < times.start