langchain: Use Blockbuster to detect blocking calls in asyncio during tests (#29616)

Same as https://github.com/langchain-ai/langchain/pull/29043 for the
langchain package.

**Dependencies:**
- blockbuster (test)

**Twitter handle:** cbornet_

---------

Co-authored-by: Erick Friis <erick@langchain.dev>
This commit is contained in:
Christophe Bornet 2025-02-08 02:08:15 +01:00 committed by GitHub
parent c67d473397
commit 3a57a28daa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 84 additions and 22 deletions

View File

@ -4,7 +4,7 @@ build-backend = "pdm.backend"
[project]
authors = []
license = {text = "MIT"}
license = { text = "MIT" }
requires-python = "<4.0,>=3.9"
dependencies = [
"langchain-core<1.0.0,>=0.3.34",
@ -66,6 +66,7 @@ test = [
"syrupy<5.0.0,>=4.0.2",
"requests-mock<2.0.0,>=1.11.0",
"pytest-xdist<4.0.0,>=3.6.1",
"blockbuster<1.6,>=1.5.14",
"cffi<1.17.1; python_version < \"3.10\"",
"cffi; python_version >= \"3.10\"",
"langchain-tests @ file:///${PROJECT_ROOT}/../standard-tests",
@ -75,9 +76,7 @@ test = [
"toml>=0.10.2",
"packaging>=24.2",
]
codespell = [
"codespell<3.0.0,>=2.2.0",
]
codespell = ["codespell<3.0.0,>=2.2.0"]
test_integration = [
"pytest-vcr<2.0.0,>=1.0.2",
"urllib3<2; python_version < \"3.10\"",
@ -116,12 +115,12 @@ dev = [
[tool.ruff]
target-version = "py39"
exclude = [ "tests/integration_tests/examples/non-utf8-encoding.py",]
exclude = ["tests/integration_tests/examples/non-utf8-encoding.py"]
[tool.mypy]
ignore_missing_imports = "True"
disallow_untyped_defs = "True"
exclude = [ "notebooks", "examples", "example_data",]
exclude = ["notebooks", "examples", "example_data"]
[tool.codespell]
skip = ".git,*.pdf,*.svg,*.pdf,*.yaml,*.ipynb,poetry.lock,*.min.js,*.css,package-lock.json,example_data,_dist,examples,*.trig"
@ -129,7 +128,7 @@ ignore-regex = ".*(Stati Uniti|Tense=Pres).*"
ignore-words-list = "momento,collison,ned,foor,reworkd,parth,whats,aapply,mysogyny,unsecure,damon,crate,aadd,symbl,precesses,accademia,nin"
[tool.ruff.lint]
select = [ "E", "F", "I", "T201", "D",]
select = ["E", "F", "I", "T201", "D"]
pydocstyle = { convention = "google" }
[tool.ruff.lint.per-file-ignores]
@ -137,10 +136,18 @@ pydocstyle = { convention = "google" }
"!langchain/indexes/vectorstore.py" = ["D"]
[tool.coverage.run]
omit = [ "tests/*",]
omit = ["tests/*"]
[tool.pytest.ini_options]
addopts = "--strict-markers --strict-config --durations=5 --snapshot-warn-unused -vv"
markers = [ "requires: mark tests as requiring a specific library", "scheduled: mark tests to run in scheduled testing", "compile: mark placeholder test used to compile integration tests without running them",]
markers = [
"requires: mark tests as requiring a specific library",
"scheduled: mark tests to run in scheduled testing",
"compile: mark placeholder test used to compile integration tests without running them",
]
asyncio_mode = "auto"
filterwarnings = [ "ignore::langchain_core._api.beta_decorator.LangChainBetaWarning", "ignore::langchain_core._api.deprecation.LangChainDeprecationWarning:tests", "ignore::langchain_core._api.deprecation.LangChainPendingDeprecationWarning:tests",]
filterwarnings = [
"ignore::langchain_core._api.beta_decorator.LangChainBetaWarning",
"ignore::langchain_core._api.deprecation.LangChainDeprecationWarning:tests",
"ignore::langchain_core._api.deprecation.LangChainPendingDeprecationWarning:tests",
]

View File

@ -1,5 +1,6 @@
"""Unit tests for agents."""
import asyncio
import json
from itertools import cycle
from typing import Any, Dict, List, Optional, Union, cast
@ -465,7 +466,7 @@ async def test_runnable_agent() -> None:
executor = AgentExecutor(agent=agent, tools=[]) # type: ignore[arg-type]
# Invoke
result = executor.invoke({"question": "hello"})
result: Any = await asyncio.to_thread(executor.invoke, {"question": "hello"})
assert result == {"foo": "meow", "question": "hello"}
# ainvoke
@ -473,8 +474,8 @@ async def test_runnable_agent() -> None:
assert result == {"foo": "meow", "question": "hello"}
# Batch
result = executor.batch( # type: ignore[assignment]
[{"question": "hello"}, {"question": "hello"}]
result = await asyncio.to_thread(
executor.batch, [{"question": "hello"}, {"question": "hello"}]
)
assert result == [
{"foo": "meow", "question": "hello"},
@ -482,16 +483,14 @@ async def test_runnable_agent() -> None:
]
# abatch
result = await executor.abatch( # type: ignore[assignment]
[{"question": "hello"}, {"question": "hello"}]
)
result = await executor.abatch([{"question": "hello"}, {"question": "hello"}])
assert result == [
{"foo": "meow", "question": "hello"},
{"foo": "meow", "question": "hello"},
]
# Stream
results = list(executor.stream({"question": "hello"}))
results = await asyncio.to_thread(list, executor.stream({"question": "hello"}))
assert results == [
{"foo": "meow", "messages": [AIMessage(content="hard-coded-message")]}
]
@ -587,7 +586,7 @@ async def test_runnable_agent_with_function_calls() -> None:
executor = AgentExecutor(agent=agent, tools=[find_pet]) # type: ignore[arg-type, list-item]
# Invoke
result = executor.invoke({"question": "hello"})
result = await asyncio.to_thread(executor.invoke, {"question": "hello"})
assert result == {"foo": "meow", "question": "hello"}
# ainvoke
@ -705,7 +704,7 @@ async def test_runnable_with_multi_action_per_step() -> None:
executor = AgentExecutor(agent=agent, tools=[find_pet]) # type: ignore[arg-type, list-item]
# Invoke
result = executor.invoke({"question": "hello"})
result = await asyncio.to_thread(executor.invoke, {"question": "hello"})
assert result == {"foo": "meow", "question": "hello"}
# ainvoke
@ -859,7 +858,7 @@ async def test_openai_agent_with_streaming() -> None:
executor = AgentExecutor(agent=agent, tools=[find_pet]) # type: ignore[arg-type, list-item]
# Invoke
result = executor.invoke({"question": "hello"})
result = await asyncio.to_thread(executor.invoke, {"question": "hello"})
assert result == {
"output": "The cat is spying from under the bed.",
"question": "hello",
@ -1066,7 +1065,7 @@ async def test_openai_agent_tools_agent() -> None:
executor = AgentExecutor(agent=agent, tools=[find_pet]) # type: ignore[arg-type, list-item]
# Invoke
result = executor.invoke({"question": "hello"})
result = await asyncio.to_thread(executor.invoke, {"question": "hello"})
assert result == {
"output": "The cat is spying from under the bed.",
"question": "hello",

View File

@ -1,12 +1,42 @@
"""Configuration for unit tests."""
from collections.abc import Iterator
from importlib import util
from typing import Dict, Sequence
import pytest
from blockbuster import blockbuster_ctx
from pytest import Config, Function, Parser
@pytest.fixture(autouse=True)
def blockbuster() -> Iterator[None]:
with blockbuster_ctx("langchain") as bb:
bb.functions["io.TextIOWrapper.read"].can_block_in(
"langchain/__init__.py", "<module>"
)
for func in ["os.stat", "os.path.abspath"]:
(
bb.functions[func]
.can_block_in("langchain_core/runnables/base.py", "__repr__")
.can_block_in(
"langchain_core/beta/runnables/context.py", "aconfig_with_context"
)
)
for func in ["os.stat", "io.TextIOWrapper.read"]:
bb.functions[func].can_block_in(
"langsmith/client.py", "_default_retry_config"
)
for bb_function in bb.functions.values():
bb_function.can_block_in(
"freezegun/api.py", "_get_cached_module_attributes"
)
yield
def pytest_addoption(parser: Parser) -> None:
"""Add custom command line options to pytest."""
parser.addoption(

View File

@ -184,7 +184,12 @@ async def test_callback_handlers() -> None:
model = GenericFakeChatModel(messages=infinite_cycle)
tokens: List[str] = []
# New model
results = list(model.stream("meow", {"callbacks": [MyCustomAsyncHandler(tokens)]}))
results = [
chunk
async for chunk in model.astream(
"meow", {"callbacks": [MyCustomAsyncHandler(tokens)]}
)
]
assert results == [
_AnyIdAIMessageChunk(content="hello"),
_AnyIdAIMessageChunk(content=" "),

View File

@ -77,6 +77,7 @@ def test_test_group_dependencies(uv_conf: Mapping[str, Any]) -> None:
"pytest-socket",
"pytest-watcher",
"pytest-xdist",
"blockbuster",
"responses",
"syrupy",
"toml",

View File

@ -305,6 +305,18 @@ css = [
{ name = "tinycss2" },
]
[[package]]
name = "blockbuster"
version = "1.5.14"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "forbiddenfruit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/77/a46b97dc6807c88c864a134793d7c7b915dea45c7e44da6c3adebac90501/blockbuster-1.5.14.tar.gz", hash = "sha256:d77ed3b931b058b4e746f65e32ea21e8ed21a4ef0ca88b7bb046bdb057e1adb0", size = 50191 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/c2/1515ea61aa08f3b44882aa59a0c03be667a6fec2a4026aad76944b40b030/blockbuster-1.5.14-py3-none-any.whl", hash = "sha256:5b5e46ac4b5f5d2a7a599944d83bee0c9eb46509868acb6d8fbc7c8058769aaf", size = 12372 },
]
[[package]]
name = "boto3"
version = "1.36.12"
@ -1053,6 +1065,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0d/47/db86fba6ef53da08488917a18cbe55b913c8b60275a7f20484ffc73a969c/fireworks_ai-0.15.12-py3-none-any.whl", hash = "sha256:3fbf3f89e65ccfc46c88b71246b9d4fdf3301955ac4050193d8f4b4058cb193a", size = 111129 },
]
[[package]]
name = "forbiddenfruit"
version = "0.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/79/d4f20e91327c98096d605646bdc6a5ffedae820f38d378d3515c42ec5e60/forbiddenfruit-0.1.4.tar.gz", hash = "sha256:e3f7e66561a29ae129aac139a85d610dbf3dd896128187ed5454b6421f624253", size = 43756 }
[[package]]
name = "fqdn"
version = "1.5.1"
@ -2289,6 +2307,7 @@ lint = [
{ name = "ruff" },
]
test = [
{ name = "blockbuster" },
{ name = "cffi", version = "1.17.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
{ name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "duckdb-engine" },
@ -2382,6 +2401,7 @@ lint = [
{ name = "ruff", specifier = ">=0.9.2,<1.0.0" },
]
test = [
{ name = "blockbuster", specifier = ">=1.5.14,<1.6" },
{ name = "cffi", marker = "python_full_version < '3.10'", specifier = "<1.17.1" },
{ name = "cffi", marker = "python_full_version >= '3.10'" },
{ name = "duckdb-engine", specifier = ">=0.9.2,<1.0.0" },