From ea601890a935a5b87ab8534d1716f9dbbe135bd0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Fri, 15 Dec 2023 08:26:01 +0200 Subject: [PATCH] Rework the `PYDANTIC_ERRORS_INCLUDE_URL` environment variable and document it Refs https://github.com/pydantic/pydantic-core/pull/1118#issuecomment-1854040572 --- python/pydantic_core/_pydantic_core.pyi | 12 ++++++ src/errors/validation_exception.rs | 32 ++++++++++++---- tests/test_errors.py | 50 +++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/python/pydantic_core/_pydantic_core.pyi b/python/pydantic_core/_pydantic_core.pyi index 382a6c804..b8c1f4e94 100644 --- a/python/pydantic_core/_pydantic_core.pyi +++ b/python/pydantic_core/_pydantic_core.pyi @@ -787,6 +787,18 @@ class ValidationError(ValueError): a JSON string. """ + def __repr__(self) -> str: + """ + A string representation of the validation error. + + Whether or not documentation URLs are included in the repr is controlled by the + environment variable `PYDANTIC_ERRORS_INCLUDE_URL` being set to `1` or + `true`; by default, URLs are shown. + + Due to implementation details, this environment variable can only be set once, + before the first validation error is created. + """ + @final class PydanticCustomError(ValueError): def __new__( diff --git a/src/errors/validation_exception.rs b/src/errors/validation_exception.rs index e77b21974..95090c1ac 100644 --- a/src/errors/validation_exception.rs +++ b/src/errors/validation_exception.rs @@ -193,15 +193,31 @@ impl ValidationError { static URL_ENV_VAR: GILOnceCell = GILOnceCell::new(); -fn _get_include_url_env() -> bool { - match std::env::var("PYDANTIC_ERRORS_OMIT_URL") { - Ok(val) => val.is_empty(), - Err(_) => true, - } -} - fn include_url_env(py: Python) -> bool { - *URL_ENV_VAR.get_or_init(py, _get_include_url_env) + *URL_ENV_VAR.get_or_init(py, || { + // Check the legacy env var first. + // Using `var_os` here instead of `var` because we don't care about + // the value (or whether we're able to decode it as UTF-8), just + // whether it exists (and if it does, whether it's non-empty). + match std::env::var_os("PYDANTIC_ERRORS_OMIT_URL") { + Some(val) => { + // We don't care whether warning succeeded or not, hence the assignment + let _ = PyErr::warn( + py, + py.get_type::(), + "PYDANTIC_ERRORS_OMIT_URL is deprecated, use PYDANTIC_ERRORS_INCLUDE_URL instead", + 1, + ); + // If OMIT_URL exists but is empty, we include the URL: + val.is_empty() + } + // If the legacy env var doesn't exist, check the documented one: + None => match std::env::var("PYDANTIC_ERRORS_INCLUDE_URL") { + Ok(val) => val == "1" || val.to_lowercase() == "true", + Err(_) => true, + }, + } + }) } static URL_PREFIX: GILOnceCell = GILOnceCell::new(); diff --git a/tests/test_errors.py b/tests/test_errors.py index 88dcace8f..94d268ef2 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,6 +1,8 @@ import enum +import os import pickle import re +import subprocess import sys from decimal import Decimal from typing import Any, Optional @@ -1089,3 +1091,51 @@ def test_validation_error_pickle() -> None: original = exc_info.value roundtripped = pickle.loads(pickle.dumps(original)) assert original.errors() == roundtripped.errors() + + +@pytest.mark.skipif('PYDANTIC_ERRORS_INCLUDE_URL' in os.environ, reason="can't test when envvar is set") +def test_errors_include_url() -> None: + s = SchemaValidator({'type': 'int'}) + with pytest.raises(ValidationError) as exc_info: + s.validate_python('definitely not an int') + assert 'https://errors.pydantic.dev' in repr(exc_info.value) + + +@pytest.mark.skipif(sys.platform == 'emscripten', reason='no subprocesses on emscripten') +@pytest.mark.parametrize( + ('env_var', 'env_var_value', 'expected_to_have_url'), + [ + ('PYDANTIC_ERRORS_INCLUDE_URL', None, True), + ('PYDANTIC_ERRORS_INCLUDE_URL', '1', True), + ('PYDANTIC_ERRORS_INCLUDE_URL', 'True', True), + ('PYDANTIC_ERRORS_INCLUDE_URL', 'no', False), + ('PYDANTIC_ERRORS_INCLUDE_URL', '0', False), + # Legacy environment variable, will raise a deprecation warning: + ('PYDANTIC_ERRORS_OMIT_URL', '1', False), + ('PYDANTIC_ERRORS_OMIT_URL', None, True), + ], +) +def test_errors_include_url_envvar(env_var, env_var_value, expected_to_have_url) -> None: + """ + Test the `PYDANTIC_ERRORS_INCLUDE_URL` environment variable. + + Since it can only be set before `ValidationError.__repr__()` is first called, + we need to spawn a subprocess to test it. + """ + code = "import pydantic_core; pydantic_core.SchemaValidator({'type': 'int'}).validate_python('ooo')" + env = os.environ.copy() + env.pop('PYDANTIC_ERRORS_OMIT_URL', None) # in case the ambient environment has it set + if env_var_value is not None: + env[env_var] = env_var_value + env['PYTHONDEVMODE'] = '1' # required to surface the deprecation warning + result = subprocess.run( + [sys.executable, '-c', code], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + encoding='utf-8', + env=env, + ) + assert result.returncode == 1 + if 'PYDANTIC_ERRORS_OMIT_URL' in env: + assert 'PYDANTIC_ERRORS_OMIT_URL is deprecated' in result.stdout + assert ('https://errors.pydantic.dev' in result.stdout) == expected_to_have_url