feat(langchain): add ruff rules DTZ (#32021)

See https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz
This commit is contained in:
Christophe Bornet 2025-07-14 18:47:16 +02:00 committed by GitHub
parent 26c2c8f70a
commit 19fff8cba9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 63 additions and 53 deletions

View File

@ -157,7 +157,7 @@ class QueryTransformer(Transformer):
def date(self, item: Any) -> ISO8601Date: def date(self, item: Any) -> ISO8601Date:
item = str(item).strip("\"'") item = str(item).strip("\"'")
try: try:
datetime.datetime.strptime(item, "%Y-%m-%d") datetime.datetime.strptime(item, "%Y-%m-%d") # noqa: DTZ007
except ValueError: except ValueError:
warnings.warn( warnings.warn(
"Dates are expected to be provided in ISO 8601 date format " "Dates are expected to be provided in ISO 8601 date format "
@ -173,7 +173,7 @@ class QueryTransformer(Transformer):
datetime.datetime.strptime(item, "%Y-%m-%dT%H:%M:%S%z") datetime.datetime.strptime(item, "%Y-%m-%dT%H:%M:%S%z")
except ValueError: except ValueError:
try: try:
datetime.datetime.strptime(item, "%Y-%m-%dT%H:%M:%S") datetime.datetime.strptime(item, "%Y-%m-%dT%H:%M:%S") # noqa: DTZ007
except ValueError as e: except ValueError as e:
msg = "Datetime values are expected to be in ISO 8601 format." msg = "Datetime values are expected to be in ISO 8601 format."
raise ValueError(msg) from e raise ValueError(msg) from e

View File

@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from langchain_core.exceptions import OutputParserException from langchain_core.exceptions import OutputParserException
from langchain_core.output_parsers import BaseOutputParser from langchain_core.output_parsers import BaseOutputParser
@ -26,7 +26,7 @@ class DatetimeOutputParser(BaseOutputParser[datetime]):
) )
else: else:
try: try:
now = datetime.now() now = datetime.now(tz=timezone.utc)
examples = comma_list( examples = comma_list(
[ [
now.strftime(self.format), now.strftime(self.format),
@ -48,7 +48,7 @@ class DatetimeOutputParser(BaseOutputParser[datetime]):
def parse(self, response: str) -> datetime: def parse(self, response: str) -> datetime:
"""Parse a string into a datetime object.""" """Parse a string into a datetime object."""
try: try:
return datetime.strptime(response.strip(), self.format) return datetime.strptime(response.strip(), self.format) # noqa: DTZ007
except ValueError as e: except ValueError as e:
msg = f"Could not parse datetime string: {response}" msg = f"Could not parse datetime string: {response}"
raise OutputParserException(msg) from e raise OutputParserException(msg) from e

View File

@ -152,6 +152,7 @@ select = [
"COM", # flake8-commas "COM", # flake8-commas
"D", # pydocstyle "D", # pydocstyle
"DOC", # pydoclint "DOC", # pydoclint
"DTZ", # flake8-datetimez
"E", # pycodestyle error "E", # pycodestyle error
"EM", # flake8-errmsg "EM", # flake8-errmsg
"F", # pyflakes "F", # pyflakes
@ -217,6 +218,11 @@ pyupgrade.keep-runtime-typing = true
"langchain/chains/constitutional_ai/principles.py" = [ "langchain/chains/constitutional_ai/principles.py" = [
"E501", # Line too long "E501", # Line too long
] ]
"**/retrievers/*time_weighted_retriever.py" = [
"DTZ001", # Use of non timezone-aware datetime
"DTZ005", # Use of non timezone-aware datetime
"DTZ006", # Use of non timezone-aware datetime
]
[tool.coverage.run] [tool.coverage.run]
omit = ["tests/*"] omit = ["tests/*"]

View File

@ -1,5 +1,5 @@
from collections.abc import AsyncIterator, Iterable, Iterator, Sequence from collections.abc import AsyncIterator, Iterable, Iterator, Sequence
from datetime import datetime from datetime import datetime, timezone
from typing import ( from typing import (
Any, Any,
Optional, Optional,
@ -166,6 +166,10 @@ def upserting_vector_store() -> InMemoryVectorStore:
return InMemoryVectorStore(permit_upserts=True) return InMemoryVectorStore(permit_upserts=True)
_JANUARY_FIRST = datetime(2021, 1, 1, tzinfo=timezone.utc).timestamp()
_JANUARY_SECOND = datetime(2021, 1, 2, tzinfo=timezone.utc).timestamp()
def test_indexing_same_content( def test_indexing_same_content(
record_manager: SQLRecordManager, record_manager: SQLRecordManager,
vector_store: InMemoryVectorStore, vector_store: InMemoryVectorStore,
@ -256,7 +260,7 @@ def test_index_simple_delete_full(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2021, 1, 1).timestamp(), return_value=_JANUARY_FIRST,
): ):
assert index(loader, record_manager, vector_store, cleanup="full") == { assert index(loader, record_manager, vector_store, cleanup="full") == {
"num_added": 2, "num_added": 2,
@ -268,7 +272,7 @@ def test_index_simple_delete_full(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2021, 1, 1).timestamp(), return_value=_JANUARY_FIRST,
): ):
assert index(loader, record_manager, vector_store, cleanup="full") == { assert index(loader, record_manager, vector_store, cleanup="full") == {
"num_added": 0, "num_added": 0,
@ -291,7 +295,7 @@ def test_index_simple_delete_full(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert index(loader, record_manager, vector_store, cleanup="full") == { assert index(loader, record_manager, vector_store, cleanup="full") == {
"num_added": 1, "num_added": 1,
@ -311,7 +315,7 @@ def test_index_simple_delete_full(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert index(loader, record_manager, vector_store, cleanup="full") == { assert index(loader, record_manager, vector_store, cleanup="full") == {
"num_added": 0, "num_added": 0,
@ -341,7 +345,7 @@ async def test_aindex_simple_delete_full(
with patch.object( with patch.object(
arecord_manager, arecord_manager,
"aget_time", "aget_time",
return_value=datetime(2021, 1, 1).timestamp(), return_value=_JANUARY_FIRST,
): ):
assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == { assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == {
"num_added": 2, "num_added": 2,
@ -353,7 +357,7 @@ async def test_aindex_simple_delete_full(
with patch.object( with patch.object(
arecord_manager, arecord_manager,
"aget_time", "aget_time",
return_value=datetime(2021, 1, 1).timestamp(), return_value=_JANUARY_FIRST,
): ):
assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == { assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == {
"num_added": 0, "num_added": 0,
@ -376,7 +380,7 @@ async def test_aindex_simple_delete_full(
with patch.object( with patch.object(
arecord_manager, arecord_manager,
"aget_time", "aget_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == { assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == {
"num_added": 1, "num_added": 1,
@ -396,7 +400,7 @@ async def test_aindex_simple_delete_full(
with patch.object( with patch.object(
arecord_manager, arecord_manager,
"aget_time", "aget_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == { assert await aindex(loader, arecord_manager, vector_store, cleanup="full") == {
"num_added": 0, "num_added": 0,
@ -507,7 +511,7 @@ def test_no_delete(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert index( assert index(
loader, loader,
@ -526,7 +530,7 @@ def test_no_delete(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert index( assert index(
loader, loader,
@ -558,7 +562,7 @@ def test_no_delete(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert index( assert index(
loader, loader,
@ -596,7 +600,7 @@ async def test_ano_delete(
with patch.object( with patch.object(
arecord_manager, arecord_manager,
"aget_time", "aget_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert await aindex( assert await aindex(
loader, loader,
@ -615,7 +619,7 @@ async def test_ano_delete(
with patch.object( with patch.object(
arecord_manager, arecord_manager,
"aget_time", "aget_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert await aindex( assert await aindex(
loader, loader,
@ -647,7 +651,7 @@ async def test_ano_delete(
with patch.object( with patch.object(
arecord_manager, arecord_manager,
"aget_time", "aget_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert await aindex( assert await aindex(
loader, loader,
@ -684,7 +688,7 @@ def test_incremental_delete(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert index( assert index(
loader, loader,
@ -710,7 +714,7 @@ def test_incremental_delete(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert index( assert index(
loader, loader,
@ -747,7 +751,7 @@ def test_incremental_delete(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2021, 1, 3).timestamp(), return_value=datetime(2021, 1, 3, tzinfo=timezone.utc).timestamp(),
): ):
assert index( assert index(
loader, loader,
@ -803,7 +807,7 @@ def test_incremental_indexing_with_batch_size(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert index( assert index(
loader, loader,
@ -870,7 +874,7 @@ def test_incremental_delete_with_batch_size(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert index( assert index(
loader, loader,
@ -897,7 +901,7 @@ def test_incremental_delete_with_batch_size(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert index( assert index(
loader, loader,
@ -917,7 +921,7 @@ def test_incremental_delete_with_batch_size(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2022, 1, 3).timestamp(), return_value=datetime(2022, 1, 3, tzinfo=timezone.utc).timestamp(),
): ):
# Docs with same content # Docs with same content
docs = [ docs = [
@ -948,7 +952,7 @@ def test_incremental_delete_with_batch_size(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2023, 1, 3).timestamp(), return_value=datetime(2023, 1, 3, tzinfo=timezone.utc).timestamp(),
): ):
# Docs with same content # Docs with same content
docs = [ docs = [
@ -979,7 +983,7 @@ def test_incremental_delete_with_batch_size(
with patch.object( with patch.object(
record_manager, record_manager,
"get_time", "get_time",
return_value=datetime(2024, 1, 3).timestamp(), return_value=datetime(2024, 1, 3, tzinfo=timezone.utc).timestamp(),
): ):
# Docs with same content # Docs with same content
docs = [ docs = [
@ -1028,7 +1032,7 @@ async def test_aincremental_delete(
with patch.object( with patch.object(
arecord_manager, arecord_manager,
"aget_time", "aget_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert await aindex( assert await aindex(
loader.lazy_load(), loader.lazy_load(),
@ -1054,7 +1058,7 @@ async def test_aincremental_delete(
with patch.object( with patch.object(
arecord_manager, arecord_manager,
"aget_time", "aget_time",
return_value=datetime(2021, 1, 2).timestamp(), return_value=_JANUARY_SECOND,
): ):
assert await aindex( assert await aindex(
loader.lazy_load(), loader.lazy_load(),
@ -1091,7 +1095,7 @@ async def test_aincremental_delete(
with patch.object( with patch.object(
arecord_manager, arecord_manager,
"aget_time", "aget_time",
return_value=datetime(2021, 1, 3).timestamp(), return_value=datetime(2021, 1, 3, tzinfo=timezone.utc).timestamp(),
): ):
assert await aindex( assert await aindex(
loader.lazy_load(), loader.lazy_load(),

View File

@ -10,14 +10,13 @@ def test_datetime_output_parser_parse() -> None:
parser = DatetimeOutputParser() parser = DatetimeOutputParser()
# Test valid input # Test valid input
date = datetime.now() date = datetime.now() # noqa: DTZ005
datestr = date.strftime(parser.format) datestr = date.strftime(parser.format)
result = parser.parse(datestr) result = parser.parse(datestr)
assert result == date assert result == date
# Test valid input # Test valid input
parser.format = "%Y-%m-%dT%H:%M:%S" parser.format = "%Y-%m-%dT%H:%M:%S"
date = datetime.now()
datestr = date.strftime(parser.format) datestr = date.strftime(parser.format)
result = parser.parse(datestr) result = parser.parse(datestr)
assert ( assert (
@ -31,7 +30,6 @@ def test_datetime_output_parser_parse() -> None:
# Test valid input # Test valid input
parser.format = "%H:%M:%S" parser.format = "%H:%M:%S"
date = datetime.now()
datestr = date.strftime(parser.format) datestr = date.strftime(parser.format)
result = parser.parse(datestr) result = parser.parse(datestr)
assert ( assert (

View File

@ -1,4 +1,5 @@
from datetime import datetime as dt from datetime import datetime as dt
from datetime import timezone
from typing import Any, Callable, Optional, TypeVar from typing import Any, Callable, Optional, TypeVar
import pytest import pytest
@ -151,17 +152,17 @@ def test_output_fixing_parser_output_type(
[ [
( (
"2024/07/08", "2024/07/08",
DatetimeOutputParser(), DatetimeOutputParser(format="%Y-%m-%dT%H:%M:%S.%f%z"),
NAIVE_FIX_PROMPT | RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"), NAIVE_FIX_PROMPT | RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"),
dt(2024, 7, 8), dt(2024, 7, 8, tzinfo=timezone.utc),
), ),
( (
# Case: retry_chain.InputType does not have 'instructions' key # Case: retry_chain.InputType does not have 'instructions' key
"2024/07/08", "2024/07/08",
DatetimeOutputParser(), DatetimeOutputParser(format="%Y-%m-%dT%H:%M:%S.%f%z"),
PromptTemplate.from_template("{completion}\n{error}") PromptTemplate.from_template("{completion}\n{error}")
| RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"), | RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"),
dt(2024, 7, 8), dt(2024, 7, 8, tzinfo=timezone.utc),
), ),
], ],
) )
@ -188,17 +189,17 @@ def test_output_fixing_parser_parse_with_retry_chain(
[ [
( (
"2024/07/08", "2024/07/08",
DatetimeOutputParser(), DatetimeOutputParser(format="%Y-%m-%dT%H:%M:%S.%f%z"),
NAIVE_FIX_PROMPT | RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"), NAIVE_FIX_PROMPT | RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"),
dt(2024, 7, 8), dt(2024, 7, 8, tzinfo=timezone.utc),
), ),
( (
# Case: retry_chain.InputType does not have 'instructions' key # Case: retry_chain.InputType does not have 'instructions' key
"2024/07/08", "2024/07/08",
DatetimeOutputParser(), DatetimeOutputParser(format="%Y-%m-%dT%H:%M:%S.%f%z"),
PromptTemplate.from_template("{completion}\n{error}") PromptTemplate.from_template("{completion}\n{error}")
| RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"), | RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"),
dt(2024, 7, 8), dt(2024, 7, 8, tzinfo=timezone.utc),
), ),
], ],
) )

View File

@ -1,4 +1,5 @@
from datetime import datetime as dt from datetime import datetime as dt
from datetime import timezone
from typing import Any, Callable, Optional, TypeVar from typing import Any, Callable, Optional, TypeVar
import pytest import pytest
@ -209,10 +210,10 @@ def test_retry_with_error_output_parser_parse_is_not_implemented() -> None:
( (
"2024/07/08", "2024/07/08",
StringPromptValue(text="dummy"), StringPromptValue(text="dummy"),
DatetimeOutputParser(), DatetimeOutputParser(format="%Y-%m-%dT%H:%M:%S.%f%z"),
NAIVE_RETRY_PROMPT NAIVE_RETRY_PROMPT
| RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"), | RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"),
dt(2024, 7, 8), dt(2024, 7, 8, tzinfo=timezone.utc),
), ),
], ],
) )
@ -237,10 +238,10 @@ def test_retry_output_parser_parse_with_prompt_with_retry_chain(
( (
"2024/07/08", "2024/07/08",
StringPromptValue(text="dummy"), StringPromptValue(text="dummy"),
DatetimeOutputParser(), DatetimeOutputParser(format="%Y-%m-%dT%H:%M:%S.%f%z"),
NAIVE_RETRY_PROMPT NAIVE_RETRY_PROMPT
| RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"), | RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"),
dt(2024, 7, 8), dt(2024, 7, 8, tzinfo=timezone.utc),
), ),
], ],
) )
@ -266,10 +267,10 @@ async def test_retry_output_parser_aparse_with_prompt_with_retry_chain(
( (
"2024/07/08", "2024/07/08",
StringPromptValue(text="dummy"), StringPromptValue(text="dummy"),
DatetimeOutputParser(), DatetimeOutputParser(format="%Y-%m-%dT%H:%M:%S.%f%z"),
NAIVE_RETRY_WITH_ERROR_PROMPT NAIVE_RETRY_WITH_ERROR_PROMPT
| RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"), | RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"),
dt(2024, 7, 8), dt(2024, 7, 8, tzinfo=timezone.utc),
), ),
], ],
) )
@ -295,10 +296,10 @@ def test_retry_with_error_output_parser_parse_with_prompt_with_retry_chain(
( (
"2024/07/08", "2024/07/08",
StringPromptValue(text="dummy"), StringPromptValue(text="dummy"),
DatetimeOutputParser(), DatetimeOutputParser(format="%Y-%m-%dT%H:%M:%S.%f%z"),
NAIVE_RETRY_WITH_ERROR_PROMPT NAIVE_RETRY_WITH_ERROR_PROMPT
| RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"), | RunnableLambda(lambda _: "2024-07-08T00:00:00.000000Z"),
dt(2024, 7, 8), dt(2024, 7, 8, tzinfo=timezone.utc),
), ),
], ],
) )

View File

@ -2,7 +2,7 @@
import uuid import uuid
from collections.abc import Iterator from collections.abc import Iterator
from datetime import datetime from datetime import datetime, timezone
from typing import Any, Optional, Union from typing import Any, Optional, Union
from unittest import mock from unittest import mock
@ -27,7 +27,7 @@ from langchain.smith.evaluation.runner_utils import (
from tests.unit_tests.llms.fake_chat_model import FakeChatModel from tests.unit_tests.llms.fake_chat_model import FakeChatModel
from tests.unit_tests.llms.fake_llm import FakeLLM from tests.unit_tests.llms.fake_llm import FakeLLM
_CREATED_AT = datetime(2015, 1, 1, 0, 0, 0) _CREATED_AT = datetime(2015, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
_TENANT_ID = "7a3d2b56-cd5b-44e5-846f-7eb6e8144ce4" _TENANT_ID = "7a3d2b56-cd5b-44e5-846f-7eb6e8144ce4"
_EXAMPLE_MESSAGE = { _EXAMPLE_MESSAGE = {
"data": {"content": "Foo", "example": False, "additional_kwargs": {}}, "data": {"content": "Foo", "example": False, "additional_kwargs": {}},