standard-tests: migrate to pytest-recording (#31425)

Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
This commit is contained in:
ccurme 2025-05-31 15:21:15 -04:00 committed by GitHub
parent d7f90f233b
commit 3db1aa0ba6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 185 additions and 119 deletions

View File

@ -1 +0,0 @@
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

View File

@ -1,7 +1,7 @@
from typing import Any from typing import Any
import pytest import pytest
from langchain_tests.conftest import YamlGzipSerializer from langchain_tests.conftest import CustomPersister, CustomSerializer
from langchain_tests.conftest import _base_vcr_config as _base_vcr_config from langchain_tests.conftest import _base_vcr_config as _base_vcr_config
from vcr import VCR # type: ignore[import-untyped] from vcr import VCR # type: ignore[import-untyped]
@ -32,9 +32,6 @@ def vcr_config(_base_vcr_config: dict) -> dict: # noqa: F811
return config return config
@pytest.fixture def pytest_recording_configure(config: dict, vcr: VCR) -> None:
def vcr(vcr_config: dict) -> VCR: vcr.register_persister(CustomPersister())
"""Override the default vcr fixture to include custom serializers""" vcr.register_serializer("yaml.gz", CustomSerializer())
my_vcr = VCR(**vcr_config)
my_vcr.register_serializer("yaml.gz", YamlGzipSerializer)
return my_vcr

View File

@ -502,7 +502,7 @@ typing = [
[[package]] [[package]]
name = "langchain-core" name = "langchain-core"
version = "0.3.62" version = "0.3.63"
source = { editable = "../../core" } source = { editable = "../../core" }
dependencies = [ dependencies = [
{ name = "jsonpatch" }, { name = "jsonpatch" },
@ -571,8 +571,8 @@ dependencies = [
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
{ name = "pytest-benchmark" }, { name = "pytest-benchmark" },
{ name = "pytest-codspeed" }, { name = "pytest-codspeed" },
{ name = "pytest-recording" },
{ name = "pytest-socket" }, { name = "pytest-socket" },
{ name = "pytest-vcr" },
{ name = "syrupy" }, { name = "syrupy" },
{ name = "vcrpy" }, { name = "vcrpy" },
] ]
@ -587,8 +587,8 @@ requires-dist = [
{ name = "pytest-asyncio", specifier = ">=0.20,<1" }, { name = "pytest-asyncio", specifier = ">=0.20,<1" },
{ name = "pytest-benchmark" }, { name = "pytest-benchmark" },
{ name = "pytest-codspeed" }, { name = "pytest-codspeed" },
{ name = "pytest-recording" },
{ name = "pytest-socket", specifier = ">=0.6.0,<1" }, { name = "pytest-socket", specifier = ">=0.6.0,<1" },
{ name = "pytest-vcr" },
{ name = "syrupy", specifier = ">=4,<5" }, { name = "syrupy", specifier = ">=4,<5" },
{ name = "vcrpy", specifier = ">=7.0" }, { name = "vcrpy", specifier = ">=7.0" },
] ]
@ -1339,6 +1339,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 },
] ]
[[package]]
name = "pytest-recording"
version = "0.13.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
{ name = "vcrpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/32/9c/f4027c5f1693847b06d11caf4b4f6bb09f22c1581ada4663877ec166b8c6/pytest_recording-0.13.4.tar.gz", hash = "sha256:568d64b2a85992eec4ae0a419c855d5fd96782c5fb016784d86f18053792768c", size = 26576 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/c2/ce34735972cc42d912173e79f200fe66530225190c06655c5632a9d88f1e/pytest_recording-0.13.4-py3-none-any.whl", hash = "sha256:ad49a434b51b1c4f78e85b1e6b74fdcc2a0a581ca16e52c798c6ace971f7f439", size = 13723 },
]
[[package]] [[package]]
name = "pytest-retry" name = "pytest-retry"
version = "1.7.0" version = "1.7.0"
@ -1375,19 +1388,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 }, { url = "https://files.pythonhosted.org/packages/03/27/14af9ef8321f5edc7527e47def2a21d8118c6f329a9342cc61387a0c0599/pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e", size = 14148 },
] ]
[[package]]
name = "pytest-vcr"
version = "1.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
{ name = "vcrpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/60/104c619483c1a42775d3f8b27293f1ecfc0728014874d065e68cb9702d49/pytest-vcr-1.0.2.tar.gz", hash = "sha256:23ee51b75abbcc43d926272773aae4f39f93aceb75ed56852d0bf618f92e1896", size = 3810 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/d3/ff520d11e6ee400602711d1ece8168dcfc5b6d8146fb7db4244a6ad6a9c3/pytest_vcr-1.0.2-py2.py3-none-any.whl", hash = "sha256:2f316e0539399bea0296e8b8401145c62b6f85e9066af7e57b6151481b0d6d9c", size = 4137 },
]
[[package]] [[package]]
name = "pytest-watcher" name = "pytest-watcher"
version = "0.4.3" version = "0.4.3"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
from typing import Any from typing import Any
import pytest import pytest
from langchain_tests.conftest import YamlGzipSerializer from langchain_tests.conftest import CustomPersister, CustomSerializer
from langchain_tests.conftest import _base_vcr_config as _base_vcr_config from langchain_tests.conftest import _base_vcr_config as _base_vcr_config
from vcr import VCR # type: ignore[import-untyped] from vcr import VCR # type: ignore[import-untyped]
@ -13,6 +13,7 @@ _EXTRA_HEADERS = [
def remove_request_headers(request: Any) -> Any: def remove_request_headers(request: Any) -> Any:
"""Remove sensitive headers from the request."""
for k in request.headers: for k in request.headers:
request.headers[k] = "**REDACTED**" request.headers[k] = "**REDACTED**"
request.uri = "**REDACTED**" request.uri = "**REDACTED**"
@ -20,6 +21,7 @@ def remove_request_headers(request: Any) -> Any:
def remove_response_headers(response: dict) -> dict: def remove_response_headers(response: dict) -> dict:
"""Remove sensitive headers from the response."""
for k in response["headers"]: for k in response["headers"]:
response["headers"][k] = "**REDACTED**" response["headers"][k] = "**REDACTED**"
return response return response
@ -27,22 +29,16 @@ def remove_response_headers(response: dict) -> dict:
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def vcr_config(_base_vcr_config: dict) -> dict: # noqa: F811 def vcr_config(_base_vcr_config: dict) -> dict: # noqa: F811
""" """Extend the default configuration coming from langchain_tests."""
Extend the default configuration coming from langchain_tests.
"""
config = _base_vcr_config.copy() config = _base_vcr_config.copy()
config.setdefault("filter_headers", []).extend(_EXTRA_HEADERS) config.setdefault("filter_headers", []).extend(_EXTRA_HEADERS)
config["before_record_request"] = remove_request_headers config["before_record_request"] = remove_request_headers
config["before_record_response"] = remove_response_headers config["before_record_response"] = remove_response_headers
config["serializer"] = "yaml.gz" config["serializer"] = "yaml.gz"
config["path_transformer"] = VCR.ensure_suffix(".yaml.gz") config["path_transformer"] = VCR.ensure_suffix(".yaml.gz")
return config return config
@pytest.fixture def pytest_recording_configure(config: dict, vcr: VCR) -> None:
def vcr(vcr_config: dict) -> VCR: vcr.register_persister(CustomPersister())
"""Override the default vcr fixture to include custom serializers""" vcr.register_serializer("yaml.gz", CustomSerializer())
my_vcr = VCR(**vcr_config)
my_vcr.register_serializer("yaml.gz", YamlGzipSerializer)
return my_vcr

View File

@ -479,7 +479,7 @@ wheels = [
[[package]] [[package]]
name = "langchain-core" name = "langchain-core"
version = "0.3.62" version = "0.3.63"
source = { editable = "../../core" } source = { editable = "../../core" }
dependencies = [ dependencies = [
{ name = "jsonpatch" }, { name = "jsonpatch" },
@ -637,8 +637,8 @@ dependencies = [
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
{ name = "pytest-benchmark" }, { name = "pytest-benchmark" },
{ name = "pytest-codspeed" }, { name = "pytest-codspeed" },
{ name = "pytest-recording" },
{ name = "pytest-socket" }, { name = "pytest-socket" },
{ name = "pytest-vcr" },
{ name = "syrupy" }, { name = "syrupy" },
{ name = "vcrpy" }, { name = "vcrpy" },
] ]
@ -653,8 +653,8 @@ requires-dist = [
{ name = "pytest-asyncio", specifier = ">=0.20,<1" }, { name = "pytest-asyncio", specifier = ">=0.20,<1" },
{ name = "pytest-benchmark" }, { name = "pytest-benchmark" },
{ name = "pytest-codspeed" }, { name = "pytest-codspeed" },
{ name = "pytest-recording" },
{ name = "pytest-socket", specifier = ">=0.6.0,<1" }, { name = "pytest-socket", specifier = ">=0.6.0,<1" },
{ name = "pytest-vcr" },
{ name = "syrupy", specifier = ">=4,<5" }, { name = "syrupy", specifier = ">=4,<5" },
{ name = "vcrpy", specifier = ">=7.0" }, { name = "vcrpy", specifier = ">=7.0" },
] ]
@ -1514,6 +1514,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 }, { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863 },
] ]
[[package]]
name = "pytest-recording"
version = "0.13.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
{ name = "vcrpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/32/9c/f4027c5f1693847b06d11caf4b4f6bb09f22c1581ada4663877ec166b8c6/pytest_recording-0.13.4.tar.gz", hash = "sha256:568d64b2a85992eec4ae0a419c855d5fd96782c5fb016784d86f18053792768c", size = 26576 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/c2/ce34735972cc42d912173e79f200fe66530225190c06655c5632a9d88f1e/pytest_recording-0.13.4-py3-none-any.whl", hash = "sha256:ad49a434b51b1c4f78e85b1e6b74fdcc2a0a581ca16e52c798c6ace971f7f439", size = 13723 },
]
[[package]] [[package]]
name = "pytest-retry" name = "pytest-retry"
version = "1.7.0" version = "1.7.0"
@ -1538,19 +1551,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/19/58/5d14cb5cb59409e491ebe816c47bf81423cd03098ea92281336320ae5681/pytest_socket-0.7.0-py3-none-any.whl", hash = "sha256:7e0f4642177d55d317bbd58fc68c6bd9048d6eadb2d46a89307fa9221336ce45", size = 6754 }, { url = "https://files.pythonhosted.org/packages/19/58/5d14cb5cb59409e491ebe816c47bf81423cd03098ea92281336320ae5681/pytest_socket-0.7.0-py3-none-any.whl", hash = "sha256:7e0f4642177d55d317bbd58fc68c6bd9048d6eadb2d46a89307fa9221336ce45", size = 6754 },
] ]
[[package]]
name = "pytest-vcr"
version = "1.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
{ name = "vcrpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/60/104c619483c1a42775d3f8b27293f1ecfc0728014874d065e68cb9702d49/pytest-vcr-1.0.2.tar.gz", hash = "sha256:23ee51b75abbcc43d926272773aae4f39f93aceb75ed56852d0bf618f92e1896", size = 3810 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/d3/ff520d11e6ee400602711d1ece8168dcfc5b6d8146fb7db4244a6ad6a9c3/pytest_vcr-1.0.2-py2.py3-none-any.whl", hash = "sha256:2f316e0539399bea0296e8b8401145c62b6f85e9066af7e57b6151481b0d6d9c", size = 4137 },
]
[[package]] [[package]]
name = "pytest-watcher" name = "pytest-watcher"
version = "0.4.3" version = "0.4.3"

View File

@ -1,25 +1,85 @@
import base64
import gzip import gzip
from os import PathLike
from pathlib import Path
from typing import Union
import pytest import pytest
from vcr import VCR # type: ignore[import-untyped] import yaml
from vcr.serializers import yamlserializer # type: ignore[import-untyped] from vcr import VCR
from vcr.persisters.filesystem import CassetteNotFoundError
from vcr.request import Request
class YamlGzipSerializer: class CustomSerializer:
@staticmethod """Custom serializer for VCR cassettes using YAML and gzip.
def serialize(cassette_dict: dict) -> str:
raw = yamlserializer.serialize(cassette_dict).encode("utf-8") We're using a custom serializer to avoid the default yaml serializer
compressed = gzip.compress(raw) used by VCR, which is not designed to be safe for untrusted input.
return base64.b64encode(compressed).decode("ascii")
This step is an extra precaution necessary because the cassette files
are in compressed YAML format, which makes it more difficult to inspect
their contents during development or debugging.
"""
@staticmethod @staticmethod
def deserialize(data: str) -> dict: def serialize(cassette_dict: dict) -> bytes:
compressed = base64.b64decode(data.encode("ascii")) """Convert cassette to YAML and compress it."""
text = gzip.decompress(compressed).decode("utf-8") cassette_dict["requests"] = [
return yamlserializer.deserialize(text) request._to_dict() for request in cassette_dict["requests"]
]
yml = yaml.safe_dump(cassette_dict)
return gzip.compress(yml.encode("utf-8"))
@staticmethod
def deserialize(data: bytes) -> dict:
"""Decompress data and convert it from YAML."""
text = gzip.decompress(data).decode("utf-8")
cassette = yaml.safe_load(text)
cassette["requests"] = [
Request._from_dict(request) for request in cassette["requests"]
]
return cassette
class CustomPersister:
"""A custom persister for VCR that uses the CustomSerializer."""
@classmethod
def load_cassette(
cls, cassette_path: Union[str, PathLike[str]], serializer: CustomSerializer
) -> tuple[dict, dict]:
"""Load a cassette from a file."""
# If cassette path is already Path this is a no-op
cassette_path = Path(cassette_path)
if not cassette_path.is_file():
raise CassetteNotFoundError(
f"Cassette file {cassette_path} does not exist."
)
with cassette_path.open(mode="rb") as f:
data = f.read()
deser = serializer.deserialize(data)
return deser["requests"], deser["responses"]
@staticmethod
def save_cassette(
cassette_path: Union[str, PathLike[str]],
cassette_dict: dict,
serializer: CustomSerializer,
) -> None:
"""Save a cassette to a file."""
data = serializer.serialize(cassette_dict)
# if cassette path is already Path this is no operation
cassette_path = Path(cassette_path)
cassette_folder = cassette_path.parent
if not cassette_folder.exists():
cassette_folder.mkdir(parents=True)
with cassette_path.open("wb") as f:
f.write(data)
# A list of headers that should be filtered out of the cassettes.
# These are typically associated with sensitive information and should
# not be stored in cassettes.
_BASE_FILTER_HEADERS = [ _BASE_FILTER_HEADERS = [
("authorization", "PLACEHOLDER"), ("authorization", "PLACEHOLDER"),
("x-api-key", "PLACEHOLDER"), ("x-api-key", "PLACEHOLDER"),
@ -29,14 +89,15 @@ _BASE_FILTER_HEADERS = [
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def _base_vcr_config() -> dict: def _base_vcr_config() -> dict:
""" """Configuration that every cassette will receive.
Configuration that every cassette will receive.
(Anything permitted by vcr.VCR(**kwargs) can be put here.) (Anything permitted by vcr.VCR(**kwargs) can be put here.)
""" """
return { return {
"record_mode": "once", "record_mode": "once",
"filter_headers": _BASE_FILTER_HEADERS.copy(), "filter_headers": _BASE_FILTER_HEADERS.copy(),
"match_on": ["method", "scheme", "host", "port", "path", "query"], "match_on": ["method", "uri", "body"],
"allow_playback_repeats": True,
"decode_compressed_response": True, "decode_compressed_response": True,
"cassette_library_dir": "tests/cassettes", "cassette_library_dir": "tests/cassettes",
"path_transformer": VCR.ensure_suffix(".yaml"), "path_transformer": VCR.ensure_suffix(".yaml"),

View File

@ -6,7 +6,6 @@ from unittest.mock import MagicMock
import httpx import httpx
import pytest import pytest
import vcr # type: ignore[import-untyped]
from langchain_core._api import warn_deprecated from langchain_core._api import warn_deprecated
from langchain_core.callbacks import BaseCallbackHandler from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.language_models import BaseChatModel, GenericFakeChatModel from langchain_core.language_models import BaseChatModel, GenericFakeChatModel
@ -31,6 +30,7 @@ from pydantic.v1 import BaseModel as BaseModelV1
from pydantic.v1 import Field as FieldV1 from pydantic.v1 import Field as FieldV1
from pytest_benchmark.fixture import BenchmarkFixture # type: ignore[import-untyped] from pytest_benchmark.fixture import BenchmarkFixture # type: ignore[import-untyped]
from typing_extensions import Annotated, TypedDict from typing_extensions import Annotated, TypedDict
from vcr.cassette import Cassette
from langchain_tests.unit_tests.chat_models import ( from langchain_tests.unit_tests.chat_models import (
ChatModelTests, ChatModelTests,
@ -592,7 +592,7 @@ class ChatModelIntegrationTests(ChatModelTests):
:caption: tests/conftest.py :caption: tests/conftest.py
import pytest import pytest
from langchain_tests.conftest import YamlGzipSerializer from langchain_tests.conftest import CustomPersister, CustomSerializer
from langchain_tests.conftest import _base_vcr_config as _base_vcr_config from langchain_tests.conftest import _base_vcr_config as _base_vcr_config
from vcr import VCR from vcr import VCR
@ -621,24 +621,26 @@ class ChatModelIntegrationTests(ChatModelTests):
return config return config
@pytest.fixture def pytest_recording_configure(config: dict, vcr: VCR) -> None:
def vcr(vcr_config: dict) -> VCR: vcr.register_persister(CustomPersister())
\"\"\"Override the default vcr fixture to include custom serializers\"\"\" vcr.register_serializer("yaml.gz", CustomSerializer())
my_vcr = VCR(**vcr_config)
my_vcr.register_serializer("yaml.gz", YamlGzipSerializer)
return my_vcr
You can inspect the contents of the compressed cassettes (e.g., to You can inspect the contents of the compressed cassettes (e.g., to
ensure no sensitive information is recorded) using the serializer: ensure no sensitive information is recorded) using
.. code-block:: bash
gunzip -k /path/to/tests/cassettes/TestClass_test.yaml.gz
or by using the serializer:
.. code-block:: python .. code-block:: python
from langchain_tests.conftest import YamlGzipSerializer from langchain_tests.conftest import CustomPersister, CustomSerializer
with open("/path/to/tests/cassettes/TestClass_test.yaml.gz", "r") as f: cassette_path = "/path/to/tests/cassettes/TestClass_test.yaml.gz"
data = f.read() requests, responses = CustomPersister().load_cassette(path, CustomSerializer())
YamlGzipSerializer.deserialize(data)
3. Run tests to generate VCR cassettes. 3. Run tests to generate VCR cassettes.
@ -2826,8 +2828,9 @@ class ChatModelIntegrationTests(ChatModelTests):
assert isinstance(response, AIMessage) assert isinstance(response, AIMessage)
@pytest.mark.benchmark @pytest.mark.benchmark
@pytest.mark.vcr
def test_stream_time( def test_stream_time(
self, model: BaseChatModel, benchmark: BenchmarkFixture, vcr: vcr.VCR self, model: BaseChatModel, benchmark: BenchmarkFixture, vcr: Cassette
) -> None: ) -> None:
"""Test that streaming does not introduce undue overhead. """Test that streaming does not introduce undue overhead.
@ -2857,11 +2860,12 @@ class ChatModelIntegrationTests(ChatModelTests):
pytest.skip("VCR not set up.") pytest.skip("VCR not set up.")
def _run() -> None: def _run() -> None:
cassette_name = f"{self.__class__.__name__}_test_stream_time"
with vcr.use_cassette(cassette_name, record_mode="once"):
for _ in model.stream("Write a story about a cat."): for _ in model.stream("Write a story about a cat."):
pass pass
if not vcr.responses:
_run()
else:
benchmark(_run) benchmark(_run)
def invoke_with_audio_input(self, *, stream: bool = False) -> AIMessage: def invoke_with_audio_input(self, *, stream: bool = False) -> AIMessage:

View File

@ -693,7 +693,7 @@ class ChatModelUnitTests(ChatModelTests):
:caption: tests/conftest.py :caption: tests/conftest.py
import pytest import pytest
from langchain_tests.conftest import YamlGzipSerializer from langchain_tests.conftest import CustomPersister, CustomSerializer
from langchain_tests.conftest import _base_vcr_config as _base_vcr_config from langchain_tests.conftest import _base_vcr_config as _base_vcr_config
from vcr import VCR from vcr import VCR
@ -722,24 +722,26 @@ class ChatModelUnitTests(ChatModelTests):
return config return config
@pytest.fixture def pytest_recording_configure(config: dict, vcr: VCR) -> None:
def vcr(vcr_config: dict) -> VCR: vcr.register_persister(CustomPersister())
\"\"\"Override the default vcr fixture to include custom serializers\"\"\" vcr.register_serializer("yaml.gz", CustomSerializer())
my_vcr = VCR(**vcr_config)
my_vcr.register_serializer("yaml.gz", YamlGzipSerializer)
return my_vcr
You can inspect the contents of the compressed cassettes (e.g., to You can inspect the contents of the compressed cassettes (e.g., to
ensure no sensitive information is recorded) using the serializer: ensure no sensitive information is recorded) using
.. code-block:: bash
gunzip -k /path/to/tests/cassettes/TestClass_test.yaml.gz
or by using the serializer:
.. code-block:: python .. code-block:: python
from langchain_tests.conftest import YamlGzipSerializer from langchain_tests.conftest import CustomPersister, CustomSerializer
with open("/path/to/tests/cassettes/TestClass_test.yaml.gz", "r") as f: cassette_path = "/path/to/tests/cassettes/TestClass_test.yaml.gz"
data = f.read() requests, responses = CustomPersister().load_cassette(path, CustomSerializer())
YamlGzipSerializer.deserialize(data)
3. Run tests to generate VCR cassettes. 3. Run tests to generate VCR cassettes.

View File

@ -15,7 +15,7 @@ dependencies = [
"pytest-socket<1,>=0.6.0", "pytest-socket<1,>=0.6.0",
"pytest-benchmark", "pytest-benchmark",
"pytest-codspeed", "pytest-codspeed",
"pytest-vcr", "pytest-recording",
"vcrpy>=7.0", "vcrpy>=7.0",
"numpy>=1.26.2; python_version<'3.13'", "numpy>=1.26.2; python_version<'3.13'",
"numpy>=2.1.0; python_version>='3.13'", "numpy>=2.1.0; python_version>='3.13'",
@ -42,6 +42,15 @@ langchain-core = { path = "../core", editable = true }
[tool.mypy] [tool.mypy]
disallow_untyped_defs = "True" disallow_untyped_defs = "True"
[[tool.mypy.overrides]]
module = "yaml"
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "vcr.*"
ignore_missing_imports = true
[tool.ruff] [tool.ruff]
target-version = "py39" target-version = "py39"

View File

@ -304,7 +304,7 @@ wheels = [
[[package]] [[package]]
name = "langchain-core" name = "langchain-core"
version = "0.3.60" version = "0.3.63"
source = { editable = "../core" } source = { editable = "../core" }
dependencies = [ dependencies = [
{ name = "jsonpatch" }, { name = "jsonpatch" },
@ -373,8 +373,8 @@ dependencies = [
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
{ name = "pytest-benchmark" }, { name = "pytest-benchmark" },
{ name = "pytest-codspeed" }, { name = "pytest-codspeed" },
{ name = "pytest-recording" },
{ name = "pytest-socket" }, { name = "pytest-socket" },
{ name = "pytest-vcr" },
{ name = "syrupy" }, { name = "syrupy" },
{ name = "vcrpy" }, { name = "vcrpy" },
] ]
@ -404,8 +404,8 @@ requires-dist = [
{ name = "pytest-asyncio", specifier = ">=0.20,<1" }, { name = "pytest-asyncio", specifier = ">=0.20,<1" },
{ name = "pytest-benchmark" }, { name = "pytest-benchmark" },
{ name = "pytest-codspeed" }, { name = "pytest-codspeed" },
{ name = "pytest-recording" },
{ name = "pytest-socket", specifier = ">=0.6.0,<1" }, { name = "pytest-socket", specifier = ">=0.6.0,<1" },
{ name = "pytest-vcr" },
{ name = "syrupy", specifier = ">=4,<5" }, { name = "syrupy", specifier = ">=4,<5" },
{ name = "vcrpy", specifier = ">=7.0" }, { name = "vcrpy", specifier = ">=7.0" },
] ]
@ -1153,6 +1153,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/9b/952c70bd1fae9baa58077272e7f191f377c86d812263c21b361195e125e6/pytest_codspeed-3.2.0-py3-none-any.whl", hash = "sha256:54b5c2e986d6a28e7b0af11d610ea57bd5531cec8326abe486f1b55b09d91c39", size = 15007 }, { url = "https://files.pythonhosted.org/packages/f1/9b/952c70bd1fae9baa58077272e7f191f377c86d812263c21b361195e125e6/pytest_codspeed-3.2.0-py3-none-any.whl", hash = "sha256:54b5c2e986d6a28e7b0af11d610ea57bd5531cec8326abe486f1b55b09d91c39", size = 15007 },
] ]
[[package]]
name = "pytest-recording"
version = "0.13.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
{ name = "vcrpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/32/9c/f4027c5f1693847b06d11caf4b4f6bb09f22c1581ada4663877ec166b8c6/pytest_recording-0.13.4.tar.gz", hash = "sha256:568d64b2a85992eec4ae0a419c855d5fd96782c5fb016784d86f18053792768c", size = 26576 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/c2/ce34735972cc42d912173e79f200fe66530225190c06655c5632a9d88f1e/pytest_recording-0.13.4-py3-none-any.whl", hash = "sha256:ad49a434b51b1c4f78e85b1e6b74fdcc2a0a581ca16e52c798c6ace971f7f439", size = 13723 },
]
[[package]] [[package]]
name = "pytest-socket" name = "pytest-socket"
version = "0.7.0" version = "0.7.0"
@ -1165,19 +1178,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/19/58/5d14cb5cb59409e491ebe816c47bf81423cd03098ea92281336320ae5681/pytest_socket-0.7.0-py3-none-any.whl", hash = "sha256:7e0f4642177d55d317bbd58fc68c6bd9048d6eadb2d46a89307fa9221336ce45", size = 6754 }, { url = "https://files.pythonhosted.org/packages/19/58/5d14cb5cb59409e491ebe816c47bf81423cd03098ea92281336320ae5681/pytest_socket-0.7.0-py3-none-any.whl", hash = "sha256:7e0f4642177d55d317bbd58fc68c6bd9048d6eadb2d46a89307fa9221336ce45", size = 6754 },
] ]
[[package]]
name = "pytest-vcr"
version = "1.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
{ name = "vcrpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1a/60/104c619483c1a42775d3f8b27293f1ecfc0728014874d065e68cb9702d49/pytest-vcr-1.0.2.tar.gz", hash = "sha256:23ee51b75abbcc43d926272773aae4f39f93aceb75ed56852d0bf618f92e1896", size = 3810 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/d3/ff520d11e6ee400602711d1ece8168dcfc5b6d8146fb7db4244a6ad6a9c3/pytest_vcr-1.0.2-py2.py3-none-any.whl", hash = "sha256:2f316e0539399bea0296e8b8401145c62b6f85e9066af7e57b6151481b0d6d9c", size = 4137 },
]
[[package]] [[package]]
name = "pyyaml" name = "pyyaml"
version = "6.0.2" version = "6.0.2"