diff --git a/libs/langchain/pyproject.toml b/libs/langchain/pyproject.toml index bb7d266e8db..5bc7acddb19 100644 --- a/libs/langchain/pyproject.toml +++ b/libs/langchain/pyproject.toml @@ -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", +] diff --git a/libs/langchain/tests/unit_tests/agents/test_agent.py b/libs/langchain/tests/unit_tests/agents/test_agent.py index a8ebcc658f6..0d5e0144583 100644 --- a/libs/langchain/tests/unit_tests/agents/test_agent.py +++ b/libs/langchain/tests/unit_tests/agents/test_agent.py @@ -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", diff --git a/libs/langchain/tests/unit_tests/conftest.py b/libs/langchain/tests/unit_tests/conftest.py index 4b7db03cbc1..fed8dbec503 100644 --- a/libs/langchain/tests/unit_tests/conftest.py +++ b/libs/langchain/tests/unit_tests/conftest.py @@ -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", "" + ) + + 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( diff --git a/libs/langchain/tests/unit_tests/llms/test_fake_chat_model.py b/libs/langchain/tests/unit_tests/llms/test_fake_chat_model.py index 9f33d73f32e..96c96263d1a 100644 --- a/libs/langchain/tests/unit_tests/llms/test_fake_chat_model.py +++ b/libs/langchain/tests/unit_tests/llms/test_fake_chat_model.py @@ -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=" "), diff --git a/libs/langchain/tests/unit_tests/test_dependencies.py b/libs/langchain/tests/unit_tests/test_dependencies.py index a029ea84788..c438ab2500f 100644 --- a/libs/langchain/tests/unit_tests/test_dependencies.py +++ b/libs/langchain/tests/unit_tests/test_dependencies.py @@ -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", diff --git a/libs/langchain/uv.lock b/libs/langchain/uv.lock index 6802c97a2c0..320901707f1 100644 --- a/libs/langchain/uv.lock +++ b/libs/langchain/uv.lock @@ -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" },