Skip to content

Commit 8b66232

Browse files
committed
chore!: Update model parsing to use Pydantic V2 instead of Pydantic V1
Signed-off-by: Mark Wiebe <[email protected]>
1 parent 010d852 commit 8b66232

32 files changed

+1297
-1069
lines changed

pyproject.toml

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ classifiers = [
3030
]
3131
dependencies = [
3232
"pyyaml == 6.0.*",
33-
"pydantic >= 1.10.17",
33+
"pydantic >= 2.10, < 3",
3434
]
3535

3636
[project.urls]
@@ -99,7 +99,7 @@ mypy_path = "src"
9999

100100
# See: https://docs.pydantic.dev/mypy_plugin/
101101
# - Helps mypy understand pydantic typing.
102-
plugins = "pydantic.v1.mypy"
102+
plugins = "pydantic.mypy"
103103

104104
[tool.ruff]
105105
line-length = 100
@@ -111,15 +111,6 @@ ignore = [
111111
"E731",
112112
]
113113

114-
[tool.ruff.lint.pep8-naming]
115-
classmethod-decorators = [
116-
"classmethod",
117-
# pydantic decorators are classmethod decorators
118-
# suppress N805 errors on classes decorated with them
119-
"pydantic.validator",
120-
"pydantic.root_validator",
121-
]
122-
123114
[tool.ruff.lint.isort]
124115
known-first-party = [
125116
"openjd",

src/openjd/model/_convert_pydantic_error.py

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,33 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22

3-
from typing import TypedDict, Union, Type
4-
from pydantic.v1 import BaseModel
3+
from typing import Type, Union
4+
from pydantic import BaseModel
5+
from pydantic_core import ErrorDetails
56
from inspect import getmodule
67

7-
# Calling pydantic's ValidationError.errors() returns a list[ErrorDict], but
8-
# pydantic doesn't export the ErrorDict type publicly. So, we create it here for
9-
# type checking.
10-
# Note that we ignore the 'ctx' key since we don't use it.
11-
# See: https://github.com/pydantic/pydantic/blob/d9c2af3a701ca982945a590de1a1da98b3fb4003/pydantic/error_wrappers.py#L50
12-
Loc = tuple[Union[int, str], ...]
138

14-
15-
class ErrorDict(TypedDict):
16-
loc: Loc
17-
msg: str
18-
type: str
19-
20-
21-
def pydantic_validationerrors_to_str(root_model: Type[BaseModel], errors: list[ErrorDict]) -> str:
9+
def pydantic_validationerrors_to_str(
10+
root_model: Type[BaseModel], errors: list[ErrorDetails]
11+
) -> str:
2212
"""This is our own custom stringification of the Pydantic ValidationError to use
2313
in place of str(<ValidationError>). Pydantic's default stringification too verbose for
2414
our purpose, and contains information that we don't want.
2515
"""
2616
results = list[str]()
27-
for error in errors:
28-
results.append(_error_dict_to_str(root_model, error))
17+
for error_details in errors:
18+
results.append(_error_dict_to_str(root_model, error_details))
2919
return f"{len(errors)} validation errors for {root_model.__name__}\n" + "\n".join(results)
3020

3121

32-
def _error_dict_to_str(root_model: Type[BaseModel], error: ErrorDict) -> str:
33-
loc = error["loc"]
34-
msg = error["msg"]
22+
def _error_dict_to_str(root_model: Type[BaseModel], error_details: ErrorDetails) -> str:
23+
error_type = error_details["type"]
24+
loc = error_details["loc"]
25+
# Skip the "Value error," prefix by getting the exception message directly.
26+
# This preserves the message formatting created when Pydantic V1 was in use.
27+
if error_type == "value_error":
28+
msg = str(error_details["ctx"]["error"])
29+
else:
30+
msg = error_details["msg"]
3531

3632
# When a model's root_validator raises an error other than a ValidationError
3733
# (i.e. raises something like a ValueError or a TypeError) then pydantic
@@ -54,7 +50,7 @@ def _error_dict_to_str(root_model: Type[BaseModel], error: ErrorDict) -> str:
5450
return f"{_loc_to_str(root_model, loc)}:\n\t{msg}"
5551

5652

57-
def _loc_to_str(root_model: Type[BaseModel], loc: Loc) -> str:
53+
def _loc_to_str(root_model: Type[BaseModel], loc: tuple[Union[int, str], ...]) -> str:
5854
model_module = getmodule(root_model)
5955

6056
# If a nested error is from a root validator, then just report the error as being

src/openjd/model/_create_job.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from pathlib import Path
55
from typing import Optional, cast
66

7-
from pydantic.v1 import ValidationError
7+
from pydantic import ValidationError
88

99
from ._errors import CompatibilityError, DecodeValidationError
1010
from ._symbol_table import SymbolTable
@@ -22,7 +22,7 @@
2222
SpecificationRevision,
2323
TemplateSpecificationVersion,
2424
)
25-
from ._convert_pydantic_error import pydantic_validationerrors_to_str, ErrorDict
25+
from ._convert_pydantic_error import pydantic_validationerrors_to_str
2626

2727
__all__ = ("preprocess_job_parameters",)
2828

@@ -330,9 +330,7 @@ def create_job(
330330
job = instantiate_model(job_template, symtab)
331331
except ValidationError as exc:
332332
raise DecodeValidationError(
333-
pydantic_validationerrors_to_str(
334-
job_template.__class__, cast(list[ErrorDict], exc.errors())
335-
)
333+
pydantic_validationerrors_to_str(job_template.__class__, exc.errors())
336334
)
337335

338336
return cast(Job, job)
Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22

3-
import re
4-
from typing import TYPE_CHECKING, Any, Callable, Optional, Pattern, Union
5-
6-
from pydantic.v1.errors import AnyStrMaxLengthError, AnyStrMinLengthError, StrRegexError
7-
from pydantic.v1.utils import update_not_none
8-
from pydantic.v1.validators import strict_str_validator
3+
from typing import Any, Callable, Optional, Pattern, Union
94

10-
if TYPE_CHECKING:
11-
from pydantic.v1.typing import CallableGenerator
5+
from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
6+
from pydantic.json_schema import JsonSchemaValue
7+
from pydantic_core import core_schema
8+
import re
129

1310

1411
class DynamicConstrainedStr(str):
@@ -33,37 +30,43 @@ def _get_max_length(cls) -> Optional[int]:
3330
return cls._max_length
3431

3532
@classmethod
36-
def __modify_schema__(cls, field_schema: dict[str, Any]) -> None:
37-
update_not_none(
38-
field_schema,
39-
minLength=cls._min_length,
40-
maxLength=cls._get_max_length(),
41-
)
42-
43-
@classmethod
44-
def __get_validators__(cls) -> "CallableGenerator":
45-
yield strict_str_validator # Always strict string.
46-
yield cls._validate_min_length
47-
yield cls._validate_max_length
48-
yield cls._validate_regex
33+
def _validate(cls, value: str) -> Any:
34+
if not isinstance(value, str):
35+
raise ValueError("String required")
4936

50-
@classmethod
51-
def _validate_min_length(cls, value: str) -> str:
5237
if cls._min_length is not None and len(value) < cls._min_length:
53-
raise AnyStrMinLengthError(limit_value=cls._min_length)
54-
return value
55-
56-
@classmethod
57-
def _validate_max_length(cls, value: str) -> str:
38+
raise ValueError(f"String must be at least {cls._min_length} characters long")
5839
max_length = cls._get_max_length()
40+
5941
if max_length is not None and len(value) > max_length:
60-
raise AnyStrMaxLengthError(limit_value=max_length)
61-
return value
42+
raise ValueError(f"String must be at most {max_length} characters long")
6243

63-
@classmethod
64-
def _validate_regex(cls, value: str) -> str:
6544
if cls._regex is not None:
6645
if not re.match(cls._regex, value):
6746
pattern: str = cls._regex if isinstance(cls._regex, str) else cls._regex.pattern
68-
raise StrRegexError(pattern=pattern)
69-
return value
47+
raise ValueError(f"String does not match the required pattern: {pattern}")
48+
49+
return cls(value)
50+
51+
@classmethod
52+
def __get_pydantic_core_schema__(
53+
cls, source_type: type[Any], handler: GetCoreSchemaHandler
54+
) -> core_schema.CoreSchema:
55+
return core_schema.no_info_plain_validator_function(cls._validate)
56+
57+
@classmethod
58+
def __get_pydantic_json_schema__(
59+
cls, core_schema: core_schema.CoreSchema, handler: GetJsonSchemaHandler
60+
) -> JsonSchemaValue:
61+
json_schema: dict[str, Any] = {"type": "string"}
62+
if cls._min_length is not None:
63+
json_schema["minLength"] = cls._min_length
64+
max_length = cls._get_max_length()
65+
if max_length is not None:
66+
json_schema["maxLength"] = max_length
67+
if cls._regex is not None:
68+
json_schema["pattern"] = (
69+
cls._regex if isinstance(cls._regex, str) else cls._regex.pattern
70+
)
71+
72+
return json_schema

src/openjd/model/_format_strings/_format_string.py

Lines changed: 3 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,13 @@
22

33
from dataclasses import dataclass
44
from numbers import Real
5-
from typing import TYPE_CHECKING, Optional, Union
5+
from typing import Optional, Union
66

77
from .._errors import ExpressionError, TokenError
88
from .._symbol_table import SymbolTable
99
from ._dyn_constrained_str import DynamicConstrainedStr
1010
from ._expression import InterpolationExpression
1111

12-
if TYPE_CHECKING:
13-
from pydantic.v1.typing import CallableGenerator
14-
1512

1613
@dataclass
1714
class ExpressionInfo:
@@ -21,8 +18,9 @@ class ExpressionInfo:
2118
resolved_value: Optional[Union[Real, str]] = None
2219

2320

24-
class FormatStringError(Exception):
21+
class FormatStringError(ValueError):
2522
def __init__(self, *, string: str, start: int, end: int, expr: str = "", details: str = ""):
23+
self.input = string
2624
expression = f"Expression: {expr}. " if expr else ""
2725
reason = f"Reason: {details}." if details else ""
2826
msg = (
@@ -202,24 +200,3 @@ def _preprocess(self) -> list[Union[str, ExpressionInfo]]:
202200
result_list.append(expression_info)
203201

204202
return result_list
205-
206-
# Pydantic datamodel interfaces
207-
# ================================
208-
# Reference: https://pydantic-docs.helpmanual.io/usage/types/#custom-data-types
209-
210-
@classmethod
211-
def __get_validators__(cls) -> "CallableGenerator":
212-
for validator in super().__get_validators__():
213-
yield validator
214-
yield cls._validate
215-
216-
@classmethod
217-
def _validate(cls, value: str) -> "FormatString":
218-
# Reference: https://pydantic-docs.helpmanual.io/usage/validators/
219-
# Class constructor will raise validation errors on the value contents.
220-
try:
221-
return cls(value)
222-
except FormatStringError as e:
223-
# Pydantic validators must return a ValueError or AssertionError
224-
# Convert the FormatStringError into a ValueError
225-
raise ValueError(str(e))

0 commit comments

Comments
 (0)