diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b619dd94b..01e1015ec 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,12 +58,26 @@ jobs: fi test: - name: test on ${{ matrix.python-version }} + name: test on Python ${{ matrix.python-version }} and pydantic ${{ matrix.pydantic-version }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + pydantic-version: ['main'] + include: + - python-version: '3.12' + pydantic-version: '2.4' + - python-version: '3.12' + pydantic-version: '2.5' + - python-version: '3.12' + pydantic-version: '2.6' + - python-version: '3.12' + pydantic-version: '2.7' + - python-version: '3.12' + pydantic-version: '2.8' + - python-version: '3.12' + pydantic-version: '2.9' env: PYTHON: ${{ matrix.python-version }} steps: @@ -87,14 +101,19 @@ jobs: - run: uv sync --python ${{ matrix.python-version }} --upgrade + - name: Install pydantic ${{ matrix.pydantic-version }} + if: matrix.pydantic-version != 'main' + # installs the most recent patch on the minor version's track, ex 2.6.0 -> 2.6.4 + run: uv pip install 'pydantic==${{ matrix.pydantic-version }}.*' + - run: mkdir coverage - - run: make test + - run: uv run --no-sync coverage run -m pytest env: - COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }} + COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-pydantic-${{ matrix.pydantic-version }} - name: store coverage files uses: actions/upload-artifact@v4 with: - name: coverage-${{ matrix.python-version }} + name: coverage-py${{ matrix.python-version }}-pydantic-${{ matrix.pydantic-version }} path: coverage include-hidden-files: true diff --git a/logfire/_internal/main.py b/logfire/_internal/main.py index 5e8980d35..878dea9d1 100644 --- a/logfire/_internal/main.py +++ b/logfire/_internal/main.py @@ -833,7 +833,7 @@ def instrument_pydantic( if record != 'off': import pydantic - if get_version(pydantic.__version__) < get_version('2.5.0'): # pragma: no cover + if get_version(pydantic.__version__) < get_version('2.5.0'): raise RuntimeError('The Pydantic plugin requires Pydantic 2.5.0 or newer.') from logfire.integrations.pydantic import PydanticPlugin, set_pydantic_plugin_config diff --git a/logfire/integrations/pydantic.py b/logfire/integrations/pydantic.py index 4cd5e3819..8e39b2e8e 100644 --- a/logfire/integrations/pydantic.py +++ b/logfire/integrations/pydantic.py @@ -296,9 +296,7 @@ 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': def new_schema_validator( # type: ignore[reportRedeclaration] self, *_: Any, **__: Any diff --git a/tests/test_configure.py b/tests/test_configure.py index 08c5ced20..bbb3f76cb 100644 --- a/tests/test_configure.py +++ b/tests/test_configure.py @@ -30,6 +30,7 @@ SpanExportResult, ) from opentelemetry.trace import get_tracer_provider +from pydantic import __version__ as pydantic_version from pytest import LogCaptureFixture import logfire @@ -51,6 +52,7 @@ from logfire._internal.exporters.wrapper import WrapperSpanExporter from logfire._internal.integrations.executors import deserialize_config, serialize_config from logfire._internal.tracer import PendingSpanProcessor +from logfire._internal.utils import get_version from logfire.exceptions import LogfireConfigError from logfire.integrations.pydantic import get_pydantic_plugin_config from logfire.testing import TestExporter @@ -422,6 +424,9 @@ def fresh_pydantic_plugin(): return get_pydantic_plugin_config() +@pytest.mark.skipif( + get_version(pydantic_version) < get_version('2.5.0'), reason='skipping for pydantic versions < v2.5' +) def test_pydantic_plugin_include_exclude_strings(): logfire.instrument_pydantic(include='inc', exclude='exc') assert fresh_pydantic_plugin().include == {'inc'} diff --git a/tests/test_logfire.py b/tests/test_logfire.py index 5a887aac2..84d53d89c 100644 --- a/tests/test_logfire.py +++ b/tests/test_logfire.py @@ -17,7 +17,7 @@ from opentelemetry.sdk.trace import ReadableSpan from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor from opentelemetry.trace import StatusCode -from pydantic import BaseModel +from pydantic import BaseModel, __version__ as pydantic_version from pydantic_core import ValidationError import logfire @@ -2583,3 +2583,12 @@ def test_force_flush(exporter: TestExporter): logfire.force_flush() assert len(exporter.exported_spans_as_dict()) == 1 + + +@pytest.mark.skipif( + pydantic_version != '2.4.2', # type: ignore + reason='just testing compatibility with versions less that Pydantic v2.5.0', +) +def test_instrument_pydantic_on_2_5() -> None: + with pytest.raises(RuntimeError, match='The Pydantic plugin requires Pydantic 2.5.0 or newer.'): + logfire.instrument_pydantic() diff --git a/tests/test_logfire_api.py b/tests/test_logfire_api.py index 94e4882b6..cbe9aae27 100644 --- a/tests/test_logfire_api.py +++ b/tests/test_logfire_api.py @@ -8,6 +8,9 @@ from unittest.mock import MagicMock import pytest +from pydantic import __version__ as pydantic_version + +from logfire._internal.utils import get_version def logfire_dunder_all() -> set[str]: @@ -137,7 +140,10 @@ def func() -> None: ... for member in [m for m in logfire__all__ if m.startswith('instrument_')]: assert hasattr(logfire_api, member), member - getattr(logfire_api, member)() + if not (get_version(pydantic_version) < get_version('2.5.0') and member == 'instrument_pydantic'): + # skip pydantic instrumentation (which uses the plugin) for versions prior to v2.5 + getattr(logfire_api, member)() + # just remove the member unconditionally to pass future asserts logfire__all__.remove(member) assert hasattr(logfire_api, 'shutdown') diff --git a/tests/test_pydantic_plugin.py b/tests/test_pydantic_plugin.py index 34d194e1a..b952f8263 100644 --- a/tests/test_pydantic_plugin.py +++ b/tests/test_pydantic_plugin.py @@ -2,7 +2,7 @@ import importlib.metadata import os -from typing import Any +from typing import TYPE_CHECKING, Any from unittest.mock import patch import cloudpickle @@ -11,17 +11,23 @@ from inline_snapshot import snapshot from opentelemetry.sdk.metrics.export import AggregationTemporality, InMemoryMetricReader from opentelemetry.sdk.trace.export import SimpleSpanProcessor -from pydantic import BaseModel, ConfigDict, ValidationError, field_validator +from pydantic import ( + AfterValidator, + BaseModel, + ConfigDict, + TypeAdapter, + ValidationError, + __version__ as pydantic_version, + field_validator, +) from pydantic.dataclasses import dataclass as pydantic_dataclass -from pydantic.functional_validators import AfterValidator -from pydantic.plugin import PydanticPluginProtocol, SchemaTypePath, ValidatePythonHandlerProtocol -from pydantic.type_adapter import TypeAdapter from pydantic_core import core_schema from typing_extensions import Annotated import logfire import logfire.integrations.pydantic from logfire._internal.config import GLOBAL_CONFIG +from logfire._internal.utils import get_version from logfire.integrations.pydantic import ( LogfirePydanticPlugin, get_schema_name, @@ -29,6 +35,20 @@ from logfire.testing import SeededRandomIdGenerator, TestExporter from tests.test_metrics import get_collected_metrics +pytestmark = pytest.mark.skipif( + get_version(pydantic_version) < get_version('2.5.0'), + reason='Skipping all tests for versions less than 2.5.', +) + +if TYPE_CHECKING: + from pydantic.plugin import PydanticPluginProtocol, SchemaTypePath, ValidatePythonHandlerProtocol + +try: + from pydantic.plugin import PydanticPluginProtocol, SchemaTypePath, ValidatePythonHandlerProtocol +except ImportError: + # it's fine, pydantic version