From 963ac33b4d78c9f697daee3747b1697ad1973520 Mon Sep 17 00:00:00 2001 From: Maxwell Muoto <41130755+max-muoto@users.noreply.github.com> Date: Tue, 30 Apr 2024 19:38:19 -0500 Subject: [PATCH 1/3] Avoid deprecated class property stacking --- .ruff.toml | 2 +- instructor/function_calls.py | 30 ++++++++++++++++-------------- instructor/utils.py | 32 +++++++++++++++++++++++++++++++- pyproject.toml | 1 - tests/test_utils.py | 21 +++++++++++++++++++++ 5 files changed, 69 insertions(+), 17 deletions(-) diff --git a/.ruff.toml b/.ruff.toml index f2415a1a0..1c3c18c9a 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -55,7 +55,7 @@ unfixable = [ ] ignore-init-module-imports = true -[extend-per-file-ignores] +[lint.extend-per-file-ignores] "instructor/distil.py" = ["ARG002"] "tests/test_distil.py" = ["ARG001"] "tests/test_patch.py" = ["ARG001"] diff --git a/instructor/function_calls.py b/instructor/function_calls.py index a14c88b59..751bdf908 100644 --- a/instructor/function_calls.py +++ b/instructor/function_calls.py @@ -4,10 +4,11 @@ from docstring_parser import parse from openai.types.chat import ChatCompletion -from pydantic import BaseModel, Field, TypeAdapter, create_model # type: ignore - remove once Pydantic is updated +from pydantic import BaseModel, Field, TypeAdapter, ConfigDict, create_model # type: ignore - remove once Pydantic is updated from instructor.exceptions import IncompleteOutputException from instructor.mode import Mode -from instructor.utils import extract_json_from_codeblock +from instructor.utils import extract_json_from_codeblock, classproperty + T = TypeVar("T") @@ -15,9 +16,11 @@ class OpenAISchema(BaseModel): - @classmethod - @property - def openai_schema(cls) -> dict[str, Any]: + # Ignore classproperty, since Pydantic doesn't understand it like it would a normal property. + model_config = ConfigDict(ignored_types=(classproperty,)) + + @classproperty + def openai_schema(self) -> dict[str, Any]: """ Return the schema in the format of OpenAI's schema as jsonschema @@ -27,8 +30,8 @@ def openai_schema(cls) -> dict[str, Any]: Returns: model_json_schema (dict): A dictionary in the format of OpenAI's schema as jsonschema """ - schema = cls.model_json_schema() - docstring = parse(cls.__doc__ or "") + schema = self.model_json_schema() + docstring = parse(self.__doc__ or "") parameters = { k: v for k, v in schema.items() if k not in ("title", "description") } @@ -48,7 +51,7 @@ def openai_schema(cls) -> dict[str, Any]: schema["description"] = docstring.short_description else: schema["description"] = ( - f"Correctly extracted `{cls.__name__}` with all " + f"Correctly extracted `{self.__name__}` with all " f"the required parameters with correct types" ) @@ -58,13 +61,12 @@ def openai_schema(cls) -> dict[str, Any]: "parameters": parameters, } - @classmethod - @property - def anthropic_schema(cls) -> dict[str, Any]: + @classproperty + def anthropic_schema(self) -> dict[str, Any]: return { - "name": cls.openai_schema["name"], - "description": cls.openai_schema["description"], - "input_schema": cls.model_json_schema(), + "name": self.openai_schema["name"], + "description": self.openai_schema["description"], + "input_schema": self.model_json_schema(), } @classmethod diff --git a/instructor/utils.py b/instructor/utils.py index b661fed4a..ffcbdf9b5 100644 --- a/instructor/utils.py +++ b/instructor/utils.py @@ -3,7 +3,15 @@ import inspect import json import logging -from typing import Callable, Generator, Iterable, AsyncGenerator, Protocol, TypeVar +from typing import ( + Callable, + Generator, + Generic, + Iterable, + AsyncGenerator, + Protocol, + TypeVar, +) from openai.types.completion_usage import CompletionUsage from anthropic.types import Usage as AnthropicUsage from typing import Any @@ -15,6 +23,7 @@ ) logger = logging.getLogger("instructor") +R_co = TypeVar("R_co", covariant=True) T_Model = TypeVar("T_Model", bound="Response") from enum import Enum @@ -179,3 +188,24 @@ def merge_consecutive_messages(messages: list[dict[str, Any]]) -> list[dict[str, ) return new_messages + + +class classproperty(Generic[R_co]): + """Descriptor for class-level properties. + + Examples: + >>> from instructor.utils import classproperty + + >>> class MyClass: + ... @classproperty + ... def my_property(cls): + ... return cls + + >>> assert MyClass.my_property + """ + + def __init__(self, method: Callable[[Any], R_co]) -> None: + self.cproperty = method + + def __get__(self, instance: object, cls: type[Any]) -> R_co: + return self.cproperty(cls) diff --git a/pyproject.toml b/pyproject.toml index d2a7baa6c..652cb73b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,4 +100,3 @@ typeCheckingMode = "strict" # Allow "redundant" runtime type-checking. reportUnnecessaryIsInstance = "none" reportUnnecessaryTypeIgnoreComment = "error" -reportDeprecated = "warning" diff --git a/tests/test_utils.py b/tests/test_utils.py index f0de74b87..217ce604d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,7 @@ import json import pytest from instructor.utils import ( + classproperty, extract_json_from_codeblock, extract_json_from_stream, extract_json_from_stream_async, @@ -170,3 +171,23 @@ def test_merge_consecutive_messages_single(): {"role": "user", "content": [{"type": "text", "text": "Hello"}]}, {"role": "assistant", "content": [{"type": "text", "text": "Hello"}]}, ] + + +def test_classproperty(): + """Test custom `classproperty` descriptor.""" + + class MyClass: + @classproperty + def my_property(cls): + return cls + + assert MyClass.my_property is MyClass + + class MyClass: + clvar = 1 + + @classproperty + def my_property(cls): + return cls.clvar + + assert MyClass.my_property == 1 From 3e00eaa03f950e1207caf72351ee0beeb96bdf80 Mon Sep 17 00:00:00 2001 From: Maxwell Muoto <41130755+max-muoto@users.noreply.github.com> Date: Tue, 30 Apr 2024 19:39:22 -0500 Subject: [PATCH 2/3] Revert --- instructor/function_calls.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/instructor/function_calls.py b/instructor/function_calls.py index 751bdf908..abffe7124 100644 --- a/instructor/function_calls.py +++ b/instructor/function_calls.py @@ -20,7 +20,7 @@ class OpenAISchema(BaseModel): model_config = ConfigDict(ignored_types=(classproperty,)) @classproperty - def openai_schema(self) -> dict[str, Any]: + def openai_schema(cls) -> dict[str, Any]: """ Return the schema in the format of OpenAI's schema as jsonschema @@ -30,8 +30,8 @@ def openai_schema(self) -> dict[str, Any]: Returns: model_json_schema (dict): A dictionary in the format of OpenAI's schema as jsonschema """ - schema = self.model_json_schema() - docstring = parse(self.__doc__ or "") + schema = cls.model_json_schema() + docstring = parse(cls.__doc__ or "") parameters = { k: v for k, v in schema.items() if k not in ("title", "description") } @@ -51,7 +51,7 @@ def openai_schema(self) -> dict[str, Any]: schema["description"] = docstring.short_description else: schema["description"] = ( - f"Correctly extracted `{self.__name__}` with all " + f"Correctly extracted `{cls.__name__}` with all " f"the required parameters with correct types" ) @@ -62,11 +62,11 @@ def openai_schema(self) -> dict[str, Any]: } @classproperty - def anthropic_schema(self) -> dict[str, Any]: + def anthropic_schema(cls) -> dict[str, Any]: return { - "name": self.openai_schema["name"], - "description": self.openai_schema["description"], - "input_schema": self.model_json_schema(), + "name": cls.openai_schema["name"], + "description": cls.openai_schema["description"], + "input_schema": cls.model_json_schema(), } @classmethod From e224b6a8c4ade391b137144011776c4989a693c3 Mon Sep 17 00:00:00 2001 From: Maxwell Muoto <41130755+max-muoto@users.noreply.github.com> Date: Tue, 30 Apr 2024 19:41:21 -0500 Subject: [PATCH 3/3] Fix --- instructor/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/instructor/utils.py b/instructor/utils.py index 6a9bc6857..d5c0ce904 100644 --- a/instructor/utils.py +++ b/instructor/utils.py @@ -5,13 +5,11 @@ import logging from typing import ( Callable, - Generator, Generic, - Iterable, - AsyncGenerator, Protocol, TypeVar, ) +from collections.abc import Generator, Iterable, AsyncGenerator from typing import Callable, Protocol, TypeVar from collections.abc import Generator, Iterable, AsyncGenerator from openai.types.completion_usage import CompletionUsage