Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework PYDANTIC_ERRORS_OMIT_URL to PYDANTIC_ERRORS_INCLUDE_URL #1123

Merged
merged 1 commit into from Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 12 additions & 0 deletions python/pydantic_core/_pydantic_core.pyi
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
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
@@ -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