From c209d1399693d7174b91f55abf1b9a1dc79516c0 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 26 Feb 2025 09:26:28 -0800 Subject: [PATCH 1/3] Upgrade to Pydantic v2 (thanks, Claude Code) Refs #520 --- llm/cli.py | 8 ++++---- llm/default_plugins/openai_models.py | 9 +-------- llm/models.py | 8 ++------ llm/templates.py | 5 ++--- pytest.ini | 3 --- setup.py | 2 +- 6 files changed, 10 insertions(+), 25 deletions(-) diff --git a/llm/cli.py b/llm/cli.py index e821c284..d04cc95a 100644 --- a/llm/cli.py +++ b/llm/cli.py @@ -1197,9 +1197,9 @@ def models_list(options, async_, query): model_with_aliases.model if not async_ else model_with_aliases.async_model ) output = str(model) + extra - if options and model.Options.schema()["properties"]: + if options and model.Options.model_json_schema()["properties"]: output += "\n Options:" - for name, field in model.Options.schema()["properties"].items(): + for name, field in model.Options.model_json_schema()["properties"].items(): any_of = field.get("anyOf") if any_of is None: any_of = [{"type": field.get("type", "str")}] @@ -1413,7 +1413,7 @@ def templates_show(name): template = load_template(name) click.echo( yaml.dump( - dict((k, v) for k, v in template.dict().items() if v is not None), + dict((k, v) for k, v in template.model_dump().items() if v is not None), indent=4, default_flow_style=False, ) @@ -2073,7 +2073,7 @@ def get_history(chat_id): def render_errors(errors): output = [] for error in errors: - output.append(", ".join(error["loc"])) + output.append(", ".join(str(loc) for loc in error["loc"])) output.append(" " + error["msg"]) return "\n".join(output) diff --git a/llm/default_plugins/openai_models.py b/llm/default_plugins/openai_models.py index b3227a1e..ef2d9be3 100644 --- a/llm/default_plugins/openai_models.py +++ b/llm/default_plugins/openai_models.py @@ -13,14 +13,7 @@ import openai import os -try: - # Pydantic 2 - from pydantic import field_validator, Field # type: ignore - -except ImportError: - # Pydantic 1 - from pydantic.fields import Field - from pydantic.class_validators import validator as field_validator # type: ignore [no-redef] +from pydantic import field_validator, Field from typing import AsyncGenerator, List, Iterable, Iterator, Optional, Union import json diff --git a/llm/models.py b/llm/models.py index c7e3c722..13f63a75 100644 --- a/llm/models.py +++ b/llm/models.py @@ -23,7 +23,7 @@ from .utils import mimetype_from_path, mimetype_from_string, token_usage_string from abc import ABC, abstractmethod import json -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from ulid import ULID CONVERSATION_NAME_LENGTH = 32 @@ -499,7 +499,6 @@ async def __anext__(self) -> str: return chunk if not hasattr(self, "_generator"): - if isinstance(self.model, AsyncModel): self._generator = self.model.execute( self.prompt, @@ -618,10 +617,7 @@ def __repr__(self): class Options(BaseModel): - # Note: using pydantic v1 style Configs, - # these are also compatible with pydantic v2 - class Config: - extra = "forbid" + model_config = ConfigDict(extra="forbid") _Options = Options diff --git a/llm/templates.py b/llm/templates.py index 0cf16162..ca477195 100644 --- a/llm/templates.py +++ b/llm/templates.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict import string from typing import Optional, Any, Dict, List, Tuple @@ -13,8 +13,7 @@ class Template(BaseModel): extract: Optional[bool] = None extract_last: Optional[bool] = None - class Config: - extra = "forbid" + model_config = ConfigDict(extra="forbid") class MissingVariables(Exception): pass diff --git a/pytest.ini b/pytest.ini index ba352d26..6a7d1706 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,2 @@ [pytest] -filterwarnings = - ignore:The `schema` method is deprecated.*:DeprecationWarning - ignore:Support for class-based `config` is deprecated*:DeprecationWarning asyncio_default_fixture_loop_scope = function \ No newline at end of file diff --git a/setup.py b/setup.py index 98452d75..23d80cf7 100644 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ def get_long_description(): "click-default-group>=1.2.3", "sqlite-utils>=3.37", "sqlite-migrate>=0.1a2", - "pydantic>=1.10.2", + "pydantic>=2.0.0", "PyYAML", "pluggy", "python-ulid", From 5fc8340ea476ede5034b30c39467609f88d14055 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 26 Feb 2025 09:29:02 -0800 Subject: [PATCH 2/3] Stop testing against Pydantic v1, refs #520 --- .github/workflows/test.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1a4d3926..05352474 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,6 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] - pydantic: ["==1.10.2", ">=2.0.0"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -24,7 +23,6 @@ jobs: - name: Install dependencies run: | pip install -e '.[test]' - pip install 'pydantic${{ matrix.pydantic }}' - name: Run tests run: | pytest From 75f7b635bda30fed036ac42bfd8a93af55ec7d90 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 26 Feb 2025 09:58:42 -0800 Subject: [PATCH 3/3] No need for the str(loc) bit --- llm/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llm/cli.py b/llm/cli.py index d04cc95a..d5d523c0 100644 --- a/llm/cli.py +++ b/llm/cli.py @@ -2073,7 +2073,7 @@ def get_history(chat_id): def render_errors(errors): output = [] for error in errors: - output.append(", ".join(str(loc) for loc in error["loc"])) + output.append(", ".join(error["loc"])) output.append(" " + error["msg"]) return "\n".join(output)