Skip to content

Commit 4e94b68

Browse files
bossentiKludexalexmojaki
authored
Raise an exception when Pydantic plugin is enabled on Pydantic <2.5.0 (#160)
Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com> Co-authored-by: Alex Hall <alex.mojaki@gmail.com>
1 parent 8e3b2dd commit 4e94b68

File tree

3 files changed

+85
-54
lines changed

3 files changed

+85
-54
lines changed

logfire/_internal/config.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
from .metrics import ProxyMeterProvider, configure_metrics
7676
from .scrubbing import Scrubber, ScrubCallback
7777
from .tracer import PendingSpanProcessor, ProxyTracerProvider
78-
from .utils import UnexpectedResponse, ensure_data_dir_exists, read_toml_file
78+
from .utils import UnexpectedResponse, ensure_data_dir_exists, get_version, read_toml_file
7979

8080
CREDENTIALS_FILENAME = 'logfire_credentials.json'
8181
"""Default base URL for the Logfire API."""
@@ -394,6 +394,11 @@ def _load_configuration(
394394
# This is particularly for deserializing from a dict as in executors.py
395395
pydantic_plugin = PydanticPlugin(**pydantic_plugin) # type: ignore
396396
self.pydantic_plugin = pydantic_plugin or param_manager.pydantic_plugin
397+
if self.pydantic_plugin.record != 'off':
398+
import pydantic
399+
400+
if get_version(pydantic.__version__) < get_version('2.5.0'): # pragma: no cover
401+
raise RuntimeError('The Pydantic plugin requires Pydantic 2.5.0 or newer.')
397402
self.fast_shutdown = fast_shutdown
398403

399404
self.id_generator = id_generator or RandomIdGenerator()

logfire/_internal/utils.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import json
44
import sys
55
from pathlib import Path
6-
from typing import Any, Dict, List, Mapping, Sequence, Tuple, TypedDict, TypeVar, Union
6+
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Sequence, Tuple, TypedDict, TypeVar, Union
77

88
from opentelemetry import trace as trace_api
99
from opentelemetry.sdk.resources import Resource
@@ -13,6 +13,9 @@
1313
from opentelemetry.util import types as otel_types
1414
from requests import RequestException, Response
1515

16+
if TYPE_CHECKING:
17+
from packaging.version import Version
18+
1619
T = TypeVar('T')
1720

1821
JsonValue = Union[int, float, str, bool, None, List['JsonValue'], Tuple['JsonValue', ...], 'JsonDict']
@@ -168,3 +171,16 @@ def ensure_data_dir_exists(data_dir: Path) -> None:
168171
data_dir.mkdir(parents=True, exist_ok=True)
169172
gitignore = data_dir / '.gitignore'
170173
gitignore.write_text('*')
174+
175+
176+
def get_version(version: str) -> Version:
177+
"""Return a packaging.version.Version object from a version string.
178+
179+
We check if `packaging` is available, falling back to `setuptools._vendor.packaging` if it's not.
180+
"""
181+
try:
182+
from packaging.version import Version
183+
184+
except ImportError: # pragma: no cover
185+
from setuptools._vendor.packaging.version import Version
186+
return Version(version) # type: ignore

logfire/integrations/pydantic.py

Lines changed: 62 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,25 @@
44

55
import functools
66
import inspect
7+
import os
78
import re
89
from dataclasses import dataclass
910
from functools import lru_cache
1011
from typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict, TypeVar
1112

13+
import pydantic
1214
from typing_extensions import ParamSpec
1315

1416
import logfire
1517
from logfire import LogfireSpan
1618

1719
from .._internal.config import GLOBAL_CONFIG, PydanticPlugin
1820
from .._internal.config_params import default_param_manager
21+
from .._internal.utils import get_version
1922

2023
if TYPE_CHECKING: # pragma: no cover
2124
from pydantic import ValidationError
22-
from pydantic.plugin import (
23-
SchemaKind,
24-
SchemaTypePath,
25-
)
25+
from pydantic.plugin import SchemaKind, SchemaTypePath
2626
from pydantic_core import CoreConfig, CoreSchema
2727

2828

@@ -292,61 +292,71 @@ def get_schema_name(schema: CoreSchema) -> str:
292292
class LogfirePydanticPlugin:
293293
"""Implements a new API for pydantic plugins.
294294
295-
Patches pydantic to accept this new API shape.
296-
297-
Set the `LOGFIRE_DISABLE_PYDANTIC_PLUGIN` environment variable to `true` to disable the plugin.
298-
299-
Note:
300-
In the future, you'll be able to use the `PYDANTIC_DISABLE_PLUGINS` instead.
295+
Patches Pydantic to accept this new API shape.
301296
302-
See [pydantic/pydantic#7709](https://github.com/pydantic/pydantic/issues/7709) for more information.
297+
Set the `LOGFIRE_PYDANTIC_RECORD` environment variable to `"off"` to disable the plugin, or
298+
`PYDANTIC_DISABLE_PLUGINS` to `true` to disable all Pydantic plugins.
303299
"""
304300

305-
def new_schema_validator(
306-
self,
307-
schema: CoreSchema,
308-
schema_type: Any,
309-
schema_type_path: SchemaTypePath,
310-
schema_kind: SchemaKind,
311-
config: CoreConfig | None,
312-
plugin_settings: dict[str, Any],
313-
) -> tuple[_ValidateWrapper, ...] | tuple[None, ...]:
314-
"""This method is called every time a new `SchemaValidator` is created.
315-
316-
Args:
317-
schema: The schema to validate against.
318-
schema_type: The original type which the schema was created from, e.g. the model class.
319-
schema_type_path: Path defining where `schema_type` was defined, or where `TypeAdapter` was called.
320-
schema_kind: The kind of schema to validate against.
321-
config: The config to use for validation.
322-
plugin_settings: The plugin settings.
323-
324-
Returns:
325-
A tuple of decorator factories for each of the three validation methods -
326-
`validate_python`, `validate_json`, `validate_strings` or a tuple of
327-
three `None` if recording is `off`.
328-
"""
329-
# Patch a bug that occurs even if the plugin is disabled.
330-
_patch_PluggableSchemaValidator()
331-
332-
logfire_settings = plugin_settings.get('logfire')
333-
if logfire_settings and 'record' in logfire_settings:
334-
record = logfire_settings['record']
335-
else:
336-
record = _pydantic_plugin_config().record
301+
if (
302+
get_version(pydantic.__version__) < get_version('2.5.0') or os.environ.get('LOGFIRE_PYDANTIC_RECORD') == 'off'
303+
): # pragma: no cover
304+
305+
def new_schema_validator( # type: ignore[reportRedeclaration]
306+
self, *_: Any, **__: Any
307+
) -> tuple[_ValidateWrapper, ...] | tuple[None, ...]:
308+
"""Backwards compatibility for Pydantic < 2.5.0.
337309
338-
if record == 'off':
310+
This method is called every time a new `SchemaValidator` is created, and is a NO-OP for Pydantic < 2.5.0.
311+
"""
339312
return None, None, None
313+
else:
340314

341-
if _include_model(schema_type_path):
342-
_patch_build_wrapper()
343-
return (
344-
_ValidateWrapper('validate_python', schema, config, plugin_settings, schema_type_path, record),
345-
_ValidateWrapper('validate_json', schema, config, plugin_settings, schema_type_path, record),
346-
_ValidateWrapper('validate_strings', schema, config, plugin_settings, schema_type_path, record),
347-
)
315+
def new_schema_validator(
316+
self,
317+
schema: CoreSchema,
318+
schema_type: Any,
319+
schema_type_path: SchemaTypePath,
320+
schema_kind: SchemaKind,
321+
config: CoreConfig | None,
322+
plugin_settings: dict[str, Any],
323+
) -> tuple[_ValidateWrapper, ...] | tuple[None, ...]:
324+
"""This method is called every time a new `SchemaValidator` is created.
325+
326+
Args:
327+
schema: The schema to validate against.
328+
schema_type: The original type which the schema was created from, e.g. the model class.
329+
schema_type_path: Path defining where `schema_type` was defined, or where `TypeAdapter` was called.
330+
schema_kind: The kind of schema to validate against.
331+
config: The config to use for validation.
332+
plugin_settings: The plugin settings.
333+
334+
Returns:
335+
A tuple of decorator factories for each of the three validation methods -
336+
`validate_python`, `validate_json`, `validate_strings` or a tuple of
337+
three `None` if recording is `off`.
338+
"""
339+
# Patch a bug that occurs even if the plugin is disabled.
340+
_patch_PluggableSchemaValidator()
341+
342+
logfire_settings = plugin_settings.get('logfire')
343+
if logfire_settings and 'record' in logfire_settings:
344+
record = logfire_settings['record']
345+
else:
346+
record = _pydantic_plugin_config().record
347+
348+
if record == 'off':
349+
return None, None, None
350+
351+
if _include_model(schema_type_path):
352+
_patch_build_wrapper()
353+
return (
354+
_ValidateWrapper('validate_python', schema, config, plugin_settings, schema_type_path, record),
355+
_ValidateWrapper('validate_json', schema, config, plugin_settings, schema_type_path, record),
356+
_ValidateWrapper('validate_strings', schema, config, plugin_settings, schema_type_path, record),
357+
)
348358

349-
return None, None, None
359+
return None, None, None
350360

351361

352362
plugin = LogfirePydanticPlugin()

0 commit comments

Comments
 (0)