mirror of
https://github.com/deepset-ai/haystack.git
synced 2026-10-12 02:12:50 +00:00
feat: Updates to make Haystack compatible with python 3.14 (#10634)
* start adding python 3.14 support in tests * start updating tests * Add reno * Updates * Collect tests that should be skipped in python 3.14+ * run tests using python 3.14 * Revert back to 3.10 for testing * Fix flaky tests * Skip only spacy tests if python 3.14+ * switch to get_running_loop
This commit is contained in:
@@ -7,10 +7,22 @@
|
||||
# test environment. Spacy is not installed in the test environment to keep the CI fast.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from haystack import Document, Pipeline
|
||||
from haystack.components.extractors import NamedEntityAnnotation, NamedEntityExtractor, NamedEntityExtractorBackend
|
||||
from haystack.components.extractors import (
|
||||
NamedEntityAnnotation,
|
||||
NamedEntityExtractor,
|
||||
NamedEntityExtractorBackend,
|
||||
)
|
||||
|
||||
# spacy uses pydantic.v1 which is not compatible with Python 3.14 (PEP 649 deferred annotations)
|
||||
skip_spacy = pytest.mark.skipif(
|
||||
sys.version_info >= (3, 14),
|
||||
reason="spacy/pydantic.v1 not compatible with Python 3.14",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -78,6 +90,7 @@ def test_ner_extractor_hf_backend_private_models(raw_texts, hf_annotations, batc
|
||||
|
||||
|
||||
@pytest.mark.parametrize("batch_size", [1, 3])
|
||||
@skip_spacy
|
||||
def test_ner_extractor_spacy_backend(raw_texts, spacy_annotations, batch_size) -> None:
|
||||
extractor = NamedEntityExtractor(backend=NamedEntityExtractorBackend.SPACY, model="en_core_web_trf")
|
||||
extractor.warm_up()
|
||||
|
||||
@@ -38,6 +38,11 @@ def _is_valid_type(obj: Any) -> bool:
|
||||
if (origin := get_origin(obj)) and _is_union_type(origin):
|
||||
return True
|
||||
|
||||
# Bare Union type (without parameters) is not a valid type annotation
|
||||
# Previously handled by inspect.isclass(obj) but in python 3.14 this returns True for typing.Union
|
||||
if obj == Union:
|
||||
return False
|
||||
|
||||
# Handle normal classes and generic types
|
||||
return inspect.isclass(obj) or type(obj).__name__ in {"_GenericAlias", "GenericAlias"}
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@ def _contains_type(container: Any, target: Any) -> bool:
|
||||
return _safe_get_origin(container) is Union and target in get_args(container)
|
||||
|
||||
|
||||
def _strict_types_are_compatible(sender: Any, receiver: Any) -> bool:
|
||||
def _strict_types_are_compatible(sender: Any, receiver: Any) -> bool: # noqa: PLR0911
|
||||
"""
|
||||
Checks whether the sender type is equal to or a subtype of the receiver type under strict validation.
|
||||
|
||||
@@ -116,6 +116,10 @@ def _strict_types_are_compatible(sender: Any, receiver: Any) -> bool:
|
||||
sender_origin = _safe_get_origin(sender)
|
||||
receiver_origin = _safe_get_origin(receiver)
|
||||
|
||||
# Special case to reject bare-Union types
|
||||
if (sender_origin is Union and not get_args(sender)) or (receiver_origin is Union and not get_args(receiver)):
|
||||
return False
|
||||
|
||||
if sender_origin is not Union and receiver_origin is Union:
|
||||
return any(_strict_types_are_compatible(sender, union_arg) for union_arg in get_args(receiver))
|
||||
|
||||
|
||||
@@ -737,7 +737,7 @@ class InMemoryDocumentStore:
|
||||
:param filters: The filters to apply to the document list.
|
||||
:returns: A list of Documents that match the given filters.
|
||||
"""
|
||||
return await asyncio.get_event_loop().run_in_executor(
|
||||
return await asyncio.get_running_loop().run_in_executor(
|
||||
self.executor, lambda: self.filter_documents(filters=filters)
|
||||
)
|
||||
|
||||
@@ -749,7 +749,7 @@ class InMemoryDocumentStore:
|
||||
|
||||
If `policy` is set to `DuplicatePolicy.NONE` defaults to `DuplicatePolicy.FAIL`.
|
||||
"""
|
||||
return await asyncio.get_event_loop().run_in_executor(
|
||||
return await asyncio.get_running_loop().run_in_executor(
|
||||
self.executor, lambda: self.write_documents(documents=documents, policy=policy)
|
||||
)
|
||||
|
||||
@@ -759,7 +759,7 @@ class InMemoryDocumentStore:
|
||||
|
||||
:param document_ids: The object_ids to delete.
|
||||
"""
|
||||
await asyncio.get_event_loop().run_in_executor(
|
||||
await asyncio.get_running_loop().run_in_executor(
|
||||
self.executor, lambda: self.delete_documents(document_ids=document_ids)
|
||||
)
|
||||
|
||||
@@ -775,7 +775,7 @@ class InMemoryDocumentStore:
|
||||
:param scale_score: Whether to scale the scores of the retrieved documents. Default is False.
|
||||
:returns: A list of the top_k documents most relevant to the query.
|
||||
"""
|
||||
return await asyncio.get_event_loop().run_in_executor(
|
||||
return await asyncio.get_running_loop().run_in_executor(
|
||||
self.executor,
|
||||
lambda: self.bm25_retrieval(query=query, filters=filters, top_k=top_k, scale_score=scale_score),
|
||||
)
|
||||
@@ -798,7 +798,7 @@ class InMemoryDocumentStore:
|
||||
:param return_embedding: Whether to return the embedding of the retrieved Documents. Default is False.
|
||||
:returns: A list of the top_k documents most relevant to the query.
|
||||
"""
|
||||
return await asyncio.get_event_loop().run_in_executor(
|
||||
return await asyncio.get_running_loop().run_in_executor(
|
||||
self.executor,
|
||||
lambda: self.embedding_retrieval(
|
||||
query_embedding=query_embedding,
|
||||
|
||||
+2
-1
@@ -38,6 +38,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
||||
]
|
||||
dependencies = [
|
||||
@@ -168,7 +169,7 @@ types = "mypy --install-types --non-interactive --cache-dir=.mypy_cache/ {args:h
|
||||
template = "test"
|
||||
extra-dependencies = [
|
||||
# NamedEntityExtractor
|
||||
"spacy>=3.8,<3.9",
|
||||
"spacy>=3.8,<3.9; python_version < '3.14'",
|
||||
"en-core-web-trf @ https://github.com/explosion/spacy-models/releases/download/en_core_web_trf-3.8.0/en_core_web_trf-3.8.0-py3-none-any.whl",
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
enhancements:
|
||||
- |
|
||||
Add support for python 3.14 to Haystack. Haystack already mostly supported python 3.14.
|
||||
Only minor changes were needed in regards to our type serialization and type checking when handling bare Union types.
|
||||
@@ -1126,7 +1126,7 @@ def pipeline_that_has_multiple_branches_of_different_lengths_that_merge_into_a_c
|
||||
("first_addition", 1): {"add": None, "value": 1},
|
||||
("fourth_addition", 1): {"add": None, "value": 11},
|
||||
("second_addition", 1): {"add": None, "value": 3},
|
||||
("sum", 1): {"values": [3, 3, 5]},
|
||||
("sum", 1): {"values": AnyOrder([3, 3, 5])},
|
||||
("third_addition", 1): {"add": None, "value": 1},
|
||||
},
|
||||
)
|
||||
@@ -2841,11 +2841,12 @@ def that_has_variadic_component_that_receives_a_conditional_input(pipeline_class
|
||||
class AnyOrder: # noqa: PLW1641 # Object does not implement `__hash__` method but it's ok
|
||||
"""List wrapper with order-insensitive equality"""
|
||||
|
||||
def __init__(self, items):
|
||||
def __init__(self, items, key=None):
|
||||
self.items = items
|
||||
self.key = key
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, list) and sorted(self.items) == sorted(other)
|
||||
return isinstance(other, list) and sorted(self.items, key=self.key) == sorted(other, key=self.key)
|
||||
|
||||
|
||||
@given("a pipeline that has a string variadic component", target_fixture="pipeline_data")
|
||||
@@ -4416,11 +4417,16 @@ some,header,row
|
||||
("txt_converter", 1): {"sources": [sources[1]], "meta": None},
|
||||
("json_converter", 1): {"sources": [sources[2]], "meta": None},
|
||||
("b_joiner", 1): {
|
||||
"documents": [[expected_pre_split_docs[0]], [expected_pre_split_docs[1]]],
|
||||
"documents": AnyOrder(
|
||||
[[expected_pre_split_docs[0]], [expected_pre_split_docs[1]]], key=lambda d: d[0].id
|
||||
),
|
||||
"top_k": None,
|
||||
},
|
||||
("splitter", 1): {"documents": expected_pre_split_docs},
|
||||
("a_joiner", 1): {"documents": [expected_csv_docs, expected_splits_docs], "top_k": None},
|
||||
("a_joiner", 1): {
|
||||
"documents": AnyOrder([expected_csv_docs, expected_splits_docs], key=lambda d: d[0].id),
|
||||
"top_k": None,
|
||||
},
|
||||
},
|
||||
)
|
||||
],
|
||||
@@ -4471,26 +4477,57 @@ def pipeline_that_converts_files_with_three_joiners(pipeline_class):
|
||||
pipe.connect("page_splitter.documents", "DocumentJoiner_2.documents")
|
||||
|
||||
expected_html_doc = Document(content="Some content", meta={"file_type": "html"})
|
||||
# Need to use ANY for id since not sure how it's calculated in splitter
|
||||
expected_html_doc_aft_splitter = Document(
|
||||
id=ANY,
|
||||
content="Some content",
|
||||
meta={
|
||||
"file_type": "html",
|
||||
"page_number": 1,
|
||||
"split_id": 0,
|
||||
"split_idx_start": 0,
|
||||
"source_id": expected_html_doc.id,
|
||||
},
|
||||
)
|
||||
expected_txt_doc = Document(content=txt_data, meta={"file_type": "txt"})
|
||||
# Need to use ANY for id since not sure how it's calculated in splitter
|
||||
expected_txt_doc_aft_splitter = Document(
|
||||
id=ANY,
|
||||
content=txt_data,
|
||||
meta={
|
||||
"file_type": "txt",
|
||||
"page_number": 1,
|
||||
"split_id": 0,
|
||||
"split_idx_start": 0,
|
||||
"source_id": expected_txt_doc.id,
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
pipe,
|
||||
[
|
||||
PipelineRunData(
|
||||
inputs={"router": {"sources": sources}},
|
||||
# HTML converter takes longer than TXT Converter and this is why the order of documents is not stable
|
||||
# for AsyncPipeline. We use [ANY, ANY] to indicate that we do not care about the order here.
|
||||
# In real usage, if the user cares about the order of documents arriving at a lazy variadic component,
|
||||
# the user should pick a different component (OutputAdapter) to combine the lists.
|
||||
expected_outputs={"DocumentJoiner_2": {"documents": [ANY, ANY]}},
|
||||
expected_outputs={
|
||||
"DocumentJoiner_2": {
|
||||
"documents": AnyOrder(
|
||||
[expected_html_doc_aft_splitter, expected_txt_doc_aft_splitter], key=lambda d: d.content
|
||||
)
|
||||
}
|
||||
},
|
||||
expected_component_calls={
|
||||
("router", 1): {"sources": sources, "meta": None},
|
||||
("html_converter", 1): {"sources": [sources[1]], "meta": None, "extraction_kwargs": None},
|
||||
("txt_converter", 1): {"sources": [sources[0]], "meta": None},
|
||||
("joiner", 1): {"documents": [[expected_txt_doc]], "top_k": None},
|
||||
("DocumentJoiner_1", 1): {"documents": [[expected_html_doc]], "top_k": None},
|
||||
# Same as above
|
||||
("DocumentJoiner_2", 1): {"documents": [ANY, ANY], "top_k": None},
|
||||
("DocumentJoiner_2", 1): {
|
||||
"documents": AnyOrder(
|
||||
[[expected_html_doc_aft_splitter], [expected_txt_doc_aft_splitter]],
|
||||
key=lambda d: d[0].content,
|
||||
),
|
||||
"top_k": None,
|
||||
},
|
||||
("splitter", 1): {"documents": [expected_txt_doc]},
|
||||
("page_splitter", 1): {"documents": [expected_html_doc]},
|
||||
},
|
||||
@@ -4898,6 +4935,17 @@ def pipeline_that_converts_files_with_three_auto_joiners(pipeline_class):
|
||||
pipe.connect("splitter.documents", "writer.documents")
|
||||
pipe.connect("page_splitter.documents", "writer.documents")
|
||||
|
||||
expected_txt_document = Document(
|
||||
id=ANY,
|
||||
content=txt_data,
|
||||
meta={"file_type": "txt", "source_id": ANY, "page_number": 1, "split_id": 0, "split_idx_start": 0},
|
||||
)
|
||||
expected_html_document = Document(
|
||||
id=ANY,
|
||||
content="Some content",
|
||||
meta={"file_type": "html", "source_id": ANY, "page_number": 1, "split_id": 0, "split_idx_start": 0},
|
||||
)
|
||||
|
||||
return (
|
||||
pipe,
|
||||
[
|
||||
@@ -4908,10 +4956,9 @@ def pipeline_that_converts_files_with_three_auto_joiners(pipeline_class):
|
||||
("router", 1): {"sources": sources, "meta": None},
|
||||
("html_converter", 1): {"sources": [sources[1]], "meta": None, "extraction_kwargs": None},
|
||||
("txt_converter", 1): {"sources": [sources[0]], "meta": None},
|
||||
# HTML converter takes longer than TXT Converter and this is why the order of documents is not
|
||||
# stable for AsyncPipeline. We test for [ANY, ANY] here to ensure at least two documents are
|
||||
# present.
|
||||
("writer", 1): {"documents": [ANY, ANY]},
|
||||
("writer", 1): {
|
||||
"documents": AnyOrder([expected_txt_document, expected_html_document], key=lambda d: d.content)
|
||||
},
|
||||
("splitter", 1): {"documents": [Document(content=txt_data, meta={"file_type": "txt"})]},
|
||||
("page_splitter", 1): {"documents": [Document(content="Some content", meta={"file_type": "html"})]},
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import sys
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, Iterable, List, Literal, Mapping, Optional, Sequence, Set, Tuple, Union
|
||||
@@ -363,34 +364,10 @@ asymmetric_cases = generate_strict_asymmetric_cases()
|
||||
pytest.param(str, "str", id="primitive-types"),
|
||||
pytest.param(Any, "Any", id="any"),
|
||||
pytest.param(Class1, "Class1", id="class"),
|
||||
pytest.param((Optional[int]), "Optional[int]", id="shallow-optional-with-primitive"),
|
||||
pytest.param((Optional[Any]), "Optional[Any]", id="shallow-optional-with-any"),
|
||||
pytest.param((Optional[Class1]), "Optional[Class1]", id="shallow-optional-with-class"),
|
||||
pytest.param((Union[(bool, Class1)]), "Union[bool, Class1]", id="shallow-union"),
|
||||
pytest.param((List[str]), "List[str]", id="shallow-sequence-of-primitives"),
|
||||
pytest.param((list[str]), "list[str]", id="shallow-sequence-of-primitives"),
|
||||
pytest.param((List[Set[Sequence[str]]]), "List[Set[Sequence[str]]]", id="nested-sequence-of-primitives"),
|
||||
pytest.param((list[set[Sequence[str]]]), "list[set[Sequence[str]]]", id="nested-sequence-of-primitives"),
|
||||
pytest.param(
|
||||
(Optional[List[Set[Sequence[str]]]]),
|
||||
"Optional[List[Set[Sequence[str]]]]",
|
||||
id="optional-nested-sequence-of-primitives",
|
||||
),
|
||||
pytest.param(
|
||||
(Optional[list[set[Sequence[str]]]]),
|
||||
"Optional[list[set[Sequence[str]]]]",
|
||||
id="optional-nested-sequence-of-primitives",
|
||||
),
|
||||
pytest.param(
|
||||
(List[Set[Sequence[Optional[str]]]]),
|
||||
"List[Set[Sequence[Optional[str]]]]",
|
||||
id="nested-optional-sequence-of-primitives",
|
||||
),
|
||||
pytest.param(
|
||||
(list[set[Sequence[Optional[str]]]]),
|
||||
"list[set[Sequence[Optional[str]]]]",
|
||||
id="nested-optional-sequence-of-primitives",
|
||||
),
|
||||
pytest.param((List[Class1]), "List[Class1]", id="shallow-sequence-of-classes"),
|
||||
pytest.param((list[Class1]), "list[Class1]", id="shallow-sequence-of-classes"),
|
||||
pytest.param((List[Set[Sequence[Class1]]]), "List[Set[Sequence[Class1]]]", id="nested-sequence-of-classes"),
|
||||
@@ -432,16 +409,6 @@ asymmetric_cases = generate_strict_asymmetric_cases()
|
||||
pytest.param((Literal["a", "b", "c"]), "Literal['a', 'b', 'c']", id="string-literal"),
|
||||
pytest.param((Literal[1, 2, 3]), "Literal[1, 2, 3]", id="primitive-literal"),
|
||||
pytest.param((Literal[Enum1.TEST1]), "Literal[Enum1.TEST1]", id="enum-literal"),
|
||||
pytest.param(
|
||||
(Tuple[(Optional[Literal["a", "b", "c"]], Union[(Path, Dict[(int, Class1)])])]),
|
||||
"Tuple[Optional[Literal['a', 'b', 'c']], Union[Path, Dict[int, Class1]]]",
|
||||
id="deeply-nested-complex-type",
|
||||
),
|
||||
pytest.param(
|
||||
(tuple[(Optional[Literal["a", "b", "c"]], Union[(Path, dict[(int, Class1)])])]),
|
||||
"tuple[Optional[Literal['a', 'b', 'c']], Union[Path, dict[int, Class1]]]",
|
||||
id="deeply-nested-complex-type",
|
||||
),
|
||||
pytest.param((int | str), "int | str", id="pep604-union"),
|
||||
pytest.param((int | None), "int | None", id="pep604-optional"),
|
||||
pytest.param((list[int] | None), "list[int] | None", id="pep604-optional-list"),
|
||||
@@ -455,6 +422,52 @@ def test_type_name(type_, repr_):
|
||||
assert _type_name(type_) == repr_
|
||||
|
||||
|
||||
if sys.version_info < (3, 14):
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"type_,repr_",
|
||||
[
|
||||
pytest.param((Union[(bool, Class1)]), "Union[bool, Class1]", id="shallow-union"),
|
||||
pytest.param((Optional[int]), "Optional[int]", id="shallow-optional-with-primitive"),
|
||||
pytest.param((Optional[Any]), "Optional[Any]", id="shallow-optional-with-any"),
|
||||
pytest.param((Optional[Class1]), "Optional[Class1]", id="shallow-optional-with-class"),
|
||||
pytest.param(
|
||||
(Tuple[(Optional[Literal["a", "b", "c"]], Union[(Path, Dict[(int, Class1)])])]),
|
||||
"Tuple[Optional[Literal['a', 'b', 'c']], Union[Path, Dict[int, Class1]]]",
|
||||
id="deeply-nested-complex-type",
|
||||
),
|
||||
pytest.param(
|
||||
(tuple[(Optional[Literal["a", "b", "c"]], Union[(Path, dict[(int, Class1)])])]),
|
||||
"tuple[Optional[Literal['a', 'b', 'c']], Union[Path, dict[int, Class1]]]",
|
||||
id="deeply-nested-complex-type",
|
||||
),
|
||||
pytest.param(
|
||||
(Optional[List[Set[Sequence[str]]]]),
|
||||
"Optional[List[Set[Sequence[str]]]]",
|
||||
id="optional-nested-sequence-of-primitives",
|
||||
),
|
||||
pytest.param(
|
||||
(Optional[list[set[Sequence[str]]]]),
|
||||
"Optional[list[set[Sequence[str]]]]",
|
||||
id="optional-nested-sequence-of-primitives",
|
||||
),
|
||||
pytest.param(
|
||||
(List[Set[Sequence[Optional[str]]]]),
|
||||
"List[Set[Sequence[Optional[str]]]]",
|
||||
id="nested-optional-sequence-of-primitives",
|
||||
),
|
||||
pytest.param(
|
||||
(list[set[Sequence[Optional[str]]]]),
|
||||
"list[set[Sequence[Optional[str]]]]",
|
||||
id="nested-optional-sequence-of-primitives",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_type_name_union_and_optional(type_, repr_):
|
||||
"""Tests for old typing.Union and typing.Optional types that are converted to builtins in python 3.14+."""
|
||||
assert _type_name(type_) == repr_
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sender_type, receiver_type", symmetric_cases)
|
||||
def test_same_types_are_compatible_strict(sender_type, receiver_type):
|
||||
assert _types_are_compatible(sender_type, receiver_type)[0]
|
||||
@@ -842,22 +855,23 @@ asymmetric_cases_pep_604 = generate_strict_asymmetric_cases_pep_604()
|
||||
"list[set[Sequence[str]]] | None",
|
||||
id="optional-nested-sequence-of-primitives",
|
||||
),
|
||||
pytest.param(
|
||||
(list[set[Sequence[str | None]]]),
|
||||
"list[set[Sequence[Optional[str]]]]",
|
||||
id="nested-optional-sequence-of-primitives",
|
||||
),
|
||||
pytest.param(
|
||||
(tuple[Literal["a", "b", "c"] | None, Path | dict[int, Class1]]),
|
||||
"tuple[Optional[Literal['a', 'b', 'c']], Path | dict[int, Class1]]",
|
||||
id="deeply-nested-complex-type",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_type_name_pep_604(type_, repr_):
|
||||
assert _type_name(type_) == repr_
|
||||
|
||||
|
||||
def test_type_name_pep_604_explicit():
|
||||
"""Sometimes the internal str | None can be treated as an Optional instead of a UnionType"""
|
||||
type_name = _type_name(list[set[Sequence[str | None]]])
|
||||
assert type_name == "list[set[Sequence[str | None]]]" or type_name == "list[set[Sequence[Optional[str]]]]"
|
||||
type_name = _type_name(tuple[Literal["a", "b", "c"] | None, Path | dict[int, Class1]])
|
||||
assert (
|
||||
type_name == "tuple[Literal['a', 'b', 'c'] | None, Path | dict[int, Class1]]"
|
||||
or type_name == "tuple[Optional[Literal['a', 'b', 'c']], Path | dict[int, Class1]]"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sender_type, receiver_type", symmetric_cases_pep_604)
|
||||
def test_same_types_are_compatible_strict_pep_604(sender_type, receiver_type):
|
||||
assert _types_are_compatible(sender_type, receiver_type)[0]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import sys
|
||||
import typing
|
||||
from collections import deque
|
||||
from types import UnionType
|
||||
@@ -47,13 +48,6 @@ TYPING_AND_TYPE_TESTS = [
|
||||
pytest.param("typing.List[dict]", List[dict]),
|
||||
pytest.param("typing.List[float]", List[float]),
|
||||
pytest.param("typing.List[bool]", List[bool]),
|
||||
# typing Optional
|
||||
pytest.param("typing.Optional", Optional),
|
||||
pytest.param("typing.Optional[str]", Optional[str]),
|
||||
pytest.param("typing.Optional[int]", Optional[int]),
|
||||
pytest.param("typing.Optional[dict]", Optional[dict]),
|
||||
pytest.param("typing.Optional[float]", Optional[float]),
|
||||
pytest.param("typing.Optional[bool]", Optional[bool]),
|
||||
# PEP 604 X | None
|
||||
pytest.param("str | None", str | None),
|
||||
pytest.param("int | None", int | None),
|
||||
@@ -90,13 +84,6 @@ TYPING_AND_TYPE_TESTS = [
|
||||
pytest.param("typing.Tuple[dict]", Tuple[dict]),
|
||||
pytest.param("typing.Tuple[float]", Tuple[float]),
|
||||
pytest.param("typing.Tuple[bool]", Tuple[bool]),
|
||||
# Union
|
||||
pytest.param("typing.Union", Union),
|
||||
pytest.param("typing.Union[str, int]", Union[str, int]),
|
||||
pytest.param("typing.Union[int, float]", Union[int, float]),
|
||||
pytest.param("typing.Union[dict, str]", Union[dict, str]),
|
||||
pytest.param("typing.Union[float, bool]", Union[float, bool]),
|
||||
pytest.param("typing.Optional[str]", typing.Union[None, str]), # Union with None becomes Optional
|
||||
# PEP 604 X | Y
|
||||
pytest.param("str | int", str | int),
|
||||
pytest.param("int | float", int | float),
|
||||
@@ -174,13 +161,10 @@ def test_output_builtin_type_deserialization():
|
||||
|
||||
def test_output_type_serialization_nested():
|
||||
# typing
|
||||
assert serialize_type(List[Union[str, int]]) == "typing.List[typing.Union[str, int]]"
|
||||
assert serialize_type(List[Optional[str]]) == "typing.List[typing.Optional[str]]"
|
||||
assert serialize_type(List[Dict[str, int]]) == "typing.List[typing.Dict[str, int]]"
|
||||
assert serialize_type(typing.List[Dict[str, int]]) == "typing.List[typing.Dict[str, int]]"
|
||||
# builtins
|
||||
assert serialize_type(list[Union[str, int]]) == "list[typing.Union[str, int]]"
|
||||
assert serialize_type(list[Optional[str]]) == "list[typing.Optional[str]]"
|
||||
|
||||
assert serialize_type(list[dict[str, int]]) == "list[dict[str, int]]"
|
||||
assert serialize_type(list[list[int]]) == "list[list[int]]"
|
||||
assert serialize_type(list[list[list[int]]]) == "list[list[list[int]]]"
|
||||
@@ -291,10 +275,6 @@ def test_output_type_deserialization_haystack_dataclasses():
|
||||
def test_output_type_serialization_pep_604():
|
||||
# PEP 604 allows for union types to be defined with the `|` operator
|
||||
assert serialize_type(str | int) == "str | int"
|
||||
assert serialize_type(List[str] | List[int]) == "typing.Union[typing.List[str], typing.List[int]]"
|
||||
assert (
|
||||
serialize_type(Dict[str, int] | Dict[int, str]) == "typing.Union[typing.Dict[str, int], typing.Dict[int, str]]"
|
||||
)
|
||||
assert serialize_type(str | None) == "str | None"
|
||||
assert serialize_type(list[str] | None) == "list[str] | None"
|
||||
assert serialize_type(int | float | str) == "int | float | str"
|
||||
@@ -370,3 +350,30 @@ def test_build_pep604_union_type():
|
||||
|
||||
result = _build_pep604_union_type([list[str], dict[str, int]])
|
||||
assert result == list[str] | dict[str, int]
|
||||
|
||||
|
||||
if sys.version_info < (3, 14):
|
||||
|
||||
def test_type_de_se_union_and_optional():
|
||||
"""Tests for old typing.Union and typing.Optional types that are converted to builtins in python 3.14+."""
|
||||
assert serialize_type(List[Union[str, int]]) == "typing.List[typing.Union[str, int]]"
|
||||
assert serialize_type(List[str] | List[int]) == "typing.Union[typing.List[str], typing.List[int]]"
|
||||
assert serialize_type(List[Optional[str]]) == "typing.List[typing.Optional[str]]"
|
||||
assert (
|
||||
serialize_type(Dict[str, int] | Dict[int, str])
|
||||
== "typing.Union[typing.Dict[str, int], typing.Dict[int, str]]"
|
||||
)
|
||||
assert serialize_type(list[Union[str, int]]) == "list[typing.Union[str, int]]"
|
||||
assert serialize_type(list[Optional[str]]) == "list[typing.Optional[str]]"
|
||||
# Union
|
||||
assert serialize_type(Union) == "typing.Union"
|
||||
assert serialize_type(Union[str, int]) == "typing.Union[str, int]"
|
||||
assert serialize_type(Union[int, float]) == "typing.Union[int, float]"
|
||||
assert serialize_type(Union[dict, str]) == "typing.Union[dict, str]"
|
||||
assert serialize_type(Union[float, bool]) == "typing.Union[float, bool]"
|
||||
assert serialize_type(Optional) == "typing.Optional"
|
||||
assert serialize_type(Optional[str]) == "typing.Optional[str]"
|
||||
assert serialize_type(Optional[int]) == "typing.Optional[int]"
|
||||
assert serialize_type(Optional[dict]) == "typing.Optional[dict]"
|
||||
assert serialize_type(Optional[float]) == "typing.Optional[float]"
|
||||
assert serialize_type(Optional[bool]) == "typing.Optional[bool]"
|
||||
|
||||
Reference in New Issue
Block a user