Skip to content

Commit 2bf0080

Browse files
addressed review feedback
1 parent 6654287 commit 2bf0080

File tree

4 files changed

+170
-27
lines changed

4 files changed

+170
-27
lines changed

docs/persistence.rst

+41-20
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,14 @@ Here are some simple examples:
114114
from typing import Optional
115115
116116
class Post(Document):
117-
title: str # same as Text(required=True)
118-
created_at: Optional[datetime] # same as Date(required=False)
119-
published: bool # same as Boolean(required=True)
117+
title: str # same as title = Text(required=True)
118+
created_at: Optional[datetime] # same as created_at = Date(required=False)
119+
published: bool # same as published = Boolean(required=True)
120+
121+
It is important to note that when using ``Field`` subclasses such as ``Text``,
122+
``Date`` and ``Boolean``, they must be given in the right-side of an assignment,
123+
as shown in examples above. Using these classes as type hints will result in
124+
errors.
120125

121126
Python types are mapped to their corresponding field type according to the
122127
following table:
@@ -140,10 +145,14 @@ following table:
140145
- ``Date(required=True)``
141146
* - ``date``
142147
- ``Date(format="yyyy-MM-dd", required=True)``
143-
144-
In addition to the above native types, a field can also be given a type hint
145-
of an ``InnerDoc`` subclass, in which case it becomes an ``Object`` field of
146-
that class. When the ``InnerDoc`` subclass is wrapped with ``List``, a
148+
* - ``InnerDocSubclass``
149+
- ``Object(InnerDocSubclass)``
150+
* - ``List(InnerDocSubclass)``
151+
- ``Nested(InnerDocSubclass)``
152+
153+
As noted in the last two rows of the table, a field can also be given a type
154+
hint of an ``InnerDoc`` subclass, in which case it becomes an ``Object`` field
155+
of that class. When the ``InnerDoc`` subclass is wrapped with ``List``, a
147156
``Nested`` field is created instead.
148157

149158
.. code:: python
@@ -157,38 +166,40 @@ that class. When the ``InnerDoc`` subclass is wrapped with ``List``, a
157166
...
158167
159168
class Post(Document):
160-
address: Address # same as Object(Address)
161-
comments: List[Comment] # same as Nested(Comment)
169+
address: Address # same as address = Object(Address)
170+
comments: List[Comment] # same as comments = Nested(Comment)
162171
163172
Unfortunately it is impossible to have Python type hints that uniquely
164173
identify every possible Elasticsearch field type. To choose a field type that
165-
is different thant the ones in the table above, the field instance can be added
174+
is different than the ones in the table above, the field instance can be added
166175
explicitly as a right-side assignment in the field declaration. The next
167176
example creates a field that is typed as ``str``, but is mapped to ``Keyword``
168177
instead of ``Text``:
169178

170179
.. code:: python
171180
172181
class MyDocument(Document):
173-
category: str = Keyword()
182+
category: str = Keyword(required=True)
174183
175184
This form can also be used when additional options need to be given to
176185
initialize the field, such as when using custom analyzer settings:
177186

178187
.. code:: python
179188
180189
class Comment(InnerDoc):
181-
content: str = Text(analyzer='snowball')
190+
content: str = Text(analyzer='snowball', required=True)
182191
183192
The standard ``Optional`` modifier from the Python ``typing`` package can be
184193
used to change a typed field from required to optional. The ``List`` modifier
185194
can be added to a field to convert it to an array, similar to using the
186195
``multi=True`` argument on the field object.
187196

188197
When using type hints as above, subclasses of ``Document`` and ``InnerDoc``
189-
inherit some of the behaviors associated with Python dataclasses. To add
190-
per-field dataclass options such as ``default`` or ``default_factory`` , the
191-
``mapped_field()`` wrapper can be used on the right side of a typed field
198+
inherit some of the behaviors associated with Python dataclasses, as defined by
199+
the `PEP 681 <https://peps.python.org/pep-0681/>`_ and the
200+
`dataclass_transform decorator <https://typing.readthedocs.io/en/latest/spec/dataclasses.html#dataclass-transform>`_.
201+
To add per-field dataclass options such as ``default`` or ``default_factory``,
202+
the ``mapped_field()`` wrapper can be used on the right side of a typed field
192203
declaration:
193204

194205
.. code:: python
@@ -197,7 +208,11 @@ declaration:
197208
title: str = mapped_field(default="no title")
198209
created_at: datetime = mapped_field(default_factory=datetime.now)
199210
published: bool = mapped_field(default=False)
200-
category: str = mapped_field(Keyword(), default="general")
211+
category: str = mapped_field(Keyword(required=True), default="general")
212+
213+
When using the ``mapped_field()`` wrapper function, an explicit field type
214+
instance can be passed as a first positional argument, as the ``category``
215+
field does in the example above.
201216

202217
Static type checkers such as `mypy <https://mypy-lang.org/>`_ and
203218
`pyright <https://github.com/microsoft/pyright>`_ can use the type hints and
@@ -210,15 +225,15 @@ using fields as class attributes. Consider the following example:
210225
.. code:: python
211226
212227
class MyDocument(Document):
213-
title: str = mapped_field(default="no title")
228+
title: str
214229
215230
doc = MyDocument()
216231
# doc.title is typed as "str" (correct)
217232
# MyDocument.title is also typed as "str" (incorrect)
218233
219234
To help type checkers correctly identify class attributes as such, the ``M``
220235
generic must be used as a wrapper to the type hint, as shown in the next
221-
example:
236+
examples:
222237

223238
.. code:: python
224239
@@ -230,10 +245,13 @@ example:
230245
231246
doc = MyDocument()
232247
# doc.title is typed as "str"
248+
# doc.created_at is typed as "datetime"
233249
# MyDocument.title is typed as "InstrumentedField"
250+
# MyDocument.created_at is typed as "InstrumentedField"
234251
235-
Note that the ``M`` type hint does not provide any runtime behavior, it just
236-
provides additional typing declarations for type checkers.
252+
Note that the ``M`` type hint does not provide any runtime behavior and its use
253+
is not required, but it can be useful to eliminate spurious type errors in IDEs
254+
or type checking builds.
237255

238256
The ``InstrumentedField`` objects returned when fields are accessed as class
239257
attributes are proxies for the field instances that can be used anywhere a
@@ -245,6 +263,9 @@ field needs to be referenced, such as when specifying sort options in a
245263
# sort by creation date descending, and title ascending
246264
s = MyDocument.search().sort(-MyDocument.created_at, MyDocument.title)
247265
266+
When specifying sorting order, the ``+`` and ``-`` unary operators can be used
267+
on the class field attributes to indicate ascending and descending order.
268+
248269
Note on dates
249270
~~~~~~~~~~~~~
250271

elasticsearch_dsl/document_base.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,16 @@ def __init__(self, name, field):
6262
self._field = field
6363

6464
def __getattr__(self, attr):
65-
f = None
6665
try:
67-
f = self._field[attr]
68-
except KeyError:
69-
pass
70-
if isinstance(f, Field):
71-
return InstrumentedField(f"{self._name}.{attr}", f)
72-
return getattr(self._field, attr)
66+
# first let's see if this is an attribute of this object
67+
return super().__getattribute__(attr)
68+
except AttributeError:
69+
try:
70+
# next we see if we have a sub-field with this name
71+
return InstrumentedField(f"{self._name}.{attr}", self._field[attr])
72+
except KeyError:
73+
# lastly we let the wrapped field resolve this attribute
74+
return getattr(self._field, attr)
7375

7476
def __pos__(self):
7577
"""Return the field name representation for ascending sort order"""

tests/_async/test_document.py

+60
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
mapped_field,
3939
utils,
4040
)
41+
from elasticsearch_dsl.document_base import InstrumentedField
4142
from elasticsearch_dsl.exceptions import IllegalOperation, ValidationException
4243

4344

@@ -755,3 +756,62 @@ class TypedDoc(AsyncDocument):
755756

756757
s = TypedDoc.search().sort(TypedDoc.st, -TypedDoc.dt, +TypedDoc.ob.st)
757758
assert s.to_dict() == {"sort": ["st", {"dt": {"order": "desc"}}, "ob.st"]}
759+
760+
761+
def test_instrumented_field():
762+
class Child(InnerDoc):
763+
st: M[str]
764+
765+
class Doc(AsyncDocument):
766+
st: str
767+
ob: Child
768+
ns: List[Child]
769+
770+
doc = Doc(
771+
st="foo",
772+
ob=Child(st="bar"),
773+
ns=[
774+
Child(st="baz"),
775+
Child(st="qux"),
776+
],
777+
)
778+
779+
assert type(doc.st) is str
780+
assert doc.st == "foo"
781+
782+
assert type(doc.ob) is Child
783+
assert doc.ob.st == "bar"
784+
785+
assert type(doc.ns) is utils.AttrList
786+
assert doc.ns[0].st == "baz"
787+
assert doc.ns[1].st == "qux"
788+
assert type(doc.ns[0]) is Child
789+
assert type(doc.ns[1]) is Child
790+
791+
assert type(Doc.st) is InstrumentedField
792+
assert str(Doc.st) == "st"
793+
assert +Doc.st == "st"
794+
assert -Doc.st == "-st"
795+
assert Doc.st.to_dict() == {"type": "text"}
796+
with raises(AttributeError):
797+
Doc.st.something
798+
799+
assert type(Doc.ob) is InstrumentedField
800+
assert str(Doc.ob) == "ob"
801+
assert str(Doc.ob.st) == "ob.st"
802+
assert +Doc.ob.st == "ob.st"
803+
assert -Doc.ob.st == "-ob.st"
804+
with raises(AttributeError):
805+
Doc.ob.something
806+
with raises(AttributeError):
807+
Doc.ob.st.something
808+
809+
assert type(Doc.ns) is InstrumentedField
810+
assert str(Doc.ns) == "ns"
811+
assert str(Doc.ns.st) == "ns.st"
812+
assert +Doc.ns.st == "ns.st"
813+
assert -Doc.ns.st == "-ns.st"
814+
with raises(AttributeError):
815+
Doc.ns.something
816+
with raises(AttributeError):
817+
Doc.ns.st.something

tests/_sync/test_document.py

+60
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
mapped_field,
3939
utils,
4040
)
41+
from elasticsearch_dsl.document_base import InstrumentedField
4142
from elasticsearch_dsl.exceptions import IllegalOperation, ValidationException
4243

4344

@@ -755,3 +756,62 @@ class TypedDoc(Document):
755756

756757
s = TypedDoc.search().sort(TypedDoc.st, -TypedDoc.dt, +TypedDoc.ob.st)
757758
assert s.to_dict() == {"sort": ["st", {"dt": {"order": "desc"}}, "ob.st"]}
759+
760+
761+
def test_instrumented_field():
762+
class Child(InnerDoc):
763+
st: M[str]
764+
765+
class Doc(Document):
766+
st: str
767+
ob: Child
768+
ns: List[Child]
769+
770+
doc = Doc(
771+
st="foo",
772+
ob=Child(st="bar"),
773+
ns=[
774+
Child(st="baz"),
775+
Child(st="qux"),
776+
],
777+
)
778+
779+
assert type(doc.st) is str
780+
assert doc.st == "foo"
781+
782+
assert type(doc.ob) is Child
783+
assert doc.ob.st == "bar"
784+
785+
assert type(doc.ns) is utils.AttrList
786+
assert doc.ns[0].st == "baz"
787+
assert doc.ns[1].st == "qux"
788+
assert type(doc.ns[0]) is Child
789+
assert type(doc.ns[1]) is Child
790+
791+
assert type(Doc.st) is InstrumentedField
792+
assert str(Doc.st) == "st"
793+
assert +Doc.st == "st"
794+
assert -Doc.st == "-st"
795+
assert Doc.st.to_dict() == {"type": "text"}
796+
with raises(AttributeError):
797+
Doc.st.something
798+
799+
assert type(Doc.ob) is InstrumentedField
800+
assert str(Doc.ob) == "ob"
801+
assert str(Doc.ob.st) == "ob.st"
802+
assert +Doc.ob.st == "ob.st"
803+
assert -Doc.ob.st == "-ob.st"
804+
with raises(AttributeError):
805+
Doc.ob.something
806+
with raises(AttributeError):
807+
Doc.ob.st.something
808+
809+
assert type(Doc.ns) is InstrumentedField
810+
assert str(Doc.ns) == "ns"
811+
assert str(Doc.ns.st) == "ns.st"
812+
assert +Doc.ns.st == "ns.st"
813+
assert -Doc.ns.st == "-ns.st"
814+
with raises(AttributeError):
815+
Doc.ns.something
816+
with raises(AttributeError):
817+
Doc.ns.st.something

0 commit comments

Comments
 (0)