From b6f8aec0d487ec53bd5ad2f502490159be04f6cd Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 12 Dec 2023 08:33:56 +0200 Subject: [PATCH] Expose API for setting the global "show errors in reprs" flag --- python/pydantic_core/__init__.py | 2 ++ python/pydantic_core/_pydantic_core.pyi | 10 ++++++++++ src/errors/mod.rs | 2 +- src/errors/validation_exception.rs | 24 ++++++++++++++++-------- src/lib.rs | 4 +++- tests/test_errors.py | 12 ++++++++++++ 6 files changed, 44 insertions(+), 10 deletions(-) diff --git a/python/pydantic_core/__init__.py b/python/pydantic_core/__init__.py index 5b2655c91..a867716aa 100644 --- a/python/pydantic_core/__init__.py +++ b/python/pydantic_core/__init__.py @@ -23,6 +23,7 @@ ValidationError, __version__, from_json, + set_errors_include_url, to_json, to_jsonable_python, validate_core_schema, @@ -65,6 +66,7 @@ 'TzInfo', 'to_json', 'from_json', + 'set_errors_include_url', 'to_jsonable_python', 'validate_core_schema', ] diff --git a/python/pydantic_core/_pydantic_core.pyi b/python/pydantic_core/_pydantic_core.pyi index 382a6c804..b5391dce0 100644 --- a/python/pydantic_core/_pydantic_core.pyi +++ b/python/pydantic_core/_pydantic_core.pyi @@ -849,6 +849,16 @@ def list_all_errors() -> list[ErrorTypeInfo]: Returns: A list of `ErrorTypeInfo` typed dicts. """ + +def set_errors_include_url(flag: bool) -> None: + """ + Set whether `repr`s of errors should include URLs to documentation on the error. + + Defaults to `true` unless the `PYDANTIC_ERRORS_OMIT_URL` is set. + + Args: + flag: Whether to include URLs in error `repr`s. + """ @final class TzInfo(datetime.tzinfo): def tzname(self, _dt: datetime.datetime | None) -> str | None: ... diff --git a/src/errors/mod.rs b/src/errors/mod.rs index 131e54177..7971545bb 100644 --- a/src/errors/mod.rs +++ b/src/errors/mod.rs @@ -9,7 +9,7 @@ mod value_exception; pub use self::line_error::{AsErrorValue, InputValue, ValError, ValLineError, ValResult}; pub use self::location::{AsLocItem, LocItem}; pub use self::types::{list_all_errors, ErrorType, ErrorTypeDefaults, Number}; -pub use self::validation_exception::ValidationError; +pub use self::validation_exception::{set_errors_include_url, ValidationError}; pub use self::value_exception::{PydanticCustomError, PydanticKnownError, PydanticOmit, PydanticUseDefault}; pub fn py_err_string(py: Python, err: PyErr) -> String { diff --git a/src/errors/validation_exception.rs b/src/errors/validation_exception.rs index 91ef60a0d..dcb0ea793 100644 --- a/src/errors/validation_exception.rs +++ b/src/errors/validation_exception.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::fmt; use std::fmt::{Display, Write}; use std::str::from_utf8; @@ -5,8 +6,8 @@ use std::str::from_utf8; use pyo3::exceptions::{PyKeyError, PyTypeError, PyValueError}; use pyo3::ffi; use pyo3::intern; -use pyo3::once_cell::GILOnceCell; use pyo3::prelude::*; +use pyo3::sync::{GILOnceCell, GILProtected}; use pyo3::types::{PyDict, PyList, PyString}; use serde::ser::{Error, SerializeMap, SerializeSeq}; use serde::{Serialize, Serializer}; @@ -85,7 +86,7 @@ impl ValidationError { } pub fn display(&self, py: Python, prefix_override: Option<&'static str>, hide_input: bool) -> String { - let url_prefix = get_url_prefix(py, include_url_env(py)); + let url_prefix = get_url_prefix(py, include_url(py)); let line_errors = pretty_py_line_errors(py, self.input_type, self.line_errors.iter(), url_prefix, hide_input); if let Some(prefix) = prefix_override { format!("{prefix}\n{line_errors}") @@ -191,17 +192,24 @@ impl ValidationError { } } -static URL_ENV_VAR: GILOnceCell = GILOnceCell::new(); +static INCLUDE_URL_FLAG: GILProtected>> = GILProtected::new(RefCell::new(None)); -fn _get_include_url_env() -> bool { - match std::env::var("PYDANTIC_ERRORS_OMIT_URL") { +fn include_url(py: Python) -> bool { + if let Some(flag) = *INCLUDE_URL_FLAG.get(py).borrow() { + return flag; + } + // If uninitialized, initialize from environment variable. + let flag = match std::env::var("PYDANTIC_ERRORS_OMIT_URL") { Ok(val) => val.is_empty(), Err(_) => true, - } + }; + INCLUDE_URL_FLAG.get(py).borrow_mut().replace(flag); + flag } -fn include_url_env(py: Python) -> bool { - *URL_ENV_VAR.get_or_init(py, _get_include_url_env) +#[pyfunction] +pub fn set_errors_include_url(py: Python, flag: bool) { + INCLUDE_URL_FLAG.get(py).borrow_mut().replace(flag); } static URL_PREFIX: GILOnceCell = GILOnceCell::new(); diff --git a/src/lib.rs b/src/lib.rs index de4a6d9bd..97b062009 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,7 +29,8 @@ pub use self::url::{PyMultiHostUrl, PyUrl}; pub use argument_markers::{ArgsKwargs, PydanticUndefinedType}; pub use build_tools::SchemaError; pub use errors::{ - list_all_errors, PydanticCustomError, PydanticKnownError, PydanticOmit, PydanticUseDefault, ValidationError, + list_all_errors, set_errors_include_url, PydanticCustomError, PydanticKnownError, PydanticOmit, PydanticUseDefault, + ValidationError, }; pub use serializers::{ to_json, to_jsonable_python, PydanticSerializationError, PydanticSerializationUnexpectedValue, SchemaSerializer, @@ -110,6 +111,7 @@ fn _pydantic_core(py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(from_json, m)?)?; m.add_function(wrap_pyfunction!(to_jsonable_python, m)?)?; m.add_function(wrap_pyfunction!(list_all_errors, m)?)?; + m.add_function(wrap_pyfunction!(set_errors_include_url, m)?)?; m.add_function(wrap_pyfunction!(validate_core_schema, m)?)?; Ok(()) } diff --git a/tests/test_errors.py b/tests/test_errors.py index 05815aec5..5732fd847 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -16,6 +16,7 @@ SchemaValidator, ValidationError, core_schema, + set_errors_include_url, ) from pydantic_core._pydantic_core import list_all_errors @@ -1074,3 +1075,14 @@ def test_hide_input_in_json() -> None: for error in exc_info.value.errors(include_input=False): assert 'input' not in error + + +def test_hide_url_in_repr() -> None: + s = SchemaValidator({'type': 'int'}) + with pytest.raises(ValidationError) as exc_info: + s.validate_python('definitely not an int') + + set_errors_include_url(False) + assert 'https' not in repr(exc_info.value) + set_errors_include_url(True) + assert 'https' in repr(exc_info.value)