Skip to content
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

PYTHON-3064 Add typings to test package #844

Merged
merged 20 commits into from
Feb 8, 2022

Conversation

blink1073
Copy link
Member

No description provided.

@blink1073 blink1073 marked this pull request as ready for review February 3, 2022 16:42
Copy link
Member

@ShaneHarvey ShaneHarvey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few points of feedback.

test/test_son.py Outdated
self.assertTrue("2" in test_son, "in failed")
self.assertFalse("22" in test_son, "in succeeded when it shouldn't")
self.assertTrue(test_son.has_key("2"), "has_key failed")
self.assertFalse(test_son.has_key("22"), "has_key succeeded when it shouldn't")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of changing these tests we need to change SON itself because it supports non-str keys.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@@ -43,7 +42,7 @@ def __init__(self) -> None:
else:
self._poller = None

def select(self, sock: Any, read: bool = False, write: bool = False, timeout: int = 0) -> bool:
def select(self, sock: Any, read: bool = False, write: bool = False, timeout: Optional[Union[float, int]] = 0) -> bool:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeout should be float. The float type supports int automatically. Also is Optional needed? Do we ever pass None?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We call it with None explicitly in test_pooling.py, and there is internal handling for None. I'll change it to Optional[float]

@@ -186,7 +186,7 @@ def test_encode_then_decode_any_mapping_legacy(self):
decoder=lambda *args: BSON(args[0]).decode(*args[1:]))

def test_encoding_defaultdict(self):
dct = collections.defaultdict(dict, [('foo', 'bar')])
dct = collections.defaultdict(dict, [('foo', 'bar')]) # type: ignore[arg-type]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you report this as a mypy bug?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, thinking about it more, I don't think this is a bug. We're declaring that new values in the dictionary are of dictionary type, but the dictionary itself is initialized with a value of string type. If I use foo = defaultdict(dict, {'a': dict(a=1)}) in the mypy playground it works fine.

Copy link
Member

@ShaneHarvey ShaneHarvey Feb 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a mypy bug. The docs for defaultdict says:

The first argument provides the initial value for the default_factory attribute; it defaults to None. All remaining arguments are treated the same as if they were passed to the dict constructor, including keyword arguments.

eg:

>>> dict([('foo', 'bar')])
{'foo': 'bar'}
>>> dct = collections.defaultdict(dict, [('foo', 'bar')])
>>> dct
defaultdict(<class 'dict'>, {'foo': 'bar'})
>>> dct['g']
{}
>>> dct
defaultdict(<class 'dict'>, {'foo': 'bar', 'g': {}})

That said, we can change this line to:

dct = collections.defaultdict(dict, {'foo': 'bar'})

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my point is, they'd have to change the signature of defaultdict in typeshed to not use generics, or perhaps add more additional overrides to provide non-generic versions. I'll update the line instead. 😄

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see what you meant. Do you still think this is worth bringing up to typeshed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we should report this bug to typeshed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh sorry, I just realized I'm wrong here. The problem is that we're passing a 'str' value to a dictionary of type Dict['str', dict]. This is not a typeshed bug:

>>> dct = collections.defaultdict(dict, [('foo', 'bar')]) # This is a type error
>>> dct = collections.defaultdict(dict, [('foo', {})])  # This works.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, that is what I was poorly trying to convey. I still think the raised issue stands, but they are likely to ignore/close it.

test/test_bulk.py Outdated Show resolved Hide resolved
test/test_cursor.py Outdated Show resolved Hide resolved
test/utils.py Outdated Show resolved Hide resolved
test/utils.py Outdated Show resolved Hide resolved
@@ -46,4 +46,4 @@ jobs:
pip install -e ".[zstd, srv]"
- name: Run mypy
run: |
mypy --install-types --non-interactive bson gridfs tools
mypy --install-types --non-interactive bson gridfs tools test
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to separate the test/ type checking so that we can globally ignore certain error codes in the test/ package? Eg:

mypy --install-types --non-interactive bson gridfs tools
mypy --install-types --non-interactive --disable-error-code var-annotated --disable-error-code attr-defined --disable-error-code union-attr --disable-error-code assignment test

Disabling some checks could make the tests easier to maintain.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -45,7 +45,7 @@ def responder(request):
kwargs = {'socketTimeoutMS': 100}
# Disable retryable reads when pymongo supports it.
kwargs['retryReads'] = False
self.client = MongoClient(self.server.uri, **kwargs)
self.client = MongoClient(self.server.uri, **kwargs) # type: ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's up with the type ignore here? It seems perfectly reasonable to pass **kwargs to MongoClient because it accepts **kwargs. Is this something we should report to mypy?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it is a type limitation of mypy, I'll look for/file an issue

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found this comment: I'll try TypedDict

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it really is a bug, already reported: python/mypy#8862

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -121,6 +122,7 @@ def kill_change_stream_cursor(self, change_stream):


class APITestsMixin(object):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this file is improperly using inheritance. Could you open a tech debt ticket so we can fix this at some point?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I addressed the code review comments, I'll handle the mypy/JIRA tickets tomorrow

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blink1073 blink1073 requested a review from ShaneHarvey February 4, 2022 19:21
import sys
from typing import Any, Optional
from typing import Any, Optional, Union
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Union is now unused.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@@ -302,7 +302,7 @@ def test_basic_decode(self):

def test_decode_all_buffer_protocol(self):
docs = [{'foo': 'bar'}, {}]
bs = b"".join(map(encode, docs))
bs = b"".join(map(encode, docs)) # type: ignore[arg-type]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like another bug we can report to mypy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about this one. This is another example of a generic in the second argument affecting the value in the first, but I think this time it is warranted. If we change it to docs: list[Mapping[str, Any]] then it matches and passes type check.

@@ -890,6 +890,7 @@ def update_employee_info(session):
update_employee_info(session)

employee = employees.find_one({"employee": 3})
assert employee is not None
self.assertIsNotNone(employee)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is disappointing. I added a comment to the mypy issue tracker here: python/mypy#4063 (comment)

test/test_examples.py Outdated Show resolved Hide resolved
@@ -1091,6 +1095,8 @@ def test_causal_consistency(self):

# Start Causal Consistency Example 2
with client.start_session(causal_consistency=True) as s2:
assert s1.cluster_time is not None
assert s1.operation_time is not None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the same reason as above, let's move this asserts to outside the example (above "Start Causal Consistency Example 2"). Also I'm very confused how this assert helps at all since we're accessing s1.cluster_time again below. From earlier conversations I would have expected mypy to not narrow the type here since each property access can theoretically return a different instance.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Yeah, that does seem like a limitation.

test/test_grid_file.py Outdated Show resolved Hide resolved
test/__init__.py Outdated Show resolved Hide resolved
as_utc = (aware - aware.utcoffset()).replace(tzinfo=utc)
offset = aware.utcoffset()
assert offset is not None
as_utc = (aware - offset).replace(tzinfo=utc)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is disappointing. IIUC a datetime with a tzinfo should always return a non-None utcoffset(). I think it could be related to python/mypy#10067 but I think it's worth reporting as a separate issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the only way to do it is to do what I suggested in python/mypy#10067 (comment)

Copy link
Member

@ShaneHarvey ShaneHarvey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one minor change

# Start Causal Consistency Example 2
with client.start_session(causal_consistency=True) as s2:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unneeded blank line.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@blink1073 blink1073 requested a review from ShaneHarvey February 7, 2022 23:30
Copy link
Member

@ShaneHarvey ShaneHarvey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@blink1073
Copy link
Member Author

Merging since the failure is fixed by #848

@blink1073 blink1073 merged commit f4cef37 into mongodb:master Feb 8, 2022
@blink1073 blink1073 deleted the PYTHON-3064 branch February 8, 2022 01:33
juliusgeo pushed a commit to juliusgeo/mongo-python-driver that referenced this pull request Apr 5, 2022
juliusgeo pushed a commit to juliusgeo/mongo-python-driver that referenced this pull request Apr 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants