Skip to content

Commit

Permalink
Rework the PYDANTIC_ERRORS_INCLUDE_URL environment variable and doc…
Browse files Browse the repository at this point in the history
…ument it

Refs pydantic#1118 (comment)
  • Loading branch information
akx committed Dec 22, 2023
1 parent bec63db commit ea60189
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 8 deletions.
12 changes: 12 additions & 0 deletions python/pydantic_core/_pydantic_core.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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__(
Expand Down
32 changes: 24 additions & 8 deletions src/errors/validation_exception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,31 @@ impl ValidationError {

static URL_ENV_VAR: GILOnceCell<bool> = 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::<pyo3::exceptions::PyDeprecationWarning>(),
"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<String> = GILOnceCell::new();
Expand Down
50 changes: 50 additions & 0 deletions tests/test_errors.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

0 comments on commit ea60189

Please sign in to comment.