Skip to content

Commit 45d812f

Browse files
authored
use pydantic's version in error messages (#759)
1 parent dfbedbd commit 45d812f

13 files changed

+83
-50
lines changed

src/errors/validation_exception.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use serde_json::ser::PrettyFormatter;
1515

1616
use crate::build_tools::py_schema_error_type;
1717
use crate::errors::LocItem;
18-
use crate::get_version;
18+
use crate::get_pydantic_version;
1919
use crate::serializers::{SerMode, SerializationState};
2020
use crate::tools::{safe_repr, SchemaDict};
2121

@@ -113,7 +113,12 @@ static URL_PREFIX: GILOnceCell<String> = GILOnceCell::new();
113113

114114
fn get_url_prefix(py: Python, include_url: bool) -> Option<&str> {
115115
if include_url {
116-
Some(URL_PREFIX.get_or_init(py, || format!("https://errors.pydantic.dev/{}/v/", get_version())))
116+
Some(URL_PREFIX.get_or_init(py, || {
117+
format!(
118+
"https://errors.pydantic.dev/{}/v/",
119+
get_pydantic_version(py).unwrap_or("latest")
120+
)
121+
}))
117122
} else {
118123
None
119124
}

src/lib.rs

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
extern crate core;
44

5-
use pyo3::prelude::*;
5+
use std::sync::OnceLock;
6+
7+
use pyo3::{prelude::*, sync::GILOnceCell};
68

79
#[cfg(feature = "mimalloc")]
810
#[global_allocator]
@@ -33,14 +35,31 @@ pub use serializers::{
3335
};
3436
pub use validators::{PySome, SchemaValidator};
3537

36-
pub fn get_version() -> String {
37-
let version = env!("CARGO_PKG_VERSION");
38-
// cargo uses "1.0-alpha1" etc. while python uses "1.0.0a1", this is not full compatibility,
39-
// but it's good enough for now
40-
// see https://docs.rs/semver/1.0.9/semver/struct.Version.html#method.parse for rust spec
41-
// see https://peps.python.org/pep-0440/ for python spec
42-
// it seems the dot after "alpha/beta" e.g. "-alpha.1" is not necessary, hence why this works
43-
version.replace("-alpha", "a").replace("-beta", "b")
38+
pub fn get_pydantic_core_version() -> &'static str {
39+
static PYDANTIC_CORE_VERSION: OnceLock<String> = OnceLock::new();
40+
41+
PYDANTIC_CORE_VERSION.get_or_init(|| {
42+
let version = env!("CARGO_PKG_VERSION");
43+
// cargo uses "1.0-alpha1" etc. while python uses "1.0.0a1", this is not full compatibility,
44+
// but it's good enough for now
45+
// see https://docs.rs/semver/1.0.9/semver/struct.Version.html#method.parse for rust spec
46+
// see https://peps.python.org/pep-0440/ for python spec
47+
// it seems the dot after "alpha/beta" e.g. "-alpha.1" is not necessary, hence why this works
48+
version.replace("-alpha", "a").replace("-beta", "b")
49+
})
50+
}
51+
52+
/// Returns the installed version of pydantic.
53+
fn get_pydantic_version(py: Python<'_>) -> Option<&'static str> {
54+
static PYDANTIC_VERSION: GILOnceCell<Option<String>> = GILOnceCell::new();
55+
56+
PYDANTIC_VERSION
57+
.get_or_init(py, || {
58+
py.import("pydantic")
59+
.and_then(|pydantic| pydantic.getattr("__version__")?.extract())
60+
.ok()
61+
})
62+
.as_deref()
4463
}
4564

4665
pub fn build_info() -> String {
@@ -54,7 +73,7 @@ pub fn build_info() -> String {
5473

5574
#[pymodule]
5675
fn _pydantic_core(py: Python, m: &PyModule) -> PyResult<()> {
57-
m.add("__version__", get_version())?;
76+
m.add("__version__", get_pydantic_core_version())?;
5877
m.add("build_profile", env!("PROFILE"))?;
5978
m.add("build_info", build_info())?;
6079
m.add("_recursion_limit", recursion_guard::RECURSION_GUARD_LIMIT)?;

tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@ def _import_execute(source: str, *, custom_module_name: 'str | None' = None):
123123
return _import_execute
124124

125125

126+
@pytest.fixture
127+
def pydantic_version():
128+
try:
129+
import pydantic
130+
131+
return pydantic.__version__
132+
except ImportError:
133+
return 'latest'
134+
135+
126136
def infinite_generator():
127137
i = 0
128138
while True:

tests/test_build.py

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

33
import pytest
44

5-
from pydantic_core import SchemaError, SchemaValidator, __version__
5+
from pydantic_core import SchemaError, SchemaValidator
66
from pydantic_core import core_schema as cs
77

88

@@ -32,13 +32,13 @@ def test_schema_as_string():
3232
assert v.validate_python('tRuE') is True
3333

3434

35-
def test_schema_wrong_type():
35+
def test_schema_wrong_type(pydantic_version):
3636
with pytest.raises(SchemaError) as exc_info:
3737
SchemaValidator(1)
3838
assert str(exc_info.value) == (
3939
'Invalid Schema:\n Input should be a valid dictionary or object to'
4040
' extract fields from [type=model_attributes_type, input_value=1, input_type=int]\n'
41-
f' For further information visit https://errors.pydantic.dev/{__version__}/v/model_attributes_type'
41+
f' For further information visit https://errors.pydantic.dev/{pydantic_version}/v/model_attributes_type'
4242
)
4343
assert exc_info.value.errors() == [
4444
{

tests/test_errors.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
PydanticOmit,
1111
SchemaValidator,
1212
ValidationError,
13-
__version__,
1413
core_schema,
1514
)
1615
from pydantic_core._pydantic_core import list_all_errors
@@ -413,7 +412,7 @@ def __repr__(self):
413412
raise RuntimeError('bad repr')
414413

415414

416-
def test_error_on_repr():
415+
def test_error_on_repr(pydantic_version):
417416
s = SchemaValidator({'type': 'int'})
418417
with pytest.raises(ValidationError) as exc_info:
419418
s.validate_python(BadRepr())
@@ -422,7 +421,7 @@ def test_error_on_repr():
422421
'1 validation error for int\n'
423422
' Input should be a valid integer '
424423
'[type=int_type, input_value=<unprintable BadRepr object>, input_type=BadRepr]\n'
425-
f' For further information visit https://errors.pydantic.dev/{__version__}/v/int_type'
424+
f' For further information visit https://errors.pydantic.dev/{pydantic_version}/v/int_type'
426425
)
427426
assert exc_info.value.errors(include_url=False) == [
428427
{'type': 'int_type', 'loc': (), 'msg': 'Input should be a valid integer', 'input': IsInstance(BadRepr)}
@@ -439,7 +438,7 @@ def test_error_on_repr():
439438
)
440439

441440

442-
def test_error_json():
441+
def test_error_json(pydantic_version):
443442
s = SchemaValidator({'type': 'str', 'min_length': 3})
444443
with pytest.raises(ValidationError) as exc_info:
445444
s.validate_python('12')
@@ -462,7 +461,7 @@ def test_error_json():
462461
'msg': 'String should have at least 3 characters',
463462
'input': '12',
464463
'ctx': {'min_length': 3},
465-
'url': f'https://errors.pydantic.dev/{__version__}/v/string_too_short',
464+
'url': f'https://errors.pydantic.dev/{pydantic_version}/v/string_too_short',
466465
}
467466
]
468467
)
@@ -620,7 +619,7 @@ def test_raise_validation_error_custom():
620619
]
621620

622621

623-
def test_loc_with_dots():
622+
def test_loc_with_dots(pydantic_version):
624623
v = SchemaValidator(
625624
core_schema.typed_dict_schema(
626625
{
@@ -649,5 +648,5 @@ def test_loc_with_dots():
649648
"`foo.bar`.0\n"
650649
" Input should be a valid integer, unable to parse string as an integer "
651650
"[type=int_parsing, input_value='x', input_type=str]\n"
652-
f' For further information visit https://errors.pydantic.dev/{__version__}/v/int_parsing'
651+
f' For further information visit https://errors.pydantic.dev/{pydantic_version}/v/int_parsing'
653652
)

tests/test_misc.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def test_schema_error():
4141
assert repr(err) == 'SchemaError("test")'
4242

4343

44-
def test_validation_error():
44+
def test_validation_error(pydantic_version):
4545
v = SchemaValidator({'type': 'int'})
4646
with pytest.raises(ValidationError) as exc_info:
4747
v.validate_python(1.5)
@@ -67,7 +67,7 @@ def test_validation_error():
6767
'loc': (),
6868
'msg': 'Input should be a valid integer, got a number with a fractional part',
6969
'input': 1.5,
70-
'url': f'https://errors.pydantic.dev/{__version__}/v/int_from_float',
70+
'url': f'https://errors.pydantic.dev/{pydantic_version}/v/int_from_float',
7171
}
7272
]
7373

@@ -108,7 +108,7 @@ def test_custom_title():
108108
assert exc_info.value.title == 'MyInt'
109109

110110

111-
def test_validation_error_multiple():
111+
def test_validation_error_multiple(pydantic_version):
112112
class MyModel:
113113
# this is not required, but it avoids `__pydantic_fields_set__` being included in `__dict__`
114114
__slots__ = '__dict__', '__pydantic_fields_set__', '__pydantic_extra__', '__pydantic_private__'
@@ -152,11 +152,11 @@ class MyModel:
152152
'x\n'
153153
' Input should be a valid number, unable to parse string as a number '
154154
"[type=float_parsing, input_value='xxxxxxxxxxxxxxxxxxxxxxxx...xxxxxxxxxxxxxxxxxxxxxxx', input_type=str]\n"
155-
f' For further information visit https://errors.pydantic.dev/{__version__}/v/float_parsing\n'
155+
f' For further information visit https://errors.pydantic.dev/{pydantic_version}/v/float_parsing\n'
156156
'y\n'
157157
' Input should be a valid integer, unable to parse string as an integer '
158158
"[type=int_parsing, input_value='y', input_type=str]\n"
159-
f' For further information visit https://errors.pydantic.dev/{__version__}/v/int_parsing'
159+
f' For further information visit https://errors.pydantic.dev/{pydantic_version}/v/int_parsing'
160160
)
161161

162162

tests/validators/test_arguments.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import pytest
88

9-
from pydantic_core import ArgsKwargs, SchemaError, SchemaValidator, ValidationError, __version__, core_schema
9+
from pydantic_core import ArgsKwargs, SchemaError, SchemaValidator, ValidationError, core_schema
1010

1111
from ..conftest import Err, PyAndJson, plain_repr
1212

@@ -984,7 +984,7 @@ def test_invalid_schema():
984984
)
985985

986986

987-
def test_error_display():
987+
def test_error_display(pydantic_version):
988988
v = SchemaValidator(
989989
core_schema.arguments_schema(
990990
[
@@ -1013,7 +1013,7 @@ def test_error_display():
10131013
"b\n"
10141014
" Missing required argument [type=missing_argument, "
10151015
"input_value=ArgsKwargs((), {'a': 1}), input_type=ArgsKwargs]\n"
1016-
f" For further information visit https://errors.pydantic.dev/{__version__}/v/missing_argument"
1016+
f" For further information visit https://errors.pydantic.dev/{pydantic_version}/v/missing_argument"
10171017
)
10181018
# insert_assert(exc_info.value.json(include_url=False))
10191019
assert exc_info.value.json(include_url=False) == (

tests/validators/test_bool.py

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

33
import pytest
44

5-
from pydantic_core import SchemaValidator, ValidationError, __version__, core_schema
5+
from pydantic_core import SchemaValidator, ValidationError, core_schema
66

77
from ..conftest import Err, PyAndJson, plain_repr
88

@@ -53,7 +53,7 @@ def test_bool_strict(py_and_json: PyAndJson):
5353
v.validate_test('true')
5454

5555

56-
def test_bool_error():
56+
def test_bool_error(pydantic_version):
5757
v = SchemaValidator({'type': 'bool'})
5858

5959
with pytest.raises(ValidationError) as exc_info:
@@ -63,7 +63,7 @@ def test_bool_error():
6363
'1 validation error for bool\n'
6464
' Input should be a valid boolean, '
6565
"unable to interpret input [type=bool_parsing, input_value='wrong', input_type=str]\n"
66-
f" For further information visit https://errors.pydantic.dev/{__version__}/v/bool_parsing"
66+
f" For further information visit https://errors.pydantic.dev/{pydantic_version}/v/bool_parsing"
6767
)
6868
assert exc_info.value.errors(include_url=False) == [
6969
{

tests/validators/test_definitions_recursive.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from dirty_equals import AnyThing, HasAttributes, IsList, IsPartialDict, IsStr, IsTuple
77

88
import pydantic_core
9-
from pydantic_core import SchemaError, SchemaValidator, ValidationError, __version__, core_schema
9+
from pydantic_core import SchemaError, SchemaValidator, ValidationError, core_schema
1010

1111
from ..conftest import Err, plain_repr
1212
from .test_typed_dict import Cls
@@ -818,7 +818,7 @@ def test_error_inside_definition_wrapper():
818818
)
819819

820820

821-
def test_recursive_definitions_schema() -> None:
821+
def test_recursive_definitions_schema(pydantic_version) -> None:
822822
s = core_schema.definitions_schema(
823823
core_schema.definition_reference_schema(schema_ref='a'),
824824
[
@@ -854,7 +854,7 @@ def test_recursive_definitions_schema() -> None:
854854
'loc': ('b', 0, 'a'),
855855
'msg': 'Input should be a valid list',
856856
'input': {},
857-
'url': f'https://errors.pydantic.dev/{__version__}/v/list_type',
857+
'url': f'https://errors.pydantic.dev/{pydantic_version}/v/list_type',
858858
}
859859
]
860860

@@ -878,7 +878,7 @@ def test_unsorted_definitions_schema() -> None:
878878
v.validate_python({'x': 'abc'})
879879

880880

881-
def test_validate_assignment() -> None:
881+
def test_validate_assignment(pydantic_version) -> None:
882882
@dataclass
883883
class Model:
884884
x: List['Model']
@@ -916,6 +916,6 @@ class Model:
916916
'msg': 'Input should be a dictionary or an instance of Model',
917917
'input': 123,
918918
'ctx': {'class_name': 'Model'},
919-
'url': f'https://errors.pydantic.dev/{__version__}/v/dataclass_type',
919+
'url': f'https://errors.pydantic.dev/{pydantic_version}/v/dataclass_type',
920920
}
921921
]

tests/validators/test_int.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pytest
77
from dirty_equals import IsStr
88

9-
from pydantic_core import SchemaValidator, ValidationError, __version__
9+
from pydantic_core import SchemaValidator, ValidationError
1010

1111
from ..conftest import Err, PyAndJson, plain_repr
1212

@@ -340,7 +340,7 @@ def test_int_repr():
340340
assert plain_repr(v).startswith('SchemaValidator(title="constrained-int",validator=ConstrainedInt(')
341341

342342

343-
def test_too_long():
343+
def test_too_long(pydantic_version):
344344
v = SchemaValidator({'type': 'int'})
345345

346346
with pytest.raises(ValidationError) as exc_info:
@@ -359,7 +359,7 @@ def test_too_long():
359359
"1 validation error for int\n"
360360
" Unable to parse input string as an integer, exceeded maximum size "
361361
"[type=int_parsing_size, input_value='111111111111111111111111...11111111111111111111111', input_type=str]\n"
362-
f" For further information visit https://errors.pydantic.dev/{__version__}/v/int_parsing_size"
362+
f" For further information visit https://errors.pydantic.dev/{pydantic_version}/v/int_parsing_size"
363363
)
364364

365365

tests/validators/test_model_fields.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99
from dirty_equals import FunctionCheck, HasRepr, IsStr
1010

11-
from pydantic_core import CoreConfig, SchemaError, SchemaValidator, ValidationError, __version__, core_schema
11+
from pydantic_core import CoreConfig, SchemaError, SchemaValidator, ValidationError, core_schema
1212

1313
from ..conftest import Err, PyAndJson
1414

@@ -105,7 +105,7 @@ def test_with_default():
105105
)
106106

107107

108-
def test_missing_error():
108+
def test_missing_error(pydantic_version):
109109
v = SchemaValidator(
110110
{
111111
'type': 'model-fields',
@@ -123,7 +123,7 @@ def test_missing_error():
123123
1 validation error for model-fields
124124
field_b
125125
Field required [type=missing, input_value={{'field_a': b'abc'}}, input_type=dict]
126-
For further information visit https://errors.pydantic.dev/{__version__}/v/missing"""
126+
For further information visit https://errors.pydantic.dev/{pydantic_version}/v/missing"""
127127
)
128128

129129

0 commit comments

Comments
 (0)