Repository: elastic/elasticsearch-dsl-py (Python)

Issue:
Regression: source() does not accept iterables
Prior to the following change, I could pass a tuple to `source()` calls, e.g. `source(includes=("a","b","c"))`


https://github.com/elastic/elasticsearch-dsl-py/pull/1845/files#diff-706d55be3a1b925b14bb3122ccecf00daeb04113df622f4ce445bec7d7753f5cR627

Seems like passing tuples, or any other iterable other than lists will now fail due to `isinstance(fields, list)` check.

Notice that this was before the typing update, which established the parameter type as list. This change at least needs to be documented if we are not considering supporting other collection types.


Discussion:
This is unintended, I'll improve the logic to allow all iterables and not just lists. Thanks!



Patch:
diff --git a/elasticsearch_dsl/search_base.py b/elasticsearch_dsl/search_base.py
index b54bbaec..76a9b1d9 100644
--- a/elasticsearch_dsl/search_base.py
+++ b/elasticsearch_dsl/search_base.py
@@ -37,13 +37,13 @@
 from typing_extensions import Self, TypeVar
 
 from .aggs import A, Agg, AggBase
+from .document_base import InstrumentedField
 from .exceptions import IllegalOperation
 from .query import Bool, Q, Query
 from .response import Hit, Response
 from .utils import _R, AnyUsingType, AttrDict, DslBase, recursive_to_dict
 
 if TYPE_CHECKING:
-    from .document_base import InstrumentedField
     from .field import Field, Object
 
 
@@ -714,10 +714,14 @@ def ensure_strings(
                 Dict[str, List[Union[str, "InstrumentedField"]]],
             ]
         ) -> Union[str, List[str], Dict[str, List[str]]]:
-            if isinstance(fields, list):
-                return [str(f) for f in fields]
-            elif isinstance(fields, dict):
+            if isinstance(fields, dict):
                 return {k: ensure_strings(v) for k, v in fields.items()}
+            elif not isinstance(fields, (str, InstrumentedField)):
+                # we assume that if `fields` is not a any of [dict, str,
+                # InstrumentedField] then it is an iterable of strings or
+                # InstrumentedFields, so we convert them to a plain list of
+                # strings
+                return [str(f) for f in fields]
             else:
                 return str(fields)
 


Test patch:
diff --git a/tests/_async/test_search.py b/tests/_async/test_search.py
index 603c3826..28e07a50 100644
--- a/tests/_async/test_search.py
+++ b/tests/_async/test_search.py
@@ -546,7 +546,7 @@ def test_source() -> None:
 
     assert {
         "_source": {"includes": ["foo.bar.*"], "excludes": ["foo.one"]}
-    } == AsyncSearch().source(includes=["foo.bar.*"], excludes=["foo.one"]).to_dict()
+    } == AsyncSearch().source(includes=["foo.bar.*"], excludes=("foo.one",)).to_dict()
 
     assert {"_source": False} == AsyncSearch().source(False).to_dict()
 
diff --git a/tests/_sync/test_search.py b/tests/_sync/test_search.py
index 638c614d..d777508f 100644
--- a/tests/_sync/test_search.py
+++ b/tests/_sync/test_search.py
@@ -546,7 +546,7 @@ def test_source() -> None:
 
     assert {
         "_source": {"includes": ["foo.bar.*"], "excludes": ["foo.one"]}
-    } == Search().source(includes=["foo.bar.*"], excludes=["foo.one"]).to_dict()
+    } == Search().source(includes=["foo.bar.*"], excludes=("foo.one",)).to_dict()
 
     assert {"_source": False} == Search().source(False).to_dict()