Skip to content

Commit 1d30802

Browse files
authored
PYTHON-3074 Add documentation for type hints (#906)
1 parent a4bba9d commit 1d30802

File tree

8 files changed

+262
-3
lines changed

8 files changed

+262
-3
lines changed

doc/changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Changes in Version 4.1
88

99
PyMongo 4.1 brings a number of improvements including:
1010

11+
- Type Hinting support (formerly provided by ``pymongo-stubs``). See :doc:`examples/type_hints` for more information.
1112
- Added support for the ``let`` parameter to
1213
:meth:`~pymongo.collection.Collection.update_one`,
1314
:meth:`~pymongo.collection.Collection.update_many`,

doc/examples/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@ MongoDB, you can start it like so:
3131
server_selection
3232
tailable
3333
tls
34+
type_hints
3435
encryption
3536
uuid

doc/examples/type_hints.rst

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
2+
.. _type_hints-example:
3+
4+
Type Hints
5+
===========
6+
7+
As of version 4.1, PyMongo ships with `type hints`_. With type hints, Python
8+
type checkers can easily find bugs before they reveal themselves in your code.
9+
10+
If your IDE is configured to use type hints,
11+
it can suggest more appropriate completions and highlight errors in your code.
12+
Some examples include `PyCharm`_, `Sublime Text`_, and `Visual Studio Code`_.
13+
14+
You can also use the `mypy`_ tool from your command line or in Continuous Integration tests.
15+
16+
All of the public APIs in PyMongo are fully type hinted, and
17+
several of them support generic parameters for the
18+
type of document object returned when decoding BSON documents.
19+
20+
Due to `limitations in mypy`_, the default
21+
values for generic document types are not yet provided (they will eventually be ``Dict[str, any]``).
22+
23+
For a larger set of examples that use types, see the PyMongo `test_mypy module`_.
24+
25+
If you would like to opt out of using the provided types, add the following to
26+
your `mypy config`_: ::
27+
28+
[mypy-pymongo]
29+
follow_imports = False
30+
31+
32+
Basic Usage
33+
-----------
34+
35+
Note that a type for :class:`~pymongo.mongo_client.MongoClient` must be specified. Here we use the
36+
default, unspecified document type:
37+
38+
.. doctest::
39+
40+
>>> from pymongo import MongoClient
41+
>>> client: MongoClient = MongoClient()
42+
>>> collection = client.test.test
43+
>>> inserted = collection.insert_one({"x": 1, "tags": ["dog", "cat"]})
44+
>>> retrieved = collection.find_one({"x": 1})
45+
>>> assert isinstance(retrieved, dict)
46+
47+
For a more accurate typing for document type you can use:
48+
49+
.. doctest::
50+
51+
>>> from typing import Any, Dict
52+
>>> from pymongo import MongoClient
53+
>>> client: MongoClient[Dict[str, Any]] = MongoClient()
54+
>>> collection = client.test.test
55+
>>> inserted = collection.insert_one({"x": 1, "tags": ["dog", "cat"]})
56+
>>> retrieved = collection.find_one({"x": 1})
57+
>>> assert isinstance(retrieved, dict)
58+
59+
Typed Client
60+
------------
61+
62+
:class:`~pymongo.mongo_client.MongoClient` is generic on the document type used to decode BSON documents.
63+
64+
You can specify a :class:`~bson.raw_bson.RawBSONDocument` document type:
65+
66+
.. doctest::
67+
68+
>>> from pymongo import MongoClient
69+
>>> from bson.raw_bson import RawBSONDocument
70+
>>> client = MongoClient(document_class=RawBSONDocument)
71+
>>> collection = client.test.test
72+
>>> inserted = collection.insert_one({"x": 1, "tags": ["dog", "cat"]})
73+
>>> result = collection.find_one({"x": 1})
74+
>>> assert isinstance(result, RawBSONDocument)
75+
76+
Subclasses of :py:class:`collections.abc.Mapping` can also be used, such as :class:`~bson.son.SON`:
77+
78+
.. doctest::
79+
80+
>>> from bson import SON
81+
>>> from pymongo import MongoClient
82+
>>> client = MongoClient(document_class=SON[str, int])
83+
>>> collection = client.test.test
84+
>>> inserted = collection.insert_one({"x": 1, "y": 2 })
85+
>>> result = collection.find_one({"x": 1})
86+
>>> assert result is not None
87+
>>> assert result["x"] == 1
88+
89+
Note that when using :class:`~bson.son.SON`, the key and value types must be given, e.g. ``SON[str, Any]``.
90+
91+
92+
Typed Collection
93+
----------------
94+
95+
You can use :py:class:`~typing.TypedDict` when using a well-defined schema for the data in a :class:`~pymongo.collection.Collection`:
96+
97+
.. doctest::
98+
99+
>>> from typing import TypedDict
100+
>>> from pymongo import MongoClient, Collection
101+
>>> class Movie(TypedDict):
102+
... name: str
103+
... year: int
104+
...
105+
>>> client: MongoClient = MongoClient()
106+
>>> collection: Collection[Movie] = client.test.test
107+
>>> inserted = collection.insert_one({"name": "Jurassic Park", "year": 1993 })
108+
>>> result = collection.find_one({"name": "Jurassic Park"})
109+
>>> assert result is not None
110+
>>> assert result["year"] == 1993
111+
112+
Typed Database
113+
--------------
114+
115+
While less common, you could specify that the documents in an entire database
116+
match a well-defined shema using :py:class:`~typing.TypedDict`.
117+
118+
119+
.. doctest::
120+
121+
>>> from typing import TypedDict
122+
>>> from pymongo import MongoClient, Database
123+
>>> class Movie(TypedDict):
124+
... name: str
125+
... year: int
126+
...
127+
>>> client: MongoClient = MongoClient()
128+
>>> db: Database[Movie] = client.test
129+
>>> collection = db.test
130+
>>> inserted = collection.insert_one({"name": "Jurassic Park", "year": 1993 })
131+
>>> result = collection.find_one({"name": "Jurassic Park"})
132+
>>> assert result is not None
133+
>>> assert result["year"] == 1993
134+
135+
Typed Command
136+
-------------
137+
When using the :meth:`~pymongo.database.Database.command`, you can specify the document type by providing a custom :class:`~bson.codec_options.CodecOptions`:
138+
139+
.. doctest::
140+
141+
>>> from pymongo import MongoClient
142+
>>> from bson.raw_bson import RawBSONDocument
143+
>>> from bson import CodecOptions
144+
>>> client: MongoClient = MongoClient()
145+
>>> options = CodecOptions(RawBSONDocument)
146+
>>> result = client.admin.command("ping", codec_options=options)
147+
>>> assert isinstance(result, RawBSONDocument)
148+
149+
Custom :py:class:`collections.abc.Mapping` subclasses and :py:class:`~typing.TypedDict` are also supported.
150+
For :py:class:`~typing.TypedDict`, use the form: ``options: CodecOptions[MyTypedDict] = CodecOptions(...)``.
151+
152+
Typed BSON Decoding
153+
-------------------
154+
You can specify the document type returned by :mod:`bson` decoding functions by providing :class:`~bson.codec_options.CodecOptions`:
155+
156+
.. doctest::
157+
158+
>>> from typing import Any, Dict
159+
>>> from bson import CodecOptions, encode, decode
160+
>>> class MyDict(Dict[str, Any]):
161+
... def foo(self):
162+
... return "bar"
163+
...
164+
>>> options = CodecOptions(document_class=MyDict)
165+
>>> doc = {"x": 1, "y": 2 }
166+
>>> bsonbytes = encode(doc, codec_options=options)
167+
>>> rt_document = decode(bsonbytes, codec_options=options)
168+
>>> assert rt_document.foo() == "bar"
169+
170+
:class:`~bson.raw_bson.RawBSONDocument` and :py:class:`~typing.TypedDict` are also supported.
171+
For :py:class:`~typing.TypedDict`, use the form: ``options: CodecOptions[MyTypedDict] = CodecOptions(...)``.
172+
173+
174+
Troubleshooting
175+
---------------
176+
177+
Client Type Annotation
178+
~~~~~~~~~~~~~~~~~~~~~~
179+
If you forget to add a type annotation for a :class:`~pymongo.mongo_client.MongoClient` object you may get the followig ``mypy`` error::
180+
181+
from pymongo import MongoClient
182+
client = MongoClient() # error: Need type annotation for "client"
183+
184+
The solution is to annotate the type as ``client: MongoClient`` or ``client: MongoClient[Dict[str, Any]]``. See `Basic Usage`_.
185+
186+
Incompatible Types
187+
~~~~~~~~~~~~~~~~~~
188+
If you use the generic form of :class:`~pymongo.mongo_client.MongoClient` you
189+
may encounter a ``mypy`` error like::
190+
191+
from pymongo import MongoClient
192+
193+
client: MongoClient = MongoClient()
194+
client.test.test.insert_many(
195+
{"a": 1}
196+
) # error: Dict entry 0 has incompatible type "str": "int";
197+
# expected "Mapping[str, Any]": "int"
198+
199+
200+
The solution is to use ``client: MongoClient[Dict[str, Any]]`` as used in
201+
`Basic Usage`_ .
202+
203+
Actual Type Errors
204+
~~~~~~~~~~~~~~~~~~
205+
206+
Other times ``mypy`` will catch an actual error, like the following code::
207+
208+
from pymongo import MongoClient
209+
from typing import Mapping
210+
client: MongoClient = MongoClient()
211+
client.test.test.insert_one(
212+
[{}]
213+
) # error: Argument 1 to "insert_one" of "Collection" has
214+
# incompatible type "List[Dict[<nothing>, <nothing>]]";
215+
# expected "Mapping[str, Any]"
216+
217+
In this case the solution is to use ``insert_one({})``, passing a document instead of a list.
218+
219+
Another example is trying to set a value on a :class:`~bson.raw_bson.RawBSONDocument`, which is read-only.::
220+
221+
from bson.raw_bson import RawBSONDocument
222+
from pymongo import MongoClient
223+
224+
client = MongoClient(document_class=RawBSONDocument)
225+
coll = client.test.test
226+
doc = {"my": "doc"}
227+
coll.insert_one(doc)
228+
retreived = coll.find_one({"_id": doc["_id"]})
229+
assert retreived is not None
230+
assert len(retreived.raw) > 0
231+
retreived[
232+
"foo"
233+
] = "bar" # error: Unsupported target for indexed assignment
234+
# ("RawBSONDocument") [index]
235+
236+
.. _PyCharm: https://www.jetbrains.com/help/pycharm/type-hinting-in-product.html
237+
.. _Visual Studio Code: https://code.visualstudio.com/docs/languages/python
238+
.. _Sublime Text: https://github.com/sublimelsp/LSP-pyright
239+
.. _type hints: https://docs.python.org/3/library/typing.html
240+
.. _mypy: https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html
241+
.. _limitations in mypy: https://github.com/python/mypy/issues/3737
242+
.. _mypy config: https://mypy.readthedocs.io/en/stable/config_file.html
243+
.. _test_mypy module: https://github.com/mongodb/mongo-python-driver/blob/master/test/test_mypy.py

doc/index.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ everything you need to know to use **PyMongo**.
2828
:doc:`examples/encryption`
2929
Using PyMongo with client side encryption.
3030

31+
:doc:`examples/type_hints`
32+
Using PyMongo with type hints.
33+
3134
:doc:`faq`
3235
Some questions that come up often.
3336

pymongo/common.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,15 @@ def validate_document_class(
448448
option: str, value: Any
449449
) -> Union[Type[MutableMapping], Type[RawBSONDocument]]:
450450
"""Validate the document_class option."""
451-
if not issubclass(value, (abc.MutableMapping, RawBSONDocument)):
451+
# issubclass can raise TypeError for generic aliases like SON[str, Any].
452+
# In that case we can use the base class for the comparison.
453+
is_mapping = False
454+
try:
455+
is_mapping = issubclass(value, abc.MutableMapping)
456+
except TypeError:
457+
if hasattr(value, "__origin__"):
458+
is_mapping = issubclass(value.__origin__, abc.MutableMapping)
459+
if not is_mapping and not issubclass(value, RawBSONDocument):
452460
raise TypeError(
453461
"%s must be dict, bson.son.SON, "
454462
"bson.raw_bson.RawBSONDocument, or a "

test/mypy_fails/insert_many_dict.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from pymongo import MongoClient
22

3-
client = MongoClient()
3+
client: MongoClient = MongoClient()
44
client.test.test.insert_many(
55
{"a": 1}
66
) # error: Dict entry 0 has incompatible type "str": "int"; expected "Mapping[str, Any]": "int"

test/mypy_fails/insert_one_list.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from pymongo import MongoClient
22

3-
client = MongoClient()
3+
client: MongoClient = MongoClient()
44
client.test.test.insert_one(
55
[{}]
66
) # error: Argument 1 to "insert_one" of "Collection" has incompatible type "List[Dict[<nothing>, <nothing>]]"; expected "Mapping[str, Any]"

test/test_mypy.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,9 @@ def test_son_document_type(self) -> None:
309309
assert retreived is not None
310310
retreived["a"] = 1
311311

312+
def test_son_document_type_runtime(self) -> None:
313+
client = MongoClient(document_class=SON[str, Any], connect=False)
314+
312315

313316
class TestCommandDocumentType(unittest.TestCase):
314317
@only_type_check

0 commit comments

Comments
 (0)