Skip to content

Commit da0f34f

Browse files
miguelgrinberggithub-actions[bot]
authored andcommitted
Support pipe syntax to declare optional fields (#1937)
* Support pipe syntax to declare optional fields Fixes #1928 * type ignores for pipe syntax in 3.8/3.9 (cherry picked from commit 5511abe)
1 parent 51558ad commit da0f34f

File tree

4 files changed

+81
-3
lines changed

4 files changed

+81
-3
lines changed

Diff for: docs/persistence.rst

+5-3
Original file line numberDiff line numberDiff line change
@@ -147,16 +147,18 @@ following table:
147147
- ``Date(format="yyyy-MM-dd", required=True)``
148148

149149
To type a field as optional, the standard ``Optional`` modifier from the Python
150-
``typing`` package can be used. The ``List`` modifier can be added to a field
151-
to convert it to an array, similar to using the ``multi=True`` argument on the
152-
field object.
150+
``typing`` package can be used. When using Python 3.10 or newer, "pipe" syntax
151+
can also be used, by adding ``| None`` to a type. The ``List`` modifier can be
152+
added to a field to convert it to an array, similar to using the ``multi=True``
153+
argument on the field object.
153154

154155
.. code:: python
155156
156157
from typing import Optional, List
157158
158159
class MyDoc(Document):
159160
pub_date: Optional[datetime] # same as pub_date = Date()
161+
middle_name: str | None # same as middle_name = Text()
160162
authors: List[str] # same as authors = Text(multi=True, required=True)
161163
comments: Optional[List[str]] # same as comments = Text(multi=True)
162164

Diff for: elasticsearch_dsl/document_base.py

+14
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,15 @@
2929
Tuple,
3030
TypeVar,
3131
Union,
32+
get_args,
3233
overload,
3334
)
3435

36+
try:
37+
from types import UnionType # type: ignore[attr-defined]
38+
except ImportError:
39+
UnionType = None
40+
3541
from typing_extensions import dataclass_transform
3642

3743
from .exceptions import ValidationException
@@ -203,6 +209,14 @@ def __init__(self, name: str, bases: Tuple[type, ...], attrs: Dict[str, Any]):
203209
if skip or type_ == ClassVar:
204210
# skip ClassVar attributes
205211
continue
212+
if type(type_) is UnionType:
213+
# a union given with the pipe syntax
214+
args = get_args(type_)
215+
if len(args) == 2 and args[1] is type(None):
216+
required = False
217+
type_ = type_.__args__[0]
218+
else:
219+
raise TypeError("Unsupported union")
206220
field = None
207221
field_args: List[Any] = []
208222
field_kwargs: Dict[str, Any] = {}

Diff for: tests/_async/test_document.py

+31
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import codecs
2525
import ipaddress
2626
import pickle
27+
import sys
2728
from datetime import datetime
2829
from hashlib import md5
2930
from typing import Any, ClassVar, Dict, List, Optional
@@ -791,6 +792,36 @@ class TypedDoc(AsyncDocument):
791792
}
792793

793794

795+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires Python 3.10")
796+
def test_doc_with_pipe_type_hints() -> None:
797+
with pytest.raises(TypeError):
798+
799+
class BadlyTypedDoc(AsyncDocument):
800+
s: str
801+
f: str | int | None # type: ignore[syntax]
802+
803+
class TypedDoc(AsyncDocument):
804+
s: str
805+
f1: str | None # type: ignore[syntax]
806+
f2: M[int | None] # type: ignore[syntax]
807+
f3: M[datetime | None] # type: ignore[syntax]
808+
809+
props = TypedDoc._doc_type.mapping.to_dict()["properties"]
810+
assert props == {
811+
"s": {"type": "text"},
812+
"f1": {"type": "text"},
813+
"f2": {"type": "integer"},
814+
"f3": {"type": "date"},
815+
}
816+
817+
doc = TypedDoc()
818+
with raises(ValidationException) as exc_info:
819+
doc.full_clean()
820+
assert set(exc_info.value.args[0].keys()) == {"s"}
821+
doc.s = "s"
822+
doc.full_clean()
823+
824+
794825
def test_instrumented_field() -> None:
795826
class Child(InnerDoc):
796827
st: M[str]

Diff for: tests/_sync/test_document.py

+31
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import codecs
2525
import ipaddress
2626
import pickle
27+
import sys
2728
from datetime import datetime
2829
from hashlib import md5
2930
from typing import Any, ClassVar, Dict, List, Optional
@@ -791,6 +792,36 @@ class TypedDoc(Document):
791792
}
792793

793794

795+
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires Python 3.10")
796+
def test_doc_with_pipe_type_hints() -> None:
797+
with pytest.raises(TypeError):
798+
799+
class BadlyTypedDoc(Document):
800+
s: str
801+
f: str | int | None # type: ignore[syntax]
802+
803+
class TypedDoc(Document):
804+
s: str
805+
f1: str | None # type: ignore[syntax]
806+
f2: M[int | None] # type: ignore[syntax]
807+
f3: M[datetime | None] # type: ignore[syntax]
808+
809+
props = TypedDoc._doc_type.mapping.to_dict()["properties"]
810+
assert props == {
811+
"s": {"type": "text"},
812+
"f1": {"type": "text"},
813+
"f2": {"type": "integer"},
814+
"f3": {"type": "date"},
815+
}
816+
817+
doc = TypedDoc()
818+
with raises(ValidationException) as exc_info:
819+
doc.full_clean()
820+
assert set(exc_info.value.args[0].keys()) == {"s"}
821+
doc.s = "s"
822+
doc.full_clean()
823+
824+
794825
def test_instrumented_field() -> None:
795826
class Child(InnerDoc):
796827
st: M[str]

0 commit comments

Comments
 (0)