From d93f00a32a98146b85459ccd9c15dad20fb43929 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Sat, 27 Jan 2024 17:02:25 -0800 Subject: [PATCH 1/2] Improve default serialization --- python/langsmith/client.py | 53 +++++++++++------ python/tests/unit_tests/test_client.py | 80 ++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 19 deletions(-) diff --git a/python/langsmith/client.py b/python/langsmith/client.py index 0f1a05083..2a44f6827 100644 --- a/python/langsmith/client.py +++ b/python/langsmith/client.py @@ -145,26 +145,41 @@ def _default_retry_config() -> Retry: return Retry(**retry_params) # type: ignore -def _serialize_json(obj: Any) -> Union[str, dict]: - if isinstance(obj, datetime.datetime): - return obj.isoformat() +def _serialize_json(obj: Any) -> Any: + if obj is None: + return None - elif hasattr(obj, "model_dump_json") and callable(obj.model_dump_json): - # Base models, V2 - try: - return json.loads(obj.model_dump_json(exclude_none=True)) - except Exception: - logger.debug(f"Failed to serialize obj of type {type(obj)} to JSON") - return str(obj) - elif hasattr(obj, "json") and callable(obj.json): - # Base models, V1 - try: - return json.loads(obj.json(exclude_none=True)) - except Exception: - logger.debug(f"Failed to json serialize {type(obj)} to JSON") - return repr(obj) - else: - return str(obj) + basic_types = (bool, int, float, str, datetime.datetime, uuid.UUID) + if isinstance(obj, basic_types): + return obj.isoformat() if isinstance(obj, datetime.datetime) else str(obj) + + serialization_methods = [ + ("model_dump_json", True), # Base models, V2 + ("json", True), # Base models, V1 + ("to_json", False), # dataclass_json + ("dict", False), # dataclass + ] + + for attr, exclude_none in serialization_methods: + if hasattr(obj, attr) and callable(getattr(obj, attr)): + try: + method = getattr(obj, attr) + json_str = ( + method(exclude_none=exclude_none) if exclude_none else method() + ) + return json.loads(json_str) + except Exception as e: + logger.debug(f"Failed to serialize {type(obj)} to JSON: {e}") + return repr(obj) + + try: + if hasattr(obj, "__slots__"): + return {slot: getattr(obj, slot) for slot in obj.__slots__} + else: + return vars(obj) + except Exception as e: + logger.debug(f"Failed to serialize {type(obj)} to JSON using vars: {e}") + return repr(obj) def close_session(session: requests.Session) -> None: diff --git a/python/tests/unit_tests/test_client.py b/python/tests/unit_tests/test_client.py index 930d4211d..e7226186f 100644 --- a/python/tests/unit_tests/test_client.py +++ b/python/tests/unit_tests/test_client.py @@ -1,14 +1,17 @@ """Test the LangSmith client.""" import asyncio +import dataclasses import json import os import uuid from datetime import datetime +from enum import Enum from io import BytesIO from typing import Optional from unittest import mock from unittest.mock import patch +import dataclasses_json import pytest from pydantic import BaseModel @@ -277,6 +280,83 @@ class MyPydantic(BaseModel): assert res2 == {"output": expected} +def test_serialize_json() -> None: + class MyClass: + def __init__(self, x: int) -> None: + self.x = x + self.y = "y" + + class MyClassWithSlots: + __slots__ = ["x", "y"] + + def __init__(self, x: int) -> None: + self.x = x + self.y = "y" + + class MyPydantic(BaseModel): + foo: str + bar: int + + @dataclasses.dataclass + class MyDataclass: + foo: str + bar: int + + class MyEnum(str, Enum): + FOO = "foo" + BAR = "bar" + + @dataclasses_json.dataclass_json + @dataclasses.dataclass + class Person: + name: str + + uid = uuid.uuid4() + current_time = datetime.now() + + class NestedClass: + __slots__ = ["person"] + + def __init__(self) -> None: + self.person = Person(name="foo") + + to_serialize = { + "uid": uid, + "time": current_time, + "my_class": MyClass(1), + "my_slotted_class": MyClassWithSlots(1), + "my_dataclass": MyDataclass("foo", 1), + "my_enum": MyEnum.FOO, + "my_pydantic": MyPydantic(foo="foo", bar=1), + "person": Person(name="foo"), + "a_bool": True, + "a_none": None, + "a_str": "foo", + "an_int": 1, + "a_float": 1.1, + "nested_class": NestedClass(), + } + + res = json.loads(json.dumps(to_serialize, default=_serialize_json)) + expected = { + "uid": str(uid), + "time": current_time.isoformat(), + "my_class": {"x": 1, "y": "y"}, + "my_slotted_class": {"x": 1, "y": "y"}, + "my_dataclass": {"foo": "foo", "bar": 1}, + "my_enum": "foo", + "my_pydantic": {"foo": "foo", "bar": 1}, + "person": {"name": "foo"}, + "a_bool": True, + "a_none": None, + "a_str": "foo", + "an_int": 1, + "a_float": 1.1, + "nested_class": {"person": {"name": "foo"}}, + } + assert res == expected + + def test_host_url() -> None: client = Client(api_url="https://api.foobar.com/api", api_key="API_KEY") assert client._host_url == "https://api.foobar.com" From fe82dffa654227eea910c85736005db183a8dba9 Mon Sep 17 00:00:00 2001 From: William Fu-Hinthorn <13333726+hinthornw@users.noreply.github.com> Date: Sat, 27 Jan 2024 17:04:18 -0800 Subject: [PATCH 2/2] Check attr dict --- python/langsmith/client.py | 64 ++++++++++++++------------ python/tests/unit_tests/test_client.py | 19 +++++++- 2 files changed, 52 insertions(+), 31 deletions(-) diff --git a/python/langsmith/client.py b/python/langsmith/client.py index 2a44f6827..f0b14ba34 100644 --- a/python/langsmith/client.py +++ b/python/langsmith/client.py @@ -2,6 +2,7 @@ from __future__ import annotations import collections +import dataclasses import datetime import functools import importlib @@ -146,39 +147,42 @@ def _default_retry_config() -> Retry: def _serialize_json(obj: Any) -> Any: - if obj is None: - return None - - basic_types = (bool, int, float, str, datetime.datetime, uuid.UUID) - if isinstance(obj, basic_types): - return obj.isoformat() if isinstance(obj, datetime.datetime) else str(obj) - - serialization_methods = [ - ("model_dump_json", True), # Base models, V2 - ("json", True), # Base models, V1 - ("to_json", False), # dataclass_json - ("dict", False), # dataclass - ] + if isinstance(obj, datetime.datetime): + return obj.isoformat() + if isinstance(obj, uuid.UUID): + return str(obj) + try: + serialization_methods = [ + ("model_dump_json", True), # Pydantic V2 + ("json", True), # Pydantic V1 + ("to_json", False), # dataclass_json + ] - for attr, exclude_none in serialization_methods: - if hasattr(obj, attr) and callable(getattr(obj, attr)): - try: - method = getattr(obj, attr) - json_str = ( - method(exclude_none=exclude_none) if exclude_none else method() - ) - return json.loads(json_str) - except Exception as e: - logger.debug(f"Failed to serialize {type(obj)} to JSON: {e}") - return repr(obj) + for attr, exclude_none in serialization_methods: + if hasattr(obj, attr) and callable(getattr(obj, attr)): + try: + method = getattr(obj, attr) + json_str = ( + method(exclude_none=exclude_none) if exclude_none else method() + ) + return json.loads(json_str) + except Exception as e: + logger.debug(f"Failed to serialize {type(obj)} to JSON: {e}") + return repr(obj) - try: - if hasattr(obj, "__slots__"): - return {slot: getattr(obj, slot) for slot in obj.__slots__} - else: - return vars(obj) + if dataclasses.is_dataclass(obj): + # Regular dataclass + return dataclasses.asdict(obj) + try: + if hasattr(obj, "__slots__"): + return {slot: getattr(obj, slot) for slot in obj.__slots__} + else: + return vars(obj) + except Exception as e: + logger.debug(f"Failed to serialize {type(obj)} to JSON using vars: {e}") + return repr(obj) except Exception as e: - logger.debug(f"Failed to serialize {type(obj)} to JSON using vars: {e}") + logger.debug(f"Failed to serialize {type(obj)} to JSON: {e}") return repr(obj) diff --git a/python/tests/unit_tests/test_client.py b/python/tests/unit_tests/test_client.py index e7226186f..dfa4bf484 100644 --- a/python/tests/unit_tests/test_client.py +++ b/python/tests/unit_tests/test_client.py @@ -7,10 +7,11 @@ from datetime import datetime from enum import Enum from io import BytesIO -from typing import Optional +from typing import NamedTuple, Optional from unittest import mock from unittest.mock import patch +import attr import dataclasses_json import pytest from pydantic import BaseModel @@ -302,6 +303,9 @@ class MyDataclass: foo: str bar: int + def something(self) -> None: + pass + class MyEnum(str, Enum): FOO = "foo" BAR = "bar" @@ -311,6 +315,11 @@ class MyEnum(str, Enum): class Person: name: str + @attr.dataclass + class AttrDict: + foo: str = attr.ib() + bar: int + uid = uuid.uuid4() current_time = datetime.now() @@ -320,6 +329,10 @@ class NestedClass: def __init__(self) -> None: self.person = Person(name="foo") + class MyNamedTuple(NamedTuple): + foo: str + bar: int + to_serialize = { "uid": uid, "time": current_time, @@ -335,6 +348,8 @@ def __init__(self) -> None: "an_int": 1, "a_float": 1.1, "nested_class": NestedClass(), + "attr_dict": AttrDict(foo="foo", bar=1), + "named_tuple": MyNamedTuple(foo="foo", bar=1), } res = json.loads(json.dumps(to_serialize, default=_serialize_json)) @@ -353,6 +368,8 @@ def __init__(self) -> None: "an_int": 1, "a_float": 1.1, "nested_class": {"person": {"name": "foo"}}, + "attr_dict": {"foo": "foo", "bar": 1}, + "named_tuple": ["foo", 1], } assert res == expected