Skip to content

Commit

Permalink
Merge pull request #72 from ff137/upgrade/pydantic
Browse files Browse the repository at this point in the history
⬆️ Upgrade pydantic to v2
  • Loading branch information
dbluhm committed May 6, 2024
2 parents 5be256f + 51c2475 commit 2c07d90
Show file tree
Hide file tree
Showing 19 changed files with 521 additions and 445 deletions.
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

0 comments on commit 2c07d90

Please sign in to comment.