Skip to content

Commit

Permalink
Merge pull request #119 from SciCatProject/pydantic-v2
Browse files Browse the repository at this point in the history
Support Pydantic v2
  • Loading branch information
jl-wynen authored Jul 6, 2023
2 parents 29bddb1 + ae251d8 commit 152b6ab
Show file tree
Hide file tree
Showing 28 changed files with 629 additions and 324 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ jobs:
commit_message: Apply automatic formatting

tests:
name: Tests py${{ matrix.python }} ${{ matrix.os }}
name: Tests ${{ matrix.os }} ${{ matrix.tox }}
needs: formatting
runs-on: ${{ matrix.os }}
strategy:
matrix:
include:
- {python: '3.11', os: ubuntu-22.04, tox: pydantic2-full}
- {python: '3.11', os: ubuntu-22.04, tox: py311-full}
- {python: '3.10', os: ubuntu-22.04, tox: py310-full}
- {python: '3.9', os: ubuntu-22.04, tox: py39-full}
Expand Down
5 changes: 5 additions & 0 deletions requirements-pydantic2/base.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
email-validator
fabric
pydantic == 2
python-dateutil
requests
8 changes: 8 additions & 0 deletions requirements-pydantic2/test.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-r base.in
filelock
hypothesis
pyfakefs
pytest
pytest-randomly
pytest-xdist
pyyaml
2 changes: 1 addition & 1 deletion requirements/base.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
email-validator
fabric
pydantic
pydantic < 2 # autodoc_pydantic is not compatible with pydantic 2
python-dateutil
requests
6 changes: 3 additions & 3 deletions requirements/base.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# SHA1:7d04d6082ad9fa4c20034b17bf0e831ba768596f
# SHA1:a579aa51495a128838b22e1692012f3918753a8c
#
# This file is autogenerated by pip-compile-multi
# To update, run:
Expand Down Expand Up @@ -35,7 +35,7 @@ paramiko==3.2.0
# via fabric
pycparser==2.21
# via cffi
pydantic==1.10.9
pydantic==1.10.11
# via -r requirements/base.in
pynacl==1.5.0
# via paramiko
Expand All @@ -45,7 +45,7 @@ requests==2.31.0
# via -r requirements/base.in
six==1.16.0
# via python-dateutil
typing-extensions==4.6.3
typing-extensions==4.7.1
# via pydantic
urllib3==2.0.3
# via requests
4 changes: 2 additions & 2 deletions requirements/ci.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ packaging==23.1
# via
# pyproject-api
# tox
platformdirs==3.6.0
platformdirs==3.8.0
# via
# tox
# virtualenv
pluggy==1.0.0
pluggy==1.2.0
# via tox
pyproject-api==1.5.2
# via tox
Expand Down
12 changes: 6 additions & 6 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
-r static.txt
-r test.txt
-r wheels.txt
anyio==3.7.0
anyio==3.7.1
# via jupyter-server
argon2-cffi==21.3.0
# via
Expand Down Expand Up @@ -41,14 +41,14 @@ json5==0.9.14
# via jupyterlab-server
jsonpointer==2.4
# via jsonschema
jsonschema[format-nongpl]==4.17.3
jsonschema[format-nongpl]==4.18.0
# via
# jupyter-events
# jupyterlab-server
# nbformat
jupyter-events==0.6.3
# via jupyter-server
jupyter-server==2.6.0
jupyter-server==2.7.0
# via
# jupyterlab
# jupyterlab-server
Expand Down Expand Up @@ -76,7 +76,7 @@ pathspec==0.11.1
# via black
pip-compile-multi==2.6.3
# via -r requirements/dev.in
pip-tools==6.13.0
pip-tools==6.14.0
# via pip-compile-multi
prometheus-client==0.17.0
# via
Expand Down Expand Up @@ -122,11 +122,11 @@ types-requests==2.31.0.1
# via -r requirements/dev.in
types-urllib3==1.26.25.13
# via types-requests
uri-template==1.2.0
uri-template==1.3.0
# via jsonschema
webcolors==1.13
# via jsonschema
websocket-client==1.6.0
websocket-client==1.6.1
# via jupyter-server
wheel==0.40.0
# via
Expand Down
28 changes: 19 additions & 9 deletions requirements/docs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ alabaster==0.7.13
asttokens==2.2.1
# via stack-data
attrs==23.1.0
# via jsonschema
autodoc-pydantic==1.8.0
# via
# jsonschema
# referencing
autodoc-pydantic==1.9.0
# via -r requirements/docs.in
babel==2.12.1
# via
Expand Down Expand Up @@ -46,7 +48,7 @@ fastjsonschema==2.17.1
# via nbformat
imagesize==1.4.1
# via sphinx
ipykernel==6.23.2
ipykernel==6.24.0
# via -r requirements/docs.in
ipython==8.14.0
# via
Expand All @@ -60,9 +62,11 @@ jinja2==3.1.2
# nbconvert
# nbsphinx
# sphinx
jsonschema==4.17.3
jsonschema==4.18.0
# via nbformat
jupyter-client==8.2.0
jsonschema-specifications==2023.6.1
# via jsonschema
jupyter-client==8.3.0
# via
# ipykernel
# nbclient
Expand Down Expand Up @@ -122,9 +126,9 @@ pexpect==4.8.0
# via ipython
pickleshare==0.7.5
# via ipython
platformdirs==3.6.0
platformdirs==3.8.0
# via jupyter-core
prompt-toolkit==3.0.38
prompt-toolkit==3.0.39
# via ipython
psutil==5.9.5
# via ipykernel
Expand All @@ -141,14 +145,20 @@ pygments==2.15.1
# nbconvert
# pydata-sphinx-theme
# sphinx
pyrsistent==0.19.3
# via jsonschema
pyyaml==6.0
# via myst-parser
pyzmq==25.1.0
# via
# ipykernel
# jupyter-client
referencing==0.29.1
# via
# jsonschema
# jsonschema-specifications
rpds-py==0.8.7
# via
# jsonschema
# referencing
snowballstemmer==2.2.0
# via sphinx
soupsieve==2.4.1
Expand Down
2 changes: 1 addition & 1 deletion requirements/static.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ identify==2.5.24
# via pre-commit
nodeenv==1.8.0
# via pre-commit
platformdirs==3.6.0
platformdirs==3.8.0
# via virtualenv
pre-commit==3.3.3
# via -r requirements/static.in
Expand Down
6 changes: 3 additions & 3 deletions requirements/test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ execnet==1.9.0
# via pytest-xdist
filelock==3.12.2
# via -r requirements/test.in
hypothesis==6.79.1
hypothesis==6.80.0
# via -r requirements/test.in
iniconfig==2.0.0
# via pytest
packaging==23.1
# via pytest
pluggy==1.0.0
pluggy==1.2.0
# via pytest
pyfakefs==5.2.2
# via -r requirements/test.in
pytest==7.3.2
pytest==7.4.0
# via
# -r requirements/test.in
# pytest-randomly
Expand Down
74 changes: 64 additions & 10 deletions src/scitacean/_base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@
)

import dataclasses
from typing import Any, Dict, Iterable, Optional, Type, TypeVar
from datetime import datetime
from typing import Any, Dict, Iterable, Optional, Type, TypeVar, Union

import pydantic
from dateutil.parser import parse as parse_datetime

from ._internal.orcid import is_valid_orcid
from ._internal.pydantic_compat import is_pydantic_v1
from .filesystem import RemotePath
from .logging import get_logger
from .pid import PID
Expand All @@ -39,12 +42,19 @@ class DatasetType(*_DatasetTypeBases):
class BaseModel(pydantic.BaseModel):
"""Base class for Pydantic models for communication with SciCat."""

class Config:
extra = pydantic.Extra.forbid
json_encoders = {
PID: lambda v: str(v),
RemotePath: lambda v: v.posix,
}
if is_pydantic_v1():

class Config:
extra = pydantic.Extra.forbid
json_encoders = {
PID: lambda v: str(v),
RemotePath: lambda v: v.posix,
}

else:
model_config = pydantic.ConfigDict(
extra="forbid",
)

# Some schemas contain fields that we don't want to use in Scitacean.
# Normally, omitting them from the model would result in an error when
Expand All @@ -58,7 +68,7 @@ def __init_subclass__(
super().__init_subclass__(**kwargs)

masked = list(masked) if masked is not None else []
field_names = {field.alias for field in cls.__fields__.values()}
field_names = {field.alias for field in cls.get_model_fields().values()}
masked.extend(key for key in _IGNORED_KWARGS if key not in field_names)
cls._masked_fields = tuple(masked)

Expand All @@ -70,6 +80,32 @@ def _delete_ignored_args(self, args: Dict[str, Any]) -> None:
for key in self._masked_fields:
args.pop(key, None)

if is_pydantic_v1():

@classmethod
def get_model_fields(cls) -> Dict[str, pydantic.fields.ModelField]:
return cls.__fields__

def model_dump(self, *args, **kwargs) -> Dict[str, Any]:
return self.dict(*args, **kwargs)

def model_dump_json(self, *args, **kwargs) -> str:
return self.json(*args, **kwargs)

@classmethod
def model_construct(cls: Type[ModelType], *args, **kwargs) -> ModelType:
return cls.construct(*args, **kwargs)

@classmethod
def model_rebuild(cls, *args, **kwargs) -> Optional[bool]:
return cls.update_forward_refs(*args, **kwargs)

else:

@classmethod
def get_model_fields(cls) -> Dict[str, pydantic.fields.FieldInfo]:
return cls.model_fields


class BaseUserModel:
"""Base class for user models.
Expand Down Expand Up @@ -142,13 +178,31 @@ def construct(
"In particular, some fields may not have the correct type",
str(e),
)
return model.construct(**fields)
return model.model_construct(**fields)


def validate_datetime(value: Optional[Union[str, datetime]]) -> Optional[datetime]:
"""Convert strings to datetimes.
This uses dateutil.parser.parse instead of Pydantic's builtin parser in order to
produce results that are consistent with user inputs.
Pydantic uses a custom type for timezones which is not fully compatible with
dateutil's.
"""
if not isinstance(value, str):
return value
return parse_datetime(value)


def validate_drop(_: Any) -> None:
"""Return ``None``."""
return None


def validate_emails(value: Optional[str]) -> Optional[str]:
if value is None:
return value
return ";".join(pydantic.EmailStr.validate(item) for item in value.split(";"))
return ";".join(pydantic.validate_email(item)[1] for item in value.split(";"))


def validate_orcids(value: Optional[str]) -> Optional[str]:
Expand Down
4 changes: 2 additions & 2 deletions src/scitacean/_dataset_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ def used_by(self, dataset_type: DatasetType) -> bool:
read_only=True,
required=False,
scicat_name="history",
type=History,
type=None,
used_by_derived=True,
used_by_raw=True,
),
Expand Down Expand Up @@ -766,7 +766,7 @@ def end_time(self, end_time: Optional[datetime]) -> None:
self._end_time = end_time

@property
def history(self) -> Optional[History]:
def history(self) -> Optional[None]:
"""List of objects containing old and new values."""
return self._history

Expand Down
19 changes: 19 additions & 0 deletions src/scitacean/_internal/pydantic_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 SciCat Project (https://github.com/SciCatProject/scitacean)
from typing import Any, Literal

import pydantic


def is_pydantic_v1() -> bool:
return pydantic.__version__.split(".", 1)[0] == "1"


def field_validator(
*args: Any,
mode: Literal["before", "after", "wrap", "plain"] = "after",
**kwargs: Any,
) -> Any:
if is_pydantic_v1():
return pydantic.validator(*args, pre=(mode == "before"), **kwargs)
return pydantic.field_validator(*args, mode=mode, **kwargs)
Loading

0 comments on commit 152b6ab

Please sign in to comment.