-
Notifications
You must be signed in to change notification settings - Fork 1.1k
PYTHON-3454 Specifying a generic type for a collection does not correctly enforce type safety when inserting data #1081
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Verified to work in Python 3.8+. |
doc/examples/type_hints.rst
Outdated
|
|
||
| You can use :py:class:`~typing.TypedDict` (Python 3.8+) when using a well-defined schema for the data in a :class:`~pymongo.collection.Collection`: | ||
| You can use :py:class:`~typing_extensions.TypedDict` (Python 3.8+) when using a well-defined schema for the data in a :class:`~pymongo.collection.Collection`. | ||
| Note that all `schema_validation`_ for inserts and updates is done on the server. This is due to the fact that these methods automatically add |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need to remove the underscore to match the link below "schema validation"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also wrap lines at 80 or 100 chars.
| ) | ||
|
|
||
| @_csot.apply | ||
| def insert_many( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add a mypy test for insert_many too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And bulkWrite InsertOne if possible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
test/utils.py
Outdated
| that are configured on the client. | ||
| """ | ||
| hello = client.admin.command(HelloCompat.LEGACY_CMD) | ||
| hello: Any = client.admin.command(HelloCompat.LEGACY_CMD) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use dict instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
doc/examples/type_hints.rst
Outdated
|
|
||
| You can use :py:class:`~typing.TypedDict` (Python 3.8+) when using a well-defined schema for the data in a :class:`~pymongo.collection.Collection`: | ||
| You can use :py:class:`~typing_extensions.TypedDict` (Python 3.8+) when using a well-defined schema for the data in a :class:`~pymongo.collection.Collection`. | ||
| Note that all `schema_validation`_ for inserts and updates is done on the server. This is due to the fact that these methods automatically add |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also wrap lines at 80 or 100 chars.
test/test_mypy.py
Outdated
|
|
||
| try: | ||
| from typing import TypedDict # type: ignore[attr-defined] | ||
| from typing_extensions import NotRequired, TypedDict |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't this mean these tests will be skipped even when TypedDict exists because typing_extensions is not installed in CI?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added it to CI.
| ) | ||
|
|
||
| @_csot.apply | ||
| def insert_many( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And bulkWrite InsertOne if possible?
test/test_mypy.py
Outdated
| assert retreived["year"] == 1 | ||
| assert retreived["name"] == "a" | ||
|
|
||
| # TODO: mypy --install-types --non-interactive test/test_mypy.py |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the todo?
test/test_mypy.py
Outdated
| assert type(out["_id"]) == ObjectId | ||
|
|
||
| @only_type_check | ||
| def test_typeddict_document_type_empty(self) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of empty what about MovieWithoutId?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
doc/examples/type_hints.rst
Outdated
| You can use :py:class:`~typing.TypedDict` (Python 3.8+) when using a well-defined schema for the data in a :class:`~pymongo.collection.Collection`: | ||
| You can use :py:class:`~typing_extensions.TypedDict` (Python 3.8+) when using a well-defined schema for the data in a :class:`~pymongo.collection.Collection`. | ||
| Note that all `schema_validation`_ for inserts and updates is done on the server. This is due to the fact that these methods automatically add | ||
| an "_id" field. In the example below the "_id" field is marked by the :py:class:`~typing_extensions.NotRequired` notation to allow it to be accessed when reading |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is due to the fact that these methods automatically add an "_id" field.
Schema validation has nothing to do with drivers adding the _id field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
test/test_mypy.py
Outdated
| (coll.insert_one, mov), | ||
| (coll.bulk_write, [InsertOne(mov)]), | ||
| ]: | ||
| meth(arg) # type:ignore[operator] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How can we put a type ignore here? insert_many/insert_one/bulk_write working without raising a mypy error is the main purpose of this bug fix. I think we're loosing sight of what's important here because the PR is muddled by tests for find_one/_id/NotRequired. Can you open a new ticket for the NotRequired stuff? To fix the bug in PYTHON-3454 I believe we only need to mention schema validation should be configured on the server and add a test like this to confirm the bug is fixed:
def test_typeddict_document_type_insertion(self) -> None:
coll: Collection[Movie] = self.db.test
mov = Movie(name="THX-1138", year=1971)
coll.insert_one(mov)
coll.insert_many([mov])
coll.bulk_write([InsertOne(mov)])There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm finding it hard to separate out the NotRequired stuff in a way that makes sense (in the documentation). I have modified the tests to be similar to what you have, but with the NotRequired stuff as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's remove everything about _id, NotRequired, typing_extensions, and all the other tests besides the one above. All that is extra stuff that's only tangentially related to the issue. We already have tests that find_one correctly yields the collection generic type: https://github.com/mongodb/mongo-python-driver/blob/4.3.2/test/test_mypy.py#L299-L305
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused how it is tangentially related to the issue when the actual issue at hand is not just whether it accepts a TypedDict, but whether it is also being type-checked, no? The title specifically mentions inserting data, but also mentions enforcing type safety, which is why I included that in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also that test does check that the generic type is yielded, it does not also test that the correct MyPy errors occur during the various different ways you can specify _id.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, that is strange. Wouldn't that be an argument in favor of clarifying the _id field in this PR? The fact that the user will have to explicitly specify the _id if they want it to be type-checked. I'm not sure how this would not be very confusing without a full explanation of what is going on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the original example the TypeDict did not have an explicit _id field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but I view that as a deficiency in the original example. The _id field will always be inserted, and there are three ways that the user can use that with type-checking:
- Movie - would require changing our examples to pass in an explicit
_idfield. - MovieWithoutId - the
_idfield is just ignored and you cannot type check the_idfield (what our previous example was doing). - ImplicitMovie - would not require passing in an
_id, but would allow type-checking.
I don't think that the user just wanted the error to go away, but also wanted it to be type-checked. This would leave us with Movie and ImplicitMovie, one of which would require the user to always pass in an _id, and the other would have the expected behavior but requires NotRequired. Both of these options require an explanation to the user, though, because it depends on how they are using the TypedDict.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not including the _id is still a valid choice, if it is not relevant to your application.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a valid choice, but it has caveats to it and I'm afraid users will be confused if they expect to be able to type check the _id field just as any other field
7331570 to
4c2c265
Compare
blink1073
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
test/test_mypy.py
Outdated
| coll.insert_one(bad_movie) | ||
| coll.insert_many([bad_mov]) # type:ignore[list-item] | ||
| coll.insert_many( | ||
| [{"name": "THX-1138", "year": "WRONG TYPE"}] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The auto-formatter got us here, you need to move the ignore comment
|
There's also some unused imports causing the lint to fail. |
pymongo/operations.py
Outdated
| __slots__ = ("_doc",) | ||
|
|
||
| def __init__(self, document: _DocumentIn) -> None: | ||
| def __init__(self, document: Union[_DocumentIn, _DocumentType]) -> None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we defer this change to PYTHON-3493?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
doc/examples/type_hints.rst
Outdated
| .. doctest:: | ||
|
|
||
| >>> from typing import TypedDict | ||
| >>> from typing_extensions import TypedDict |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's remove typing_extensions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
.github/workflows/test-python.yml
Outdated
| - name: Install dependencies | ||
| run: | | ||
| python -m pip install -U pip mypy | ||
| python -m pip install -U pip mypy typing_extensions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's remove typing_extensions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
|
|
||
| # Not available in Python 3.7 | ||
| from typing_extensions import TypedDict | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's revert these changes and go back to using typing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We will need to keep using typing_extensions in this file, or else split off a different file to run mypy on Python 3.7. The reason it was working before is that we were inadvertently ignoring errors when running this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason it was working before is that we were inadvertently ignoring errors when running this file.
Can you explain? How was this fixed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We added a new invocation in GitHub Actions in this PR to run this file without disabling several error checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see thanks.
| import unittest | ||
| from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, List | ||
|
|
||
| try: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to keep this in a try/except so that python test/test_mypy.py (or python3.7 setup.py test -s test.test_mypy) works even when typing_extensions is not installed. It's fine for the typing checking job to depend on typing_extensions, but not the test suite itself.
No description provided.