Skip to content

Commit 726ef5f

Browse files
authored
Add hide_input to ValidationError (#633)
1 parent 26fa27d commit 726ef5f

File tree

11 files changed

+253
-25
lines changed

11 files changed

+253
-25
lines changed

pydantic_core/_pydantic_core.pyi

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@ class SchemaError(Exception):
173173
class ValidationError(ValueError):
174174
@staticmethod
175175
def from_exception_data(
176-
title: str, errors: 'list[InitErrorDetails]', error_mode: Literal['python', 'json'] = 'python'
176+
title: str,
177+
errors: 'list[InitErrorDetails]',
178+
error_mode: Literal['python', 'json'] = 'python',
179+
hide_input_in_errors: bool = False,
177180
) -> ValidationError:
178181
"""
179182
Provisory constructor for a Validation Error.

pydantic_core/core_schema.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ class CoreConfig(TypedDict, total=False):
5151
# the config options are used to customise serialization to JSON
5252
ser_json_timedelta: Literal['iso8601', 'float'] # default: 'iso8601'
5353
ser_json_bytes: Literal['utf8', 'base64'] # default: 'utf8'
54+
# used to hide input data from ValidationError repr
55+
hide_input_in_errors: bool
5456

5557

5658
IncExCall: TypeAlias = 'set[int | str] | dict[int | str, IncExCall] | None'

src/build_tools.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,8 @@ impl SchemaError {
140140
match error {
141141
ValError::LineErrors(raw_errors) => {
142142
let line_errors = raw_errors.into_iter().map(|e| e.into_py(py)).collect();
143-
let validation_error = ValidationError::new(line_errors, "Schema".to_object(py), ErrorMode::Python);
143+
let validation_error =
144+
ValidationError::new(line_errors, "Schema".to_object(py), ErrorMode::Python, false);
144145
let schema_error = SchemaError(SchemaErrorEnum::ValidationError(validation_error));
145146
match Py::new(py, schema_error) {
146147
Ok(err) => PyErr::from_value(err.into_ref(py)),
@@ -177,21 +178,21 @@ impl SchemaError {
177178
fn errors(&self, py: Python) -> PyResult<Py<PyList>> {
178179
match &self.0 {
179180
SchemaErrorEnum::Message(_) => Ok(PyList::empty(py).into_py(py)),
180-
SchemaErrorEnum::ValidationError(error) => error.errors(py, false, true),
181+
SchemaErrorEnum::ValidationError(error) => error.errors(py, false, false),
181182
}
182183
}
183184

184185
fn __str__(&self, py: Python) -> String {
185186
match &self.0 {
186187
SchemaErrorEnum::Message(message) => message.to_owned(),
187-
SchemaErrorEnum::ValidationError(error) => error.display(py, Some("Invalid Schema:")),
188+
SchemaErrorEnum::ValidationError(error) => error.display(py, Some("Invalid Schema:"), false),
188189
}
189190
}
190191

191192
fn __repr__(&self, py: Python) -> String {
192193
match &self.0 {
193194
SchemaErrorEnum::Message(message) => format!("SchemaError({message:?})"),
194-
SchemaErrorEnum::ValidationError(error) => error.display(py, Some("Invalid Schema:")),
195+
SchemaErrorEnum::ValidationError(error) => error.display(py, Some("Invalid Schema:"), false),
195196
}
196197
}
197198
}

src/errors/validation_exception.rs

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,21 @@ pub struct ValidationError {
3131
line_errors: Vec<PyLineError>,
3232
error_mode: ErrorMode,
3333
title: PyObject,
34+
hide_input_in_errors: bool,
3435
}
3536

3637
impl ValidationError {
37-
pub fn new(line_errors: Vec<PyLineError>, title: PyObject, error_mode: ErrorMode) -> Self {
38+
pub fn new(
39+
line_errors: Vec<PyLineError>,
40+
title: PyObject,
41+
error_mode: ErrorMode,
42+
hide_input_in_errors: bool,
43+
) -> Self {
3844
Self {
3945
line_errors,
4046
title,
4147
error_mode,
48+
hide_input_in_errors,
4249
}
4350
}
4451

@@ -48,6 +55,7 @@ impl ValidationError {
4855
error_mode: ErrorMode,
4956
error: ValError,
5057
outer_location: Option<LocItem>,
58+
hide_input_in_errors: bool,
5159
) -> PyErr {
5260
match error {
5361
ValError::LineErrors(raw_errors) => {
@@ -58,7 +66,7 @@ impl ValidationError {
5866
.collect(),
5967
None => raw_errors.into_iter().map(|e| e.into_py(py)).collect(),
6068
};
61-
let validation_error = Self::new(line_errors, title, error_mode);
69+
let validation_error = Self::new(line_errors, title, error_mode, hide_input_in_errors);
6270
match Py::new(py, validation_error) {
6371
Ok(err) => PyErr::from_value(err.into_ref(py)),
6472
Err(err) => err,
@@ -69,9 +77,15 @@ impl ValidationError {
6977
}
7078
}
7179

72-
pub fn display(&self, py: Python, prefix_override: Option<&'static str>) -> String {
80+
pub fn display(&self, py: Python, prefix_override: Option<&'static str>, hide_input_in_errors: bool) -> String {
7381
let url_prefix = get_url_prefix(py, include_url_env(py));
74-
let line_errors = pretty_py_line_errors(py, &self.error_mode, self.line_errors.iter(), url_prefix);
82+
let line_errors = pretty_py_line_errors(
83+
py,
84+
&self.error_mode,
85+
self.line_errors.iter(),
86+
url_prefix,
87+
hide_input_in_errors,
88+
);
7589
if let Some(prefix) = prefix_override {
7690
format!("{prefix}\n{line_errors}")
7791
} else {
@@ -124,18 +138,21 @@ impl<'a> IntoPy<ValError<'a>> for ValidationError {
124138
#[pymethods]
125139
impl ValidationError {
126140
#[staticmethod]
141+
#[pyo3(signature = (title, line_errors, error_mode=None, hide_input_in_errors=false))]
127142
fn from_exception_data(
128143
py: Python,
129144
title: PyObject,
130145
line_errors: &PyList,
131146
error_mode: Option<&str>,
147+
hide_input_in_errors: bool,
132148
) -> PyResult<Py<Self>> {
133149
Py::new(
134150
py,
135151
Self {
136152
line_errors: line_errors.iter().map(PyLineError::try_from).collect::<PyResult<_>>()?,
137153
title,
138154
error_mode: ErrorMode::try_from(error_mode)?,
155+
hide_input_in_errors,
139156
},
140157
)
141158
}
@@ -210,7 +227,7 @@ impl ValidationError {
210227
}
211228

212229
fn __repr__(&self, py: Python) -> String {
213-
self.display(py, None)
230+
self.display(py, None, self.hide_input_in_errors)
214231
}
215232

216233
fn __str__(&self, py: Python) -> String {
@@ -238,9 +255,10 @@ pub fn pretty_py_line_errors<'a>(
238255
error_mode: &ErrorMode,
239256
line_errors_iter: impl Iterator<Item = &'a PyLineError>,
240257
url_prefix: Option<&str>,
258+
hide_input_in_errors: bool,
241259
) -> String {
242260
line_errors_iter
243-
.map(|i| i.pretty(py, error_mode, url_prefix))
261+
.map(|i| i.pretty(py, error_mode, url_prefix, hide_input_in_errors))
244262
.collect::<Result<Vec<_>, _>>()
245263
.unwrap_or_else(|err| vec![format!("[error formatting line errors: {err}]")])
246264
.join("\n")
@@ -349,7 +367,13 @@ impl PyLineError {
349367
Ok(dict.into_py(py))
350368
}
351369

352-
fn pretty(&self, py: Python, error_mode: &ErrorMode, url_prefix: Option<&str>) -> Result<String, fmt::Error> {
370+
fn pretty(
371+
&self,
372+
py: Python,
373+
error_mode: &ErrorMode,
374+
url_prefix: Option<&str>,
375+
hide_input_in_errors: bool,
376+
) -> Result<String, fmt::Error> {
353377
let mut output = String::with_capacity(200);
354378
write!(output, "{}", self.location)?;
355379

@@ -359,12 +383,14 @@ impl PyLineError {
359383
};
360384
write!(output, " {message} [type={}", self.error_type.type_string())?;
361385

362-
let input_value = self.input_value.as_ref(py);
363-
let input_str = safe_repr(input_value);
364-
truncate_input_value!(output, input_str);
386+
if !hide_input_in_errors {
387+
let input_value = self.input_value.as_ref(py);
388+
let input_str = safe_repr(input_value);
389+
truncate_input_value!(output, input_str);
365390

366-
if let Ok(type_) = input_value.get_type().name() {
367-
write!(output, ", input_type={type_}")?;
391+
if let Ok(type_) = input_value.get_type().name() {
392+
write!(output, ", input_type={type_}")?;
393+
}
368394
}
369395
if let Some(url_prefix) = url_prefix {
370396
match self.error_type {

src/validators/function.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,9 +262,36 @@ pub struct FunctionWrapValidator {
262262
name: String,
263263
is_field_validator: bool,
264264
info_arg: bool,
265+
hide_input_in_errors: bool,
265266
}
266267

267-
impl_build!(FunctionWrapValidator, "function-wrap");
268+
impl BuildValidator for FunctionWrapValidator {
269+
const EXPECTED_TYPE: &'static str = "function-wrap";
270+
271+
fn build(
272+
schema: &PyDict,
273+
config: Option<&PyDict>,
274+
definitions: &mut DefinitionsBuilder<CombinedValidator>,
275+
) -> PyResult<CombinedValidator> {
276+
let py = schema.py();
277+
let validator = build_validator(schema.get_as_req(intern!(py, "schema"))?, config, definitions)?;
278+
let (is_field_validator, info_arg, function) = destructure_function_schema(schema)?;
279+
let hide_input_in_errors: bool = config.get_as(intern!(py, "hide_input_in_errors"))?.unwrap_or(false);
280+
Ok(Self {
281+
validator: Box::new(validator),
282+
func: function.into_py(py),
283+
config: match config {
284+
Some(c) => c.into(),
285+
None => py.None(),
286+
},
287+
name: format!("function-wrap[{}()]", function_name(function)?),
288+
is_field_validator,
289+
info_arg,
290+
hide_input_in_errors,
291+
}
292+
.into())
293+
}
294+
}
268295

269296
impl FunctionWrapValidator {
270297
fn _validate<'s, 'data>(
@@ -301,6 +328,7 @@ impl Validator for FunctionWrapValidator {
301328
definitions,
302329
extra,
303330
recursion_guard,
331+
self.hide_input_in_errors,
304332
),
305333
};
306334
self._validate(
@@ -329,6 +357,7 @@ impl Validator for FunctionWrapValidator {
329357
definitions,
330358
extra,
331359
recursion_guard,
360+
self.hide_input_in_errors,
332361
),
333362
updated_field_name: field_name.to_string(),
334363
updated_field_value: field_value.to_object(py),

src/validators/generator.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub struct GeneratorValidator {
1818
min_length: Option<usize>,
1919
max_length: Option<usize>,
2020
name: String,
21+
hide_input_in_errors: bool,
2122
}
2223

2324
impl BuildValidator for GeneratorValidator {
@@ -33,11 +34,15 @@ impl BuildValidator for GeneratorValidator {
3334
Some(ref v) => format!("{}[{}]", Self::EXPECTED_TYPE, v.get_name()),
3435
None => format!("{}[any]", Self::EXPECTED_TYPE),
3536
};
37+
let hide_input_in_errors: bool = config
38+
.get_as(pyo3::intern!(schema.py(), "hide_input_in_errors"))?
39+
.unwrap_or(false);
3640
Ok(Self {
3741
item_validator,
3842
name,
3943
min_length: schema.get_as(pyo3::intern!(schema.py(), "min_length"))?,
4044
max_length: schema.get_as(pyo3::intern!(schema.py(), "max_length"))?,
45+
hide_input_in_errors,
4146
}
4247
.into())
4348
}
@@ -53,16 +58,24 @@ impl Validator for GeneratorValidator {
5358
recursion_guard: &'s mut RecursionGuard,
5459
) -> ValResult<'data, PyObject> {
5560
let iterator = input.validate_iter()?;
56-
let validator = self
57-
.item_validator
58-
.as_ref()
59-
.map(|v| InternalValidator::new(py, "ValidatorIterator", v, definitions, extra, recursion_guard));
61+
let validator = self.item_validator.as_ref().map(|v| {
62+
InternalValidator::new(
63+
py,
64+
"ValidatorIterator",
65+
v,
66+
definitions,
67+
extra,
68+
recursion_guard,
69+
self.hide_input_in_errors,
70+
)
71+
});
6072

6173
let v_iterator = ValidatorIterator {
6274
iterator,
6375
validator,
6476
min_length: self.min_length,
6577
max_length: self.max_length,
78+
hide_input_in_errors: self.hide_input_in_errors,
6679
};
6780
Ok(v_iterator.into_py(py))
6881
}
@@ -98,6 +111,7 @@ struct ValidatorIterator {
98111
validator: Option<InternalValidator>,
99112
min_length: Option<usize>,
100113
max_length: Option<usize>,
114+
hide_input_in_errors: bool,
101115
}
102116

103117
#[pymethods]
@@ -109,6 +123,7 @@ impl ValidatorIterator {
109123
fn __next__(mut slf: PyRefMut<'_, Self>, py: Python) -> PyResult<Option<PyObject>> {
110124
let min_length = slf.min_length;
111125
let max_length = slf.max_length;
126+
let hide_input_in_errors = slf.hide_input_in_errors;
112127
let Self {
113128
validator, iterator, ..
114129
} = &mut *slf;
@@ -133,6 +148,7 @@ impl ValidatorIterator {
133148
ErrorMode::Python,
134149
val_error,
135150
None,
151+
hide_input_in_errors,
136152
));
137153
}
138154
}
@@ -157,6 +173,7 @@ impl ValidatorIterator {
157173
ErrorMode::Python,
158174
val_error,
159175
None,
176+
hide_input_in_errors,
160177
));
161178
}
162179
}
@@ -203,6 +220,7 @@ pub struct InternalValidator {
203220
self_instance: Option<PyObject>,
204221
recursion_guard: RecursionGuard,
205222
validation_mode: InputType,
223+
hide_input_in_errors: bool,
206224
}
207225

208226
impl fmt::Debug for InternalValidator {
@@ -219,6 +237,7 @@ impl InternalValidator {
219237
definitions: &[CombinedValidator],
220238
extra: &Extra,
221239
recursion_guard: &RecursionGuard,
240+
hide_input_in_errors: bool,
222241
) -> Self {
223242
Self {
224243
name: name.to_string(),
@@ -230,6 +249,7 @@ impl InternalValidator {
230249
self_instance: extra.self_instance.map(|d| d.into_py(py)),
231250
recursion_guard: recursion_guard.clone(),
232251
validation_mode: extra.mode,
252+
hide_input_in_errors,
233253
}
234254
}
235255

@@ -261,7 +281,14 @@ impl InternalValidator {
261281
&mut self.recursion_guard,
262282
)
263283
.map_err(|e| {
264-
ValidationError::from_val_error(py, self.name.to_object(py), ErrorMode::Python, e, outer_location)
284+
ValidationError::from_val_error(
285+
py,
286+
self.name.to_object(py),
287+
ErrorMode::Python,
288+
e,
289+
outer_location,
290+
self.hide_input_in_errors,
291+
)
265292
})
266293
}
267294

@@ -286,7 +313,14 @@ impl InternalValidator {
286313
self.validator
287314
.validate(py, input, &extra, &self.definitions, &mut self.recursion_guard)
288315
.map_err(|e| {
289-
ValidationError::from_val_error(py, self.name.to_object(py), ErrorMode::Python, e, outer_location)
316+
ValidationError::from_val_error(
317+
py,
318+
self.name.to_object(py),
319+
ErrorMode::Python,
320+
e,
321+
outer_location,
322+
self.hide_input_in_errors,
323+
)
290324
})
291325
}
292326
}

0 commit comments

Comments
 (0)