Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⬆️ Upgrade pydantic to v2 #72

Merged
merged 44 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
16e5115
:art: organise imports with isort
ff137 Aug 19, 2023
c56f48e
:arrow_up: upgrade pydantic to v2.2
ff137 Aug 19, 2023
e57e36b
version bump to `0.5.0`
ff137 Aug 19, 2023
5262505
update lock file
ff137 Aug 19, 2023
8667b96
replace `GenericModel` with `BaseModel`, as v2 makes GenericModel unn…
ff137 Aug 19, 2023
ddd8cac
replace deprecated validators with `field_validator` and `model_valid…
ff137 Aug 19, 2023
e2bc30e
replace custom Config class with new `model_config` and `ConfigDict`
ff137 Aug 19, 2023
84673bf
update deprecated `__get_validators__` and `__modify_schema__` with n…
ff137 Aug 19, 2023
7adf5c6
replace deprecated `validate` with `model_validate`
ff137 Aug 19, 2023
f5e63b7
replace deprecated `.dict` and `.json` with `model_dump` and `model_d…
ff137 Aug 19, 2023
e15c1c8
implement `TypeAdapter` for validating/serialising non-BaseModel type…
ff137 Aug 19, 2023
7ed0c95
replace deprecated `construct` with `model_construct`
ff137 Aug 19, 2023
d3e33ae
replace deprecated `__fields__` with `model_fields`
ff137 Aug 19, 2023
4cc41a0
replace custom Config class with new `model_config` and `ConfigDict`.…
ff137 Aug 19, 2023
b872c21
rename `validate` method to `model_validate`, for consistency with py…
ff137 Aug 19, 2023
a708272
:art: fix ruff warnings
ff137 Mar 26, 2024
d027189
:arrow_up: Update `pydantic` to latest
ff137 Mar 26, 2024
24e65c5
Update lock file
ff137 Mar 26, 2024
7f7f0eb
:art: replace deprecated "Extra" field
ff137 Mar 26, 2024
457c7f5
:art: ignore pylance warning
ff137 Mar 26, 2024
a2b0ace
:art: rename `mock_indexed_resource_factory` for clarity, and add typ…
ff137 Mar 26, 2024
db81923
:art: update expected json output
ff137 Mar 26, 2024
ccde93e
:art: fix model_validators to handle Union[dict, Model] values types
ff137 Mar 26, 2024
8fce05b
:art: black formatting
ff137 Apr 12, 2024
27a8381
:arrow_up: Upgrade pydantic to 2.7.0
ff137 Apr 12, 2024
0921fb1
Update lock file
ff137 Apr 12, 2024
0b1e204
:art: black formatting
ff137 Apr 12, 2024
4b2c4c8
:bug: this model validator should be "after"
ff137 Apr 25, 2024
1ad87da
:art: add type to listify
ff137 Apr 25, 2024
8657167
:art:
ff137 Apr 25, 2024
8960744
:art: neaten type adapter dereferencing
ff137 Apr 25, 2024
472a157
:art: typing for field_validator methods
ff137 Apr 25, 2024
9d9d767
:bug: fix _method_appears_to_contain_material
ff137 Apr 25, 2024
0af333f
:bug: fix model validators must all be "before" (!!!)
ff137 Apr 25, 2024
366f4ca
:bug: fix required_group logic for pydantic v2
ff137 Apr 25, 2024
9a13aec
:bug: fix model_fields usage in pydantic v2
ff137 Apr 25, 2024
00c2c38
:white_check: fix test -- dereference as the known type. Dereferencin…
ff137 Apr 25, 2024
64e8288
:sparkles: fix material prop validator to handle fact that model para…
ff137 Apr 25, 2024
cb3cb8d
:rewind: revert change; Deserializing to `KnownVerificationMethods` d…
ff137 Apr 25, 2024
1ec56b2
Update lock file
ff137 Apr 25, 2024
75de2ed
:art: re-add `DID.validate` method for backward compatibility
ff137 Apr 26, 2024
455118a
Update lock file
ff137 Apr 26, 2024
ebc5c4f
:art:
ff137 Apr 26, 2024
51c2475
Merge remote-tracking branch 'upstream/main' into upgrade/pydantic
ff137 May 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
602 changes: 320 additions & 282 deletions poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pydid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .common import DIDError
from .did import DID, InvalidDIDError
from .did_url import DIDUrl, InvalidDIDUrlError
from .doc import corrections, generic
from .doc.builder import DIDDocumentBuilder
from .doc.doc import (
BaseDIDDocument,
Expand All @@ -21,7 +22,6 @@
VerificationMaterialUnknown,
VerificationMethod,
)
from .doc import generic, corrections

__all__ = [
"BasicDIDDocument",
Expand Down
29 changes: 22 additions & 7 deletions pydid/did.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@

"""

from typing import Dict, Optional
from typing import Any, Dict, Optional

from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import CoreSchema, core_schema

from .common import DID_PATTERN, DIDError
from .did_url import DIDUrl
Expand Down Expand Up @@ -35,14 +39,20 @@ def __init__(self, did: str):
self._id = matched.group(2)

@classmethod
def __get_validators__(cls):
"""Yield validators for pydantic."""
yield cls._validate
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
"""Get core schema."""
return core_schema.no_info_after_validator_function(cls, handler(str))

@classmethod
def __modify_schema__(cls, field_schema): # pragma: no cover
def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
"""Update schema fields."""
field_schema.update(pattern=DID_PATTERN)
json_schema = handler(core_schema)
json_schema["pattern"] = DID_PATTERN
return json_schema

@property
def method(self):
Expand Down Expand Up @@ -73,12 +83,17 @@ def is_valid(cls, did: str):
return DID_PATTERN.match(did)

@classmethod
def validate(cls, did: str):
def model_validate(cls, did: str):
"""Validate the given string as a DID."""
if not cls.is_valid(did):
raise InvalidDIDError('"{}" is not a valid DID'.format(did))
return did

@classmethod
def validate(cls, did: str):
"""Validate the given string as a DID."""
return cls.model_validate(did)

@classmethod
def _validate(cls, did):
"""Pydantic validator."""
Expand Down
24 changes: 17 additions & 7 deletions pydid/did_url.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
"""DID URL Object."""

from typing import Dict, Optional, TYPE_CHECKING
from typing import TYPE_CHECKING, Any, Dict, Optional
from urllib.parse import parse_qsl, urlencode, urlparse

from .common import DID_URL_DID_PART_PATTERN, DIDError, DID_URL_RELATIVE_FRONT
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
from pydantic.json_schema import JsonSchemaValue
from pydantic_core import CoreSchema, core_schema

from .common import DID_URL_DID_PART_PATTERN, DID_URL_RELATIVE_FRONT, DIDError

if TYPE_CHECKING: # pragma: no cover
from .did import DID
Expand Down Expand Up @@ -39,14 +43,20 @@ def __init__(self, url: str):
self.fragment = parts.fragment or None

@classmethod
def __get_validators__(cls):
"""Yield validators."""
yield cls.validate
def __get_pydantic_core_schema__(
cls, source_type: Any, handler: GetCoreSchemaHandler
) -> CoreSchema:
"""Get core schema."""
return core_schema.no_info_after_validator_function(cls, handler(str))

@classmethod
def __modify_schema__(cls, field_schema): # pragma: no cover
def __get_pydantic_json_schema__(
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
) -> JsonSchemaValue:
"""Update schema fields."""
field_schema.update(examples=["did:example:123/some/path?query=test#fragment"])
json_schema = handler(core_schema)
json_schema["examples"] = ["did:example:123/some/path?query=test#fragment"]
return json_schema

@classmethod
def parse(cls, url: str):
Expand Down
20 changes: 9 additions & 11 deletions pydid/doc/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
"""DID Document classes."""

from .builder import (
DIDDocumentBuilder,
RelationshipBuilder,
ServiceBuilder,
VerificationMethodBuilder,
)
from .doc import (
IdentifiedResourceMismatch,
IDNotFoundError,
DIDDocumentRoot,
BasicDIDDocument,
DIDDocument,
DIDDocumentError,
DIDDocumentRoot,
IdentifiedResourceMismatch,
IDNotFoundError,
)

from .builder import (
VerificationMethodBuilder,
RelationshipBuilder,
ServiceBuilder,
DIDDocumentBuilder,
)


__all__ = [
"DIDDocumentError",
"IdentifiedResourceMismatch",
Expand Down
2 changes: 1 addition & 1 deletion pydid/doc/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def from_doc(cls, doc: DIDDocument) -> "DIDDocumentBuilder":

def build(self) -> DIDDocument:
"""Build document."""
return DIDDocument.construct(
return DIDDocument.model_construct(
id=self.id,
context=self.context,
also_known_as=self.also_known_as,
Expand Down
8 changes: 4 additions & 4 deletions pydid/doc/doc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from abc import ABC
from typing import Any, List, Optional, Union

from pydantic import Field, validator
from pydantic import Field, field_validator
from typing_extensions import Annotated

from ..did import DID, InvalidDIDError
Expand Down Expand Up @@ -46,12 +46,12 @@ class DIDDocumentRoot(Resource):
capability_delegation: Optional[List[Union[DIDUrl, VerificationMethod]]] = None
service: Optional[List[Service]] = None

@validator("context", "controller", pre=True, allow_reuse=True)
@field_validator("context", "controller", mode="before")
@classmethod
def _listify(cls, value):
def _listify(cls, value) -> Optional[list]:
"""Transform values into lists that are allowed to be a list or single."""
if value is None:
return value
return
if isinstance(value, list):
return value
return [value]
Expand Down
16 changes: 8 additions & 8 deletions pydid/doc/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
"""

import sys
from typing import List, Optional, TypeVar, Union

from pydantic import BaseModel

from typing import TypeVar, Optional, List, Union
from .doc import DIDDocumentRoot, BasicDIDDocument
from ..verification_method import VerificationMethod
from ..service import Service
from ..did_url import DIDUrl
from ..service import Service
from ..verification_method import VerificationMethod
from .doc import BasicDIDDocument, DIDDocumentRoot

if sys.version_info >= (3, 7): # pragma: no cover
# In Python 3.7+, we can use Generics with Pydantic to simplify subclassing
from pydantic.generics import GenericModel
from typing import Generic

VM = TypeVar("VM", bound=VerificationMethod)
Expand All @@ -24,7 +24,7 @@
Relationships = Optional[List[Union[DIDUrl, VM]]]
Services = Optional[List[SV]]

class GenericDIDDocumentRoot(DIDDocumentRoot, GenericModel, Generic[VM, SV]):
class GenericDIDDocumentRoot(DIDDocumentRoot, BaseModel, Generic[VM, SV]):
"""DID Document Root with Generics."""

verification_method: Methods[VM] = None
Expand All @@ -35,7 +35,7 @@ class GenericDIDDocumentRoot(DIDDocumentRoot, GenericModel, Generic[VM, SV]):
capability_delegation: Relationships[VM] = None
service: Services[SV] = None

class GenericBasicDIDDocument(BasicDIDDocument, GenericModel, Generic[VM, SV]):
class GenericBasicDIDDocument(BasicDIDDocument, BaseModel, Generic[VM, SV]):
"""BasicDIDDocument with Generics."""

verification_method: Methods[VM] = None
Expand Down
75 changes: 34 additions & 41 deletions pydid/resource.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
"""Resource class that forms the base of all DID Document components."""

from abc import ABC, abstractmethod
import json
from abc import ABC, abstractmethod
from typing import Any, Dict, Type, TypeVar

from inflection import camelize
from pydantic import BaseModel, Extra, parse_obj_as
from typing_extensions import Literal
import typing_extensions
from pydantic import BaseModel, ConfigDict, TypeAdapter, alias_generators
from typing_extensions import Literal

from .validation import wrap_validation_error


ResourceType = TypeVar("ResourceType", bound="Resource")


Expand Down Expand Up @@ -42,31 +40,25 @@ def is_literal(type_):
class Resource(BaseModel):
"""Base class for DID Document components."""

class Config:
"""Configuration for Resources."""

underscore_attrs_are_private = True
extra = Extra.allow
allow_population_by_field_name = True
allow_mutation = False

@classmethod
def alias_generator(cls, string: str) -> str:
"""Transform snake_case to camelCase."""
return camelize(string, uppercase_first_letter=False)
model_config = ConfigDict(
populate_by_name=True,
extra="allow",
alias_generator=alias_generators.to_camel,
)

def serialize(self):
"""Return serialized representation of Resource."""
return self.dict(exclude_none=True, by_alias=True)
return self.model_dump(exclude_none=True, by_alias=True)

@classmethod
def deserialize(cls: Type[ResourceType], value: dict) -> ResourceType:
"""Deserialize into VerificationMethod."""
"""Deserialize into Resource subtype."""
with wrap_validation_error(
ValueError,
message="Failed to deserialize {}".format(cls.__name__),
message=f"Failed to deserialize {cls.__name__}",
):
return parse_obj_as(cls, value)
resource_adapter = TypeAdapter(cls)
return resource_adapter.validate_python(value)

@classmethod
def from_json(cls, value: str):
Expand All @@ -76,26 +68,29 @@ def from_json(cls, value: str):

def to_json(self):
"""Serialize Resource to JSON."""
return self.json(exclude_none=True, by_alias=True)
return self.model_dump_json(exclude_none=True, by_alias=True)

@classmethod
def _fill_in_required_literals(cls, **kwargs) -> Dict[str, Any]:
"""Return dictionary of field name to value from literals."""
for field in cls.__fields__.values():
for field in cls.model_fields.values():
field_name = field.alias
field_type = field.annotation
if (
field.required
and is_literal(field.type_)
and (field.name not in kwargs or kwargs[field.name] is None)
field.is_required()
and is_literal(field_type)
and (field_name not in kwargs or kwargs[field_name] is None)
):
kwargs[field.name] = get_literal_values(field.type_)[0]
kwargs[field_name] = get_literal_values(field_type)[0]
return kwargs

@classmethod
def _overwrite_none_with_defaults(cls, **kwargs) -> Dict[str, Any]:
"""Overwrite none values in kwargs with defaults for corresponding field."""
for field in cls.__fields__.values():
if field.name in kwargs and kwargs[field.name] is None:
kwargs[field.name] = field.get_default()
for field in cls.model_fields.values():
field_name = field.alias
if field_name in kwargs and kwargs[field_name] is None:
kwargs[field_name] = field.get_default()
return kwargs

@classmethod
Expand Down Expand Up @@ -126,19 +121,17 @@ def dereference(self, reference: str) -> Resource:

def dereference_as(self, typ: Type[ResourceType], reference: str) -> ResourceType:
"""Dereference a resource to a specific type."""
resource = self.dereference(reference)
try:
return parse_obj_as(typ, resource.dict())
except ValueError as error:
raise ValueError(
"Dereferenced resource {} could not be parsed as {}".format(
resource, typ
)
) from error
with wrap_validation_error(
ValueError,
message=f"Dereferenced resource {reference} could not be parsed as {typ}",
):
resource = self.dereference(reference)
resource_adapter: TypeAdapter[ResourceType] = TypeAdapter(typ)
return resource_adapter.validate_python(resource.model_dump())

@classmethod
def construct(cls, **data):
def model_construct(cls, **data):
"""Construct and index."""
resource = super(Resource, cls).construct(**data)
resource = super(Resource, cls).model_construct(**data)
resource._index_resources()
return resource
15 changes: 4 additions & 11 deletions pydid/service.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
"""DID Doc Service."""

from typing import Any, List, Mapping, Optional, Union
from typing_extensions import Literal

from pydantic import AnyUrl, Extra, StrictStr
from pydantic import AnyUrl, ConfigDict, StrictStr
from typing_extensions import Literal

from .did import DID
from .did_url import DIDUrl
from .resource import Resource


EndpointStrings = Union[DID, DIDUrl, AnyUrl, StrictStr]


Expand All @@ -28,10 +27,7 @@ class Service(Resource):
class DIDCommV1Service(Service):
"""DID Communication Service."""

class Config:
"""DIDComm Service Config."""

extra = Extra.forbid
model_config = ConfigDict(extra="forbid")

type: Literal["IndyAgent", "did-communication", "DIDCommMessaging"] = (
"did-communication"
Expand All @@ -57,10 +53,7 @@ class DIDCommV2ServiceEndpoint(Resource):
class DIDCommV2Service(Service):
"""DID Communication V2 Service."""

class Config:
"""DIDComm Service Config."""

extra = Extra.forbid
model_config = ConfigDict(extra="forbid")

type: Literal["DIDCommMessaging"] = "DIDCommMessaging"
service_endpoint: Union[List[DIDCommV2ServiceEndpoint], DIDCommV2ServiceEndpoint]
Expand Down
Loading