Skip to content

Commit f457262

Browse files
type hints for document, search_base and utils classes (#1851)
* type hints for document, search_base and utils classes * Update elasticsearch_dsl/utils.py Co-authored-by: Quentin Pradet <[email protected]> * Update tests/test_validation.py Co-authored-by: Quentin Pradet <[email protected]> * feedback --------- Co-authored-by: Quentin Pradet <[email protected]>
1 parent 667dd68 commit f457262

27 files changed

+1075
-660
lines changed

elasticsearch_dsl/_async/document.py

Lines changed: 99 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,45 @@
1616
# under the License.
1717

1818
import collections.abc
19+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
1920

2021
from elasticsearch.exceptions import NotFoundError, RequestError
21-
from typing_extensions import dataclass_transform
22+
from typing_extensions import Self, dataclass_transform
2223

2324
from .._async.index import AsyncIndex
2425
from ..async_connections import get_connection
2526
from ..document_base import DocumentBase, DocumentMeta, mapped_field
2627
from ..exceptions import IllegalOperation
27-
from ..utils import DOC_META_FIELDS, META_FIELDS, merge
28+
from ..utils import DOC_META_FIELDS, META_FIELDS, AsyncUsingType, merge
2829
from .search import AsyncSearch
2930

31+
if TYPE_CHECKING:
32+
from elasticsearch import AsyncElasticsearch
33+
3034

3135
class AsyncIndexMeta(DocumentMeta):
36+
_index: AsyncIndex
37+
3238
# global flag to guard us from associating an Index with the base Document
3339
# class, only user defined subclasses should have an _index attr
3440
_document_initialized = False
3541

36-
def __new__(cls, name, bases, attrs):
42+
def __new__(
43+
cls, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any]
44+
) -> "AsyncIndexMeta":
3745
new_cls = super().__new__(cls, name, bases, attrs)
3846
if cls._document_initialized:
3947
index_opts = attrs.pop("Index", None)
4048
index = cls.construct_index(index_opts, bases)
4149
new_cls._index = index
4250
index.document(new_cls)
4351
cls._document_initialized = True
44-
return new_cls
52+
return cast(AsyncIndexMeta, new_cls)
4553

4654
@classmethod
47-
def construct_index(cls, opts, bases):
55+
def construct_index(
56+
cls, opts: Dict[str, Any], bases: Tuple[type, ...]
57+
) -> AsyncIndex:
4858
if opts is None:
4959
for b in bases:
5060
if hasattr(b, "_index"):
@@ -69,12 +79,23 @@ class AsyncDocument(DocumentBase, metaclass=AsyncIndexMeta):
6979
Model-like class for persisting documents in elasticsearch.
7080
"""
7181

82+
if TYPE_CHECKING:
83+
_index: AsyncIndex
84+
7285
@classmethod
73-
def _get_connection(cls, using=None):
86+
def _get_using(cls, using: Optional[AsyncUsingType] = None) -> AsyncUsingType:
87+
return using or cls._index._using
88+
89+
@classmethod
90+
def _get_connection(
91+
cls, using: Optional[AsyncUsingType] = None
92+
) -> "AsyncElasticsearch":
7493
return get_connection(cls._get_using(using))
7594

7695
@classmethod
77-
async def init(cls, index=None, using=None):
96+
async def init(
97+
cls, index: Optional[str] = None, using: Optional[AsyncUsingType] = None
98+
) -> None:
7899
"""
79100
Create the index and populate the mappings in elasticsearch.
80101
"""
@@ -84,7 +105,9 @@ async def init(cls, index=None, using=None):
84105
await i.save(using=using)
85106

86107
@classmethod
87-
def search(cls, using=None, index=None):
108+
def search(
109+
cls, using: Optional[AsyncUsingType] = None, index: Optional[str] = None
110+
) -> AsyncSearch[Self]:
88111
"""
89112
Create an :class:`~elasticsearch_dsl.Search` instance that will search
90113
over this ``Document``.
@@ -94,7 +117,13 @@ def search(cls, using=None, index=None):
94117
)
95118

96119
@classmethod
97-
async def get(cls, id, using=None, index=None, **kwargs):
120+
async def get(
121+
cls,
122+
id: str,
123+
using: Optional[AsyncUsingType] = None,
124+
index: Optional[str] = None,
125+
**kwargs: Any,
126+
) -> Optional[Self]:
98127
"""
99128
Retrieve a single document from elasticsearch using its ``id``.
100129
@@ -113,7 +142,13 @@ async def get(cls, id, using=None, index=None, **kwargs):
113142
return cls.from_es(doc)
114143

115144
@classmethod
116-
async def exists(cls, id, using=None, index=None, **kwargs):
145+
async def exists(
146+
cls,
147+
id: str,
148+
using: Optional[AsyncUsingType] = None,
149+
index: Optional[str] = None,
150+
**kwargs: Any,
151+
) -> bool:
117152
"""
118153
check if exists a single document from elasticsearch using its ``id``.
119154
@@ -126,12 +161,18 @@ async def exists(cls, id, using=None, index=None, **kwargs):
126161
``Elasticsearch.exists`` unchanged.
127162
"""
128163
es = cls._get_connection(using)
129-
return await es.exists(index=cls._default_index(index), id=id, **kwargs)
164+
return bool(await es.exists(index=cls._default_index(index), id=id, **kwargs))
130165

131166
@classmethod
132167
async def mget(
133-
cls, docs, using=None, index=None, raise_on_error=True, missing="none", **kwargs
134-
):
168+
cls,
169+
docs: List[Dict[str, Any]],
170+
using: Optional[AsyncUsingType] = None,
171+
index: Optional[str] = None,
172+
raise_on_error: bool = True,
173+
missing: str = "none",
174+
**kwargs: Any,
175+
) -> List[Optional[Self]]:
135176
r"""
136177
Retrieve multiple document by their ``id``\s. Returns a list of instances
137178
in the same order as requested.
@@ -160,7 +201,9 @@ async def mget(
160201
}
161202
results = await es.mget(index=cls._default_index(index), body=body, **kwargs)
162203

163-
objs, error_docs, missing_docs = [], [], []
204+
objs: List[Optional[Self]] = []
205+
error_docs: List[Self] = []
206+
missing_docs: List[Self] = []
164207
for doc in results["docs"]:
165208
if doc.get("found"):
166209
if error_docs or missing_docs:
@@ -186,14 +229,19 @@ async def mget(
186229
error_ids = [doc["_id"] for doc in error_docs]
187230
message = "Required routing not provided for documents %s."
188231
message %= ", ".join(error_ids)
189-
raise RequestError(400, message, error_docs)
232+
raise RequestError(400, message, error_docs) # type: ignore
190233
if missing_docs:
191234
missing_ids = [doc["_id"] for doc in missing_docs]
192235
message = f"Documents {', '.join(missing_ids)} not found."
193-
raise NotFoundError(404, message, {"docs": missing_docs})
236+
raise NotFoundError(404, message, {"docs": missing_docs}) # type: ignore
194237
return objs
195238

196-
async def delete(self, using=None, index=None, **kwargs):
239+
async def delete(
240+
self,
241+
using: Optional[AsyncUsingType] = None,
242+
index: Optional[str] = None,
243+
**kwargs: Any,
244+
) -> None:
197245
"""
198246
Delete the instance in elasticsearch.
199247
@@ -214,23 +262,26 @@ async def delete(self, using=None, index=None, **kwargs):
214262
doc_meta["if_primary_term"] = self.meta["primary_term"]
215263

216264
doc_meta.update(kwargs)
217-
await es.delete(index=self._get_index(index), **doc_meta)
265+
i = self._get_index(index)
266+
assert i is not None
267+
268+
await es.delete(index=i, **doc_meta)
218269

219270
async def update(
220271
self,
221-
using=None,
222-
index=None,
223-
detect_noop=True,
224-
doc_as_upsert=False,
225-
refresh=False,
226-
retry_on_conflict=None,
227-
script=None,
228-
script_id=None,
229-
scripted_upsert=False,
230-
upsert=None,
231-
return_doc_meta=False,
232-
**fields,
233-
):
272+
using: Optional[AsyncUsingType] = None,
273+
index: Optional[str] = None,
274+
detect_noop: bool = True,
275+
doc_as_upsert: bool = False,
276+
refresh: bool = False,
277+
retry_on_conflict: Optional[int] = None,
278+
script: Optional[Union[str, Dict[str, Any]]] = None,
279+
script_id: Optional[str] = None,
280+
scripted_upsert: bool = False,
281+
upsert: Optional[Dict[str, Any]] = None,
282+
return_doc_meta: bool = False,
283+
**fields: Any,
284+
) -> Any:
234285
"""
235286
Partial update of the document, specify fields you wish to update and
236287
both the instance and the document in elasticsearch will be updated::
@@ -261,7 +312,7 @@ async def update(
261312
262313
:return: operation result noop/updated
263314
"""
264-
body = {
315+
body: Dict[str, Any] = {
265316
"doc_as_upsert": doc_as_upsert,
266317
"detect_noop": detect_noop,
267318
}
@@ -317,9 +368,13 @@ async def update(
317368
doc_meta["if_seq_no"] = self.meta["seq_no"]
318369
doc_meta["if_primary_term"] = self.meta["primary_term"]
319370

371+
i = self._get_index(index)
372+
assert i is not None
373+
320374
meta = await self._get_connection(using).update(
321-
index=self._get_index(index), body=body, refresh=refresh, **doc_meta
375+
index=i, body=body, refresh=refresh, **doc_meta
322376
)
377+
323378
# update meta information from ES
324379
for k in META_FIELDS:
325380
if "_" + k in meta:
@@ -329,13 +384,13 @@ async def update(
329384

330385
async def save(
331386
self,
332-
using=None,
333-
index=None,
334-
validate=True,
335-
skip_empty=True,
336-
return_doc_meta=False,
337-
**kwargs,
338-
):
387+
using: Optional[AsyncUsingType] = None,
388+
index: Optional[str] = None,
389+
validate: bool = True,
390+
skip_empty: bool = True,
391+
return_doc_meta: bool = False,
392+
**kwargs: Any,
393+
) -> Any:
339394
"""
340395
Save the document into elasticsearch. If the document doesn't exist it
341396
is created, it is overwritten otherwise. Returns ``True`` if this
@@ -369,8 +424,11 @@ async def save(
369424
doc_meta["if_primary_term"] = self.meta["primary_term"]
370425

371426
doc_meta.update(kwargs)
427+
i = self._get_index(index)
428+
assert i is not None
429+
372430
meta = await es.index(
373-
index=self._get_index(index),
431+
index=i,
374432
body=self.to_dict(skip_empty=skip_empty),
375433
**doc_meta,
376434
)

elasticsearch_dsl/_async/index.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,21 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18+
from typing import TYPE_CHECKING, Any, Optional
19+
20+
from typing_extensions import Self
21+
1822
from ..async_connections import get_connection
1923
from ..exceptions import IllegalOperation
2024
from ..index_base import IndexBase
25+
from ..utils import AsyncUsingType
2126
from .mapping import AsyncMapping
2227
from .search import AsyncSearch
2328
from .update_by_query import AsyncUpdateByQuery
2429

30+
if TYPE_CHECKING:
31+
from elastic_transport import ObjectApiResponse
32+
2533

2634
class AsyncIndexTemplate:
2735
def __init__(self, name, template, index=None, order=None, **kwargs):
@@ -56,7 +64,7 @@ async def save(self, using=None):
5664

5765

5866
class AsyncIndex(IndexBase):
59-
def __init__(self, name, using="default"):
67+
def __init__(self, name: str, using: AsyncUsingType = "default"):
6068
"""
6169
:arg name: name of the index
6270
:arg using: connection alias to use, defaults to ``'default'``
@@ -83,7 +91,9 @@ async def load_mappings(self, using=None):
8391
self._name, using=using or self._using
8492
)
8593

86-
def clone(self, name=None, using=None):
94+
def clone(
95+
self, name: Optional[str] = None, using: Optional[AsyncUsingType] = None
96+
) -> Self:
8797
"""
8898
Create a copy of the instance with another name or connection alias.
8999
Useful for creating multiple indices with shared configuration::
@@ -148,7 +158,7 @@ async def is_closed(self, using=None):
148158
)
149159
return state["metadata"]["indices"][self._name]["state"] == "close"
150160

151-
async def save(self, using=None):
161+
async def save(self, using: Optional[AsyncUsingType] = None):
152162
"""
153163
Sync the index definition with elasticsearch, creating the index if it
154164
doesn't exist and updating its settings and mappings if it does.
@@ -267,7 +277,9 @@ async def close(self, using=None, **kwargs):
267277
index=self._name, **kwargs
268278
)
269279

270-
async def delete(self, using=None, **kwargs):
280+
async def delete(
281+
self, using: Optional[AsyncUsingType] = None, **kwargs: Any
282+
) -> "ObjectApiResponse[Any]":
271283
"""
272284
Deletes the index in elasticsearch.
273285
@@ -278,15 +290,17 @@ async def delete(self, using=None, **kwargs):
278290
index=self._name, **kwargs
279291
)
280292

281-
async def exists(self, using=None, **kwargs):
293+
async def exists(
294+
self, using: Optional[AsyncUsingType] = None, **kwargs: Any
295+
) -> bool:
282296
"""
283297
Returns ``True`` if the index already exists in elasticsearch.
284298
285299
Any additional keyword arguments will be passed to
286300
``Elasticsearch.indices.exists`` unchanged.
287301
"""
288-
return await self._get_connection(using).indices.exists(
289-
index=self._name, **kwargs
302+
return bool(
303+
await self._get_connection(using).indices.exists(index=self._name, **kwargs)
290304
)
291305

292306
async def exists_type(self, using=None, **kwargs):

0 commit comments

Comments
 (0)