Skip to content

Commit

Permalink
Raise an exception when Pydantic plugin is enabled on Pydantic <2.5.0 (
Browse files Browse the repository at this point in the history
…#160)

Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
Co-authored-by: Alex Hall <alex.mojaki@gmail.com>
  • Loading branch information
3 people authored May 14, 2024
1 parent 8e3b2dd commit 4e94b68
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 54 deletions.
7 changes: 6 additions & 1 deletion logfire/_internal/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
from .metrics import ProxyMeterProvider, configure_metrics
from .scrubbing import Scrubber, ScrubCallback
from .tracer import PendingSpanProcessor, ProxyTracerProvider
from .utils import UnexpectedResponse, ensure_data_dir_exists, read_toml_file
from .utils import UnexpectedResponse, ensure_data_dir_exists, get_version, read_toml_file

CREDENTIALS_FILENAME = 'logfire_credentials.json'
"""Default base URL for the Logfire API."""
Expand Down Expand Up @@ -394,6 +394,11 @@ def _load_configuration(
# This is particularly for deserializing from a dict as in executors.py
pydantic_plugin = PydanticPlugin(**pydantic_plugin) # type: ignore
self.pydantic_plugin = pydantic_plugin or param_manager.pydantic_plugin
if self.pydantic_plugin.record != 'off':
import pydantic

if get_version(pydantic.__version__) < get_version('2.5.0'): # pragma: no cover
raise RuntimeError('The Pydantic plugin requires Pydantic 2.5.0 or newer.')
self.fast_shutdown = fast_shutdown

self.id_generator = id_generator or RandomIdGenerator()
Expand Down
18 changes: 17 additions & 1 deletion logfire/_internal/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
import sys
from pathlib import Path
from typing import Any, Dict, List, Mapping, Sequence, Tuple, TypedDict, TypeVar, Union
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Sequence, Tuple, TypedDict, TypeVar, Union

from opentelemetry import trace as trace_api
from opentelemetry.sdk.resources import Resource
Expand All @@ -13,6 +13,9 @@
from opentelemetry.util import types as otel_types
from requests import RequestException, Response

if TYPE_CHECKING:
from packaging.version import Version

T = TypeVar('T')

JsonValue = Union[int, float, str, bool, None, List['JsonValue'], Tuple['JsonValue', ...], 'JsonDict']
Expand Down Expand Up @@ -168,3 +171,16 @@ def ensure_data_dir_exists(data_dir: Path) -> None:
data_dir.mkdir(parents=True, exist_ok=True)
gitignore = data_dir / '.gitignore'
gitignore.write_text('*')


def get_version(version: str) -> Version:
"""Return a packaging.version.Version object from a version string.
We check if `packaging` is available, falling back to `setuptools._vendor.packaging` if it's not.
"""
try:
from packaging.version import Version

except ImportError: # pragma: no cover
from setuptools._vendor.packaging.version import Version
return Version(version) # type: ignore
114 changes: 62 additions & 52 deletions logfire/integrations/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,25 @@

import functools
import inspect
import os
import re
from dataclasses import dataclass
from functools import lru_cache
from typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict, TypeVar

import pydantic
from typing_extensions import ParamSpec

import logfire
from logfire import LogfireSpan

from .._internal.config import GLOBAL_CONFIG, PydanticPlugin
from .._internal.config_params import default_param_manager
from .._internal.utils import get_version

if TYPE_CHECKING: # pragma: no cover
from pydantic import ValidationError
from pydantic.plugin import (
SchemaKind,
SchemaTypePath,
)
from pydantic.plugin import SchemaKind, SchemaTypePath
from pydantic_core import CoreConfig, CoreSchema


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

def new_schema_validator(
self,
schema: CoreSchema,
schema_type: Any,
schema_type_path: SchemaTypePath,
schema_kind: SchemaKind,
config: CoreConfig | None,
plugin_settings: dict[str, Any],
) -> tuple[_ValidateWrapper, ...] | tuple[None, ...]:
"""This method is called every time a new `SchemaValidator` is created.
Args:
schema: The schema to validate against.
schema_type: The original type which the schema was created from, e.g. the model class.
schema_type_path: Path defining where `schema_type` was defined, or where `TypeAdapter` was called.
schema_kind: The kind of schema to validate against.
config: The config to use for validation.
plugin_settings: The plugin settings.
Returns:
A tuple of decorator factories for each of the three validation methods -
`validate_python`, `validate_json`, `validate_strings` or a tuple of
three `None` if recording is `off`.
"""
# Patch a bug that occurs even if the plugin is disabled.
_patch_PluggableSchemaValidator()

logfire_settings = plugin_settings.get('logfire')
if logfire_settings and 'record' in logfire_settings:
record = logfire_settings['record']
else:
record = _pydantic_plugin_config().record
if (
get_version(pydantic.__version__) < get_version('2.5.0') or os.environ.get('LOGFIRE_PYDANTIC_RECORD') == 'off'
): # pragma: no cover

def new_schema_validator( # type: ignore[reportRedeclaration]
self, *_: Any, **__: Any
) -> tuple[_ValidateWrapper, ...] | tuple[None, ...]:
"""Backwards compatibility for Pydantic < 2.5.0.
if record == 'off':
This method is called every time a new `SchemaValidator` is created, and is a NO-OP for Pydantic < 2.5.0.
"""
return None, None, None
else:

if _include_model(schema_type_path):
_patch_build_wrapper()
return (
_ValidateWrapper('validate_python', schema, config, plugin_settings, schema_type_path, record),
_ValidateWrapper('validate_json', schema, config, plugin_settings, schema_type_path, record),
_ValidateWrapper('validate_strings', schema, config, plugin_settings, schema_type_path, record),
)
def new_schema_validator(
self,
schema: CoreSchema,
schema_type: Any,
schema_type_path: SchemaTypePath,
schema_kind: SchemaKind,
config: CoreConfig | None,
plugin_settings: dict[str, Any],
) -> tuple[_ValidateWrapper, ...] | tuple[None, ...]:
"""This method is called every time a new `SchemaValidator` is created.
Args:
schema: The schema to validate against.
schema_type: The original type which the schema was created from, e.g. the model class.
schema_type_path: Path defining where `schema_type` was defined, or where `TypeAdapter` was called.
schema_kind: The kind of schema to validate against.
config: The config to use for validation.
plugin_settings: The plugin settings.
Returns:
A tuple of decorator factories for each of the three validation methods -
`validate_python`, `validate_json`, `validate_strings` or a tuple of
three `None` if recording is `off`.
"""
# Patch a bug that occurs even if the plugin is disabled.
_patch_PluggableSchemaValidator()

logfire_settings = plugin_settings.get('logfire')
if logfire_settings and 'record' in logfire_settings:
record = logfire_settings['record']
else:
record = _pydantic_plugin_config().record

if record == 'off':
return None, None, None

if _include_model(schema_type_path):
_patch_build_wrapper()
return (
_ValidateWrapper('validate_python', schema, config, plugin_settings, schema_type_path, record),
_ValidateWrapper('validate_json', schema, config, plugin_settings, schema_type_path, record),
_ValidateWrapper('validate_strings', schema, config, plugin_settings, schema_type_path, record),
)

return None, None, None
return None, None, None


plugin = LogfirePydanticPlugin()
Expand Down

0 comments on commit 4e94b68

Please sign in to comment.