From 16dd2345b98a10d67225ccb3e3cd448cddae0c1a Mon Sep 17 00:00:00 2001 From: bossenti Date: Fri, 10 May 2024 15:52:47 +0200 Subject: [PATCH 1/8] docs: add note about minimal pydantic version --- docs/integrations/pydantic.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/integrations/pydantic.md b/docs/integrations/pydantic.md index 151f5591c..6c5e78cfa 100644 --- a/docs/integrations/pydantic.md +++ b/docs/integrations/pydantic.md @@ -11,6 +11,11 @@ import logfire logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record='all')) ``` +!!! info + + Please note that logfire requires pydantic in version >= `2.7.0`. + + ## Third party modules By default, third party modules are not instrumented by the plugin to avoid noise. You can enable instrumentation for those From 61d606da6da1501bf91ee84db9e53dc6bce295d4 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 13 May 2024 06:35:21 -0400 Subject: [PATCH 2/8] Move error to config --- docs/integrations/pydantic.md | 5 ----- logfire/_internal/config.py | 8 ++++++++ logfire/integrations/pydantic.py | 5 +---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/integrations/pydantic.md b/docs/integrations/pydantic.md index 6c5e78cfa..151f5591c 100644 --- a/docs/integrations/pydantic.md +++ b/docs/integrations/pydantic.md @@ -11,11 +11,6 @@ import logfire logfire.configure(pydantic_plugin=logfire.PydanticPlugin(record='all')) ``` -!!! info - - Please note that logfire requires pydantic in version >= `2.7.0`. - - ## Third party modules By default, third party modules are not instrumented by the plugin to avoid noise. You can enable instrumentation for those diff --git a/logfire/_internal/config.py b/logfire/_internal/config.py index 77e4c4810..e5c74dea3 100644 --- a/logfire/_internal/config.py +++ b/logfire/_internal/config.py @@ -383,6 +383,14 @@ 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 + + # setuptools is a dependency of opentelemetry-instrumentation, we use it instead of adding "packaging" as a dependency. + from setuptools._vendor.packaging.version import Version + + if Version(pydantic.__version__) < 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() diff --git a/logfire/integrations/pydantic.py b/logfire/integrations/pydantic.py index f8c3d3b36..95495ab0e 100644 --- a/logfire/integrations/pydantic.py +++ b/logfire/integrations/pydantic.py @@ -19,10 +19,7 @@ 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 From a7acd0a36891f4fdc34048621d062fa1d9759356 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 13 May 2024 06:55:39 -0400 Subject: [PATCH 3/8] update --- logfire/integrations/pydantic.py | 103 +++++++++++++++++-------------- 1 file changed, 56 insertions(+), 47 deletions(-) diff --git a/logfire/integrations/pydantic.py b/logfire/integrations/pydantic.py index 95495ab0e..308bef327 100644 --- a/logfire/integrations/pydantic.py +++ b/logfire/integrations/pydantic.py @@ -9,6 +9,8 @@ from functools import lru_cache from typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict, TypeVar +import pydantic +from setuptools._vendor.packaging.version import Version from typing_extensions import ParamSpec import logfire @@ -289,61 +291,68 @@ def get_schema_name(schema: CoreSchema) -> str: class LogfirePydanticPlugin: """Implements a new API for pydantic plugins. - Patches pydantic to accept this new API shape. + 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. - - See [pydantic/pydantic#7709](https://github.com/pydantic/pydantic/issues/7709) for more information. """ - 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 Version(pydantic.__version__) < Version('2.5.0'): # pragma: no branch + + def new_schema_validator( # type: ignore[reportRedeclaration] + self, schema: CoreSchema, config: CoreConfig | None, plugin_settings: dict[str, 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() From 997dfa86f7325161dacc212c85807c88d8f47cbf Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 13 May 2024 07:13:10 -0400 Subject: [PATCH 4/8] Add pragma --- logfire/integrations/pydantic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logfire/integrations/pydantic.py b/logfire/integrations/pydantic.py index 308bef327..a5aff24b8 100644 --- a/logfire/integrations/pydantic.py +++ b/logfire/integrations/pydantic.py @@ -296,7 +296,7 @@ class LogfirePydanticPlugin: Set the `LOGFIRE_DISABLE_PYDANTIC_PLUGIN` environment variable to `true` to disable the plugin. """ - if Version(pydantic.__version__) < Version('2.5.0'): # pragma: no branch + if Version(pydantic.__version__) < Version('2.5.0'): # pragma: no cover def new_schema_validator( # type: ignore[reportRedeclaration] self, schema: CoreSchema, config: CoreConfig | None, plugin_settings: dict[str, Any] From 4084d34de4bb9b6e5e3043f42fca20eac553ebcf Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 13 May 2024 07:49:59 -0400 Subject: [PATCH 5/8] Fix comments --- logfire/_internal/config.py | 7 ++----- logfire/_internal/utils.py | 18 +++++++++++++++++- logfire/integrations/pydantic.py | 7 ++++--- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/logfire/_internal/config.py b/logfire/_internal/config.py index e5c74dea3..da01d7953 100644 --- a/logfire/_internal/config.py +++ b/logfire/_internal/config.py @@ -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.""" @@ -386,10 +386,7 @@ def _load_configuration( if self.pydantic_plugin.record != 'off': import pydantic - # setuptools is a dependency of opentelemetry-instrumentation, we use it instead of adding "packaging" as a dependency. - from setuptools._vendor.packaging.version import Version - - if Version(pydantic.__version__) < Version('2.5.0'): # pragma: no cover + 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 diff --git a/logfire/_internal/utils.py b/logfire/_internal/utils.py index 69a77aac7..2c4938d8c 100644 --- a/logfire/_internal/utils.py +++ b/logfire/_internal/utils.py @@ -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 @@ -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'] @@ -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 diff --git a/logfire/integrations/pydantic.py b/logfire/integrations/pydantic.py index a5aff24b8..62d6f395f 100644 --- a/logfire/integrations/pydantic.py +++ b/logfire/integrations/pydantic.py @@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict, TypeVar import pydantic -from setuptools._vendor.packaging.version import Version from typing_extensions import ParamSpec import logfire @@ -18,6 +17,7 @@ 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 @@ -293,10 +293,11 @@ class LogfirePydanticPlugin: Patches Pydantic to accept this new API shape. - Set the `LOGFIRE_DISABLE_PYDANTIC_PLUGIN` environment variable to `true` to disable the plugin. + Set the `LOGFIRE_PYDANTIC_RECORD` environment variable to `"off"` to disable the plugin, or + `PYDANTIC_DISABLE_PLUGINS` to `true` to disable all Pydantic plugins. """ - if Version(pydantic.__version__) < Version('2.5.0'): # pragma: no cover + if get_version(pydantic.__version__) < get_version('2.5.0'): # pragma: no cover def new_schema_validator( # type: ignore[reportRedeclaration] self, schema: CoreSchema, config: CoreConfig | None, plugin_settings: dict[str, Any] From ac247a6ced4fdec5d84fdcc57fa545c54b35df31 Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 13 May 2024 10:24:21 -0600 Subject: [PATCH 6/8] Update logfire/integrations/pydantic.py Co-authored-by: Alex Hall --- logfire/integrations/pydantic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logfire/integrations/pydantic.py b/logfire/integrations/pydantic.py index 62d6f395f..003ac9c66 100644 --- a/logfire/integrations/pydantic.py +++ b/logfire/integrations/pydantic.py @@ -297,10 +297,10 @@ class LogfirePydanticPlugin: `PYDANTIC_DISABLE_PLUGINS` to `true` to disable all Pydantic plugins. """ - if get_version(pydantic.__version__) < get_version('2.5.0'): # pragma: no cover + 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, schema: CoreSchema, config: CoreConfig | None, plugin_settings: dict[str, Any] + self, *_: Any, *__: Any ) -> tuple[_ValidateWrapper, ...] | tuple[None, ...]: """Backwards compatibility for Pydantic < 2.5.0. From 23a402fe4726d445d8872f1af3ffe2cdc805490f Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Mon, 13 May 2024 20:41:51 +0200 Subject: [PATCH 7/8] Update logfire/integrations/pydantic.py --- logfire/integrations/pydantic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logfire/integrations/pydantic.py b/logfire/integrations/pydantic.py index 003ac9c66..bc10b7fdf 100644 --- a/logfire/integrations/pydantic.py +++ b/logfire/integrations/pydantic.py @@ -300,7 +300,7 @@ class LogfirePydanticPlugin: 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 + self, *_: Any, **__: Any ) -> tuple[_ValidateWrapper, ...] | tuple[None, ...]: """Backwards compatibility for Pydantic < 2.5.0. From 4ea64f7df492fe31ce75051b50e12ab2d85dbd3f Mon Sep 17 00:00:00 2001 From: Marcelo Trylesinski Date: Mon, 13 May 2024 18:09:41 -0400 Subject: [PATCH 8/8] Add import os --- logfire/integrations/pydantic.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/logfire/integrations/pydantic.py b/logfire/integrations/pydantic.py index bc10b7fdf..ce66f608c 100644 --- a/logfire/integrations/pydantic.py +++ b/logfire/integrations/pydantic.py @@ -4,6 +4,7 @@ import functools import inspect +import os import re from dataclasses import dataclass from functools import lru_cache @@ -297,7 +298,9 @@ class LogfirePydanticPlugin: `PYDANTIC_DISABLE_PLUGINS` to `true` to disable all Pydantic plugins. """ - if get_version(pydantic.__version__) < get_version('2.5.0') or os.environ.get('LOGFIRE_PYDANTIC_RECORD') == 'off': # pragma: no cover + 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