Skip to content

Commit 2c07d90

Browse files
authored
Merge pull request #72 from ff137/upgrade/pydantic
⬆️ Upgrade pydantic to v2
2 parents 5be256f + 51c2475 commit 2c07d90

19 files changed

+521
-445
lines changed

poetry.lock

+320-282
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pydid/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .common import DIDError
77
from .did import DID, InvalidDIDError
88
from .did_url import DIDUrl, InvalidDIDUrlError
9+
from .doc import corrections, generic
910
from .doc.builder import DIDDocumentBuilder
1011
from .doc.doc import (
1112
BaseDIDDocument,
@@ -21,7 +22,6 @@
2122
VerificationMaterialUnknown,
2223
VerificationMethod,
2324
)
24-
from .doc import generic, corrections
2525

2626
__all__ = [
2727
"BasicDIDDocument",

pydid/did.py

+22-7
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@
77
88
"""
99

10-
from typing import Dict, Optional
10+
from typing import Any, Dict, Optional
11+
12+
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
13+
from pydantic.json_schema import JsonSchemaValue
14+
from pydantic_core import CoreSchema, core_schema
1115

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

3741
@classmethod
38-
def __get_validators__(cls):
39-
"""Yield validators for pydantic."""
40-
yield cls._validate
42+
def __get_pydantic_core_schema__(
43+
cls, source_type: Any, handler: GetCoreSchemaHandler
44+
) -> CoreSchema:
45+
"""Get core schema."""
46+
return core_schema.no_info_after_validator_function(cls, handler(str))
4147

4248
@classmethod
43-
def __modify_schema__(cls, field_schema): # pragma: no cover
49+
def __get_pydantic_json_schema__(
50+
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
51+
) -> JsonSchemaValue:
4452
"""Update schema fields."""
45-
field_schema.update(pattern=DID_PATTERN)
53+
json_schema = handler(core_schema)
54+
json_schema["pattern"] = DID_PATTERN
55+
return json_schema
4656

4757
@property
4858
def method(self):
@@ -73,12 +83,17 @@ def is_valid(cls, did: str):
7383
return DID_PATTERN.match(did)
7484

7585
@classmethod
76-
def validate(cls, did: str):
86+
def model_validate(cls, did: str):
7787
"""Validate the given string as a DID."""
7888
if not cls.is_valid(did):
7989
raise InvalidDIDError('"{}" is not a valid DID'.format(did))
8090
return did
8191

92+
@classmethod
93+
def validate(cls, did: str):
94+
"""Validate the given string as a DID."""
95+
return cls.model_validate(did)
96+
8297
@classmethod
8398
def _validate(cls, did):
8499
"""Pydantic validator."""

pydid/did_url.py

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
"""DID URL Object."""
22

3-
from typing import Dict, Optional, TYPE_CHECKING
3+
from typing import TYPE_CHECKING, Any, Dict, Optional
44
from urllib.parse import parse_qsl, urlencode, urlparse
55

6-
from .common import DID_URL_DID_PART_PATTERN, DIDError, DID_URL_RELATIVE_FRONT
6+
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
7+
from pydantic.json_schema import JsonSchemaValue
8+
from pydantic_core import CoreSchema, core_schema
9+
10+
from .common import DID_URL_DID_PART_PATTERN, DID_URL_RELATIVE_FRONT, DIDError
711

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

4145
@classmethod
42-
def __get_validators__(cls):
43-
"""Yield validators."""
44-
yield cls.validate
46+
def __get_pydantic_core_schema__(
47+
cls, source_type: Any, handler: GetCoreSchemaHandler
48+
) -> CoreSchema:
49+
"""Get core schema."""
50+
return core_schema.no_info_after_validator_function(cls, handler(str))
4551

4652
@classmethod
47-
def __modify_schema__(cls, field_schema): # pragma: no cover
53+
def __get_pydantic_json_schema__(
54+
cls, core_schema: CoreSchema, handler: GetJsonSchemaHandler
55+
) -> JsonSchemaValue:
4856
"""Update schema fields."""
49-
field_schema.update(examples=["did:example:123/some/path?query=test#fragment"])
57+
json_schema = handler(core_schema)
58+
json_schema["examples"] = ["did:example:123/some/path?query=test#fragment"]
59+
return json_schema
5060

5161
@classmethod
5262
def parse(cls, url: str):

pydid/doc/__init__.py

+9-11
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,20 @@
11
"""DID Document classes."""
22

3+
from .builder import (
4+
DIDDocumentBuilder,
5+
RelationshipBuilder,
6+
ServiceBuilder,
7+
VerificationMethodBuilder,
8+
)
39
from .doc import (
4-
IdentifiedResourceMismatch,
5-
IDNotFoundError,
6-
DIDDocumentRoot,
710
BasicDIDDocument,
811
DIDDocument,
912
DIDDocumentError,
13+
DIDDocumentRoot,
14+
IdentifiedResourceMismatch,
15+
IDNotFoundError,
1016
)
1117

12-
from .builder import (
13-
VerificationMethodBuilder,
14-
RelationshipBuilder,
15-
ServiceBuilder,
16-
DIDDocumentBuilder,
17-
)
18-
19-
2018
__all__ = [
2119
"DIDDocumentError",
2220
"IdentifiedResourceMismatch",

pydid/doc/builder.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ def from_doc(cls, doc: DIDDocument) -> "DIDDocumentBuilder":
240240

241241
def build(self) -> DIDDocument:
242242
"""Build document."""
243-
return DIDDocument.construct(
243+
return DIDDocument.model_construct(
244244
id=self.id,
245245
context=self.context,
246246
also_known_as=self.also_known_as,

pydid/doc/doc.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from abc import ABC
44
from typing import Any, List, Optional, Union
55

6-
from pydantic import Field, validator
6+
from pydantic import Field, field_validator
77
from typing_extensions import Annotated
88

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

49-
@validator("context", "controller", pre=True, allow_reuse=True)
49+
@field_validator("context", "controller", mode="before")
5050
@classmethod
51-
def _listify(cls, value):
51+
def _listify(cls, value) -> Optional[list]:
5252
"""Transform values into lists that are allowed to be a list or single."""
5353
if value is None:
54-
return value
54+
return
5555
if isinstance(value, list):
5656
return value
5757
return [value]

pydid/doc/generic.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@
66
"""
77

88
import sys
9+
from typing import List, Optional, TypeVar, Union
10+
11+
from pydantic import BaseModel
912

10-
from typing import TypeVar, Optional, List, Union
11-
from .doc import DIDDocumentRoot, BasicDIDDocument
12-
from ..verification_method import VerificationMethod
13-
from ..service import Service
1413
from ..did_url import DIDUrl
14+
from ..service import Service
15+
from ..verification_method import VerificationMethod
16+
from .doc import BasicDIDDocument, DIDDocumentRoot
1517

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

2121
VM = TypeVar("VM", bound=VerificationMethod)
@@ -24,7 +24,7 @@
2424
Relationships = Optional[List[Union[DIDUrl, VM]]]
2525
Services = Optional[List[SV]]
2626

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

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

38-
class GenericBasicDIDDocument(BasicDIDDocument, GenericModel, Generic[VM, SV]):
38+
class GenericBasicDIDDocument(BasicDIDDocument, BaseModel, Generic[VM, SV]):
3939
"""BasicDIDDocument with Generics."""
4040

4141
verification_method: Methods[VM] = None

pydid/resource.py

+34-41
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
"""Resource class that forms the base of all DID Document components."""
22

3-
from abc import ABC, abstractmethod
43
import json
4+
from abc import ABC, abstractmethod
55
from typing import Any, Dict, Type, TypeVar
66

7-
from inflection import camelize
8-
from pydantic import BaseModel, Extra, parse_obj_as
9-
from typing_extensions import Literal
107
import typing_extensions
8+
from pydantic import BaseModel, ConfigDict, TypeAdapter, alias_generators
9+
from typing_extensions import Literal
1110

1211
from .validation import wrap_validation_error
1312

14-
1513
ResourceType = TypeVar("ResourceType", bound="Resource")
1614

1715

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

45-
class Config:
46-
"""Configuration for Resources."""
47-
48-
underscore_attrs_are_private = True
49-
extra = Extra.allow
50-
allow_population_by_field_name = True
51-
allow_mutation = False
52-
53-
@classmethod
54-
def alias_generator(cls, string: str) -> str:
55-
"""Transform snake_case to camelCase."""
56-
return camelize(string, uppercase_first_letter=False)
43+
model_config = ConfigDict(
44+
populate_by_name=True,
45+
extra="allow",
46+
alias_generator=alias_generators.to_camel,
47+
)
5748

5849
def serialize(self):
5950
"""Return serialized representation of Resource."""
60-
return self.dict(exclude_none=True, by_alias=True)
51+
return self.model_dump(exclude_none=True, by_alias=True)
6152

6253
@classmethod
6354
def deserialize(cls: Type[ResourceType], value: dict) -> ResourceType:
64-
"""Deserialize into VerificationMethod."""
55+
"""Deserialize into Resource subtype."""
6556
with wrap_validation_error(
6657
ValueError,
67-
message="Failed to deserialize {}".format(cls.__name__),
58+
message=f"Failed to deserialize {cls.__name__}",
6859
):
69-
return parse_obj_as(cls, value)
60+
resource_adapter = TypeAdapter(cls)
61+
return resource_adapter.validate_python(value)
7062

7163
@classmethod
7264
def from_json(cls, value: str):
@@ -76,26 +68,29 @@ def from_json(cls, value: str):
7668

7769
def to_json(self):
7870
"""Serialize Resource to JSON."""
79-
return self.json(exclude_none=True, by_alias=True)
71+
return self.model_dump_json(exclude_none=True, by_alias=True)
8072

8173
@classmethod
8274
def _fill_in_required_literals(cls, **kwargs) -> Dict[str, Any]:
8375
"""Return dictionary of field name to value from literals."""
84-
for field in cls.__fields__.values():
76+
for field in cls.model_fields.values():
77+
field_name = field.alias
78+
field_type = field.annotation
8579
if (
86-
field.required
87-
and is_literal(field.type_)
88-
and (field.name not in kwargs or kwargs[field.name] is None)
80+
field.is_required()
81+
and is_literal(field_type)
82+
and (field_name not in kwargs or kwargs[field_name] is None)
8983
):
90-
kwargs[field.name] = get_literal_values(field.type_)[0]
84+
kwargs[field_name] = get_literal_values(field_type)[0]
9185
return kwargs
9286

9387
@classmethod
9488
def _overwrite_none_with_defaults(cls, **kwargs) -> Dict[str, Any]:
9589
"""Overwrite none values in kwargs with defaults for corresponding field."""
96-
for field in cls.__fields__.values():
97-
if field.name in kwargs and kwargs[field.name] is None:
98-
kwargs[field.name] = field.get_default()
90+
for field in cls.model_fields.values():
91+
field_name = field.alias
92+
if field_name in kwargs and kwargs[field_name] is None:
93+
kwargs[field_name] = field.get_default()
9994
return kwargs
10095

10196
@classmethod
@@ -126,19 +121,17 @@ def dereference(self, reference: str) -> Resource:
126121

127122
def dereference_as(self, typ: Type[ResourceType], reference: str) -> ResourceType:
128123
"""Dereference a resource to a specific type."""
129-
resource = self.dereference(reference)
130-
try:
131-
return parse_obj_as(typ, resource.dict())
132-
except ValueError as error:
133-
raise ValueError(
134-
"Dereferenced resource {} could not be parsed as {}".format(
135-
resource, typ
136-
)
137-
) from error
124+
with wrap_validation_error(
125+
ValueError,
126+
message=f"Dereferenced resource {reference} could not be parsed as {typ}",
127+
):
128+
resource = self.dereference(reference)
129+
resource_adapter: TypeAdapter[ResourceType] = TypeAdapter(typ)
130+
return resource_adapter.validate_python(resource.model_dump())
138131

139132
@classmethod
140-
def construct(cls, **data):
133+
def model_construct(cls, **data):
141134
"""Construct and index."""
142-
resource = super(Resource, cls).construct(**data)
135+
resource = super(Resource, cls).model_construct(**data)
143136
resource._index_resources()
144137
return resource

pydid/service.py

+4-11
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
"""DID Doc Service."""
22

33
from typing import Any, List, Mapping, Optional, Union
4-
from typing_extensions import Literal
54

6-
from pydantic import AnyUrl, Extra, StrictStr
5+
from pydantic import AnyUrl, ConfigDict, StrictStr
6+
from typing_extensions import Literal
77

88
from .did import DID
99
from .did_url import DIDUrl
1010
from .resource import Resource
1111

12-
1312
EndpointStrings = Union[DID, DIDUrl, AnyUrl, StrictStr]
1413

1514

@@ -28,10 +27,7 @@ class Service(Resource):
2827
class DIDCommV1Service(Service):
2928
"""DID Communication Service."""
3029

31-
class Config:
32-
"""DIDComm Service Config."""
33-
34-
extra = Extra.forbid
30+
model_config = ConfigDict(extra="forbid")
3531

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

60-
class Config:
61-
"""DIDComm Service Config."""
62-
63-
extra = Extra.forbid
56+
model_config = ConfigDict(extra="forbid")
6457

6558
type: Literal["DIDCommMessaging"] = "DIDCommMessaging"
6659
service_endpoint: Union[List[DIDCommV2ServiceEndpoint], DIDCommV2ServiceEndpoint]

0 commit comments

Comments
 (0)