diff --git a/docs/persistence.rst b/docs/persistence.rst index f505dc6b..10f36871 100644 --- a/docs/persistence.rst +++ b/docs/persistence.rst @@ -273,6 +273,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 ~~~~~~~~~~~~~ diff --git a/elasticsearch_dsl/document_base.py b/elasticsearch_dsl/document_base.py index a26aff22..a7026778 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, @@ -165,7 +166,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()) @@ -178,10 +182,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: @@ -198,6 +206,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 if type(type_) is UnionType: # a union given with the pipe syntax args = get_args(type_) diff --git a/tests/_async/test_document.py b/tests/_async/test_document.py index b00b7822..933d1809 100644 --- a/tests/_async/test_document.py +++ b/tests/_async/test_document.py @@ -27,7 +27,7 @@ import sys 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 @@ -676,6 +676,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 == { @@ -709,6 +711,9 @@ class TypedDoc(AsyncDocument): "s4": {"type": "text"}, } + TypedDoc.i1 = "foo" + TypedDoc.i2 = 123 + doc = TypedDoc() assert doc.k3 == "foo" assert doc.s4 == "foo" @@ -724,6 +729,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 c00ec195..919afdf7 100644 --- a/tests/_sync/test_document.py +++ b/tests/_sync/test_document.py @@ -27,7 +27,7 @@ import sys 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 @@ -676,6 +676,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 == { @@ -709,6 +711,9 @@ class TypedDoc(Document): "s4": {"type": "text"}, } + TypedDoc.i1 = "foo" + TypedDoc.i2 = 123 + doc = TypedDoc() assert doc.k3 == "foo" assert doc.s4 == "foo" @@ -724,6 +729,9 @@ class TypedDoc(Document): "s3", } + assert TypedDoc.i1 == "foo" + assert TypedDoc.i2 == 123 + doc.st = "s" doc.li = [1, 2, 3] doc.k1 = "k1"