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

Raise an exception when Pydantic plugin is enabled on Pydantic <2.5.0 #160

Merged
merged 11 commits into from
May 14, 2024
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