From ee66938d228c79343dfa97f7716b03a2fb0860f9 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 5 Nov 2024 12:20:19 +0000 Subject: [PATCH 1/2] Do not map ClassVar attributes Fixes #1927 --- elasticsearch_dsl/document_base.py | 15 +++++++++++++-- elasticsearch_dsl/response/__init__.py | 2 +- tests/_async/test_document.py | 10 +++++++++- tests/_sync/test_document.py | 10 +++++++++- 4 files changed, 32 insertions(+), 5 deletions(-) diff --git a/elasticsearch_dsl/document_base.py b/elasticsearch_dsl/document_base.py index 83445aec..67eae0ab 100644 --- a/elasticsearch_dsl/document_base.py +++ b/elasticsearch_dsl/document_base.py @@ -21,6 +21,7 @@ TYPE_CHECKING, Any, Callable, + ClassVar, Dict, Generic, List, @@ -159,7 +160,10 @@ def __init__(self, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any]): # field8: M[str] = mapped_field(MyCustomText(), default="foo") # # # legacy format without Python typing - # field8 = Text() + # field9 = Text() + # + # # ignore attributes + # field10: ClassVar[string] = "a regular class variable" annotations = attrs.get("__annotations__", {}) fields = set([n for n in attrs if isinstance(attrs[n], Field)]) fields.update(annotations.keys()) @@ -172,10 +176,14 @@ def __init__(self, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any]): # the field has a type annotation, so next we try to figure out # what field type we can use type_ = annotations[name] + skip = False required = True multi = False while hasattr(type_, "__origin__"): - if type_.__origin__ == Mapped: + if type_.__origin__ == ClassVar: + skip = True + break + elif type_.__origin__ == Mapped: # M[type] -> extract the wrapped type type_ = type_.__args__[0] elif type_.__origin__ == Union: @@ -192,6 +200,9 @@ def __init__(self, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any]): type_ = type_.__args__[0] else: break + if skip or type_ == ClassVar: + # skip ClassVar attributes + continue field = None field_args: List[Any] = [] field_kwargs: Dict[str, Any] = {} diff --git a/elasticsearch_dsl/response/__init__.py b/elasticsearch_dsl/response/__init__.py index 51c24ff9..a711950f 100644 --- a/elasticsearch_dsl/response/__init__.py +++ b/elasticsearch_dsl/response/__init__.py @@ -44,7 +44,7 @@ class Response(AttrDict[Any], Generic[_R]): - """An Elasticsearch response. + """An Elasticsearch _search response. :arg took: (required) :arg timed_out: (required) diff --git a/tests/_async/test_document.py b/tests/_async/test_document.py index 0233ddcc..00a570b2 100644 --- a/tests/_async/test_document.py +++ b/tests/_async/test_document.py @@ -26,7 +26,7 @@ import pickle from datetime import datetime from hashlib import md5 -from typing import Any, Dict, List, Optional +from typing import Any, ClassVar, Dict, List, Optional import pytest from pytest import raises @@ -675,6 +675,8 @@ class TypedDoc(AsyncDocument): s4: M[Optional[Secret]] = mapped_field( SecretField(), default_factory=lambda: "foo" ) + i1: ClassVar + i2: ClassVar[int] props = TypedDoc._doc_type.mapping.to_dict()["properties"] assert props == { @@ -708,6 +710,9 @@ class TypedDoc(AsyncDocument): "s4": {"type": "text"}, } + TypedDoc.i1 = "foo" + TypedDoc.i2 = 123 + doc = TypedDoc() assert doc.k3 == "foo" assert doc.s4 == "foo" @@ -723,6 +728,9 @@ class TypedDoc(AsyncDocument): "s3", } + assert TypedDoc.i1 == "foo" + assert TypedDoc.i2 == 123 + doc.st = "s" doc.li = [1, 2, 3] doc.k1 = "k1" diff --git a/tests/_sync/test_document.py b/tests/_sync/test_document.py index 4ba6992f..dddfc688 100644 --- a/tests/_sync/test_document.py +++ b/tests/_sync/test_document.py @@ -26,7 +26,7 @@ import pickle from datetime import datetime from hashlib import md5 -from typing import Any, Dict, List, Optional +from typing import Any, ClassVar, Dict, List, Optional import pytest from pytest import raises @@ -675,6 +675,8 @@ class TypedDoc(Document): s4: M[Optional[Secret]] = mapped_field( SecretField(), default_factory=lambda: "foo" ) + i1: ClassVar + i2: ClassVar[int] props = TypedDoc._doc_type.mapping.to_dict()["properties"] assert props == { @@ -708,6 +710,9 @@ class TypedDoc(Document): "s4": {"type": "text"}, } + TypedDoc.i1 = "foo" + TypedDoc.i2 = 123 + doc = TypedDoc() assert doc.k3 == "foo" assert doc.s4 == "foo" @@ -723,6 +728,9 @@ class TypedDoc(Document): "s3", } + assert TypedDoc.i1 == "foo" + assert TypedDoc.i2 == 123 + doc.st = "s" doc.li = [1, 2, 3] doc.k1 = "k1" From c62f7beeb9159caf7289dc88ed98d9c72a242a8e Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Tue, 5 Nov 2024 12:37:29 +0000 Subject: [PATCH 2/2] document ClassVar usage --- docs/persistence.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/persistence.rst b/docs/persistence.rst index ab3ffbe4..da7f488c 100644 --- a/docs/persistence.rst +++ b/docs/persistence.rst @@ -271,6 +271,18 @@ field needs to be referenced, such as when specifying sort options in a When specifying sorting order, the ``+`` and ``-`` unary operators can be used on the class field attributes to indicate ascending and descending order. +Finally, the ``ClassVar`` annotation can be used to define a regular class +attribute that should not be mapped to the Elasticsearch index:: + +.. code:: python + + from typing import ClassVar + + class MyDoc(Document): + title: M[str] + created_at: M[datetime] = mapped_field(default_factory=datetime.now) + my_var: ClassVar[str] # regular class variable, ignored by Elasticsearch + Note on dates ~~~~~~~~~~~~~