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

Typing: replace "type" with specific metaclass #2181

Merged
merged 4 commits into from
Oct 9, 2024
Merged

Typing: replace "type" with specific metaclass #2181

merged 4 commits into from
Oct 9, 2024

Conversation

lafrech
Copy link
Member

@lafrech lafrech commented Sep 10, 2023

See #2164.

Feedback welcome.

@deckar01
Copy link
Member

deckar01 commented Sep 11, 2023

It looks like there are two last anonymous type annotations that could be fixed. f8f4c8f

Changing the return type of dict_class() to type[Mapping] reveals that Mapping doesn't provide some of the dict interface we are using.

The _T TypeVar is hiding some of that incompatibility from the type checker also. serialize doesn't actually provide a generic pass through of the input type. It takes any dict type as input, and regardless of that type it returns an instance of the configured dict_class().

@lafrech
Copy link
Member Author

lafrech commented Nov 21, 2023

I applied @deckar01's commit and rebased.

Guys, feel free to merge.

Copy link
Member

@sloria sloria left a comment

Choose a reason for hiding this comment

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

the changes in fields.py look right to me, but i'm less sure about the changes to schema.py. can you double check them?

@@ -497,7 +498,7 @@ def _call_and_store(getter_func, data, *, field_name, error_store, index=None):
return error.valid_data or missing
return value

def _serialize(self, obj: _T | typing.Iterable[_T], *, many: bool = False):
def _serialize(self, obj: dict | typing.Iterable[dict], *, many: bool = False):
Copy link
Member

Choose a reason for hiding this comment

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

this isn't quite right, is it? obj can be any arbitrary object type or an iterable objects.

Copy link
Member

@deckar01 deckar01 Nov 21, 2023

Choose a reason for hiding this comment

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

Per https://github.com/marshmallow-code/marshmallow/pull/2181/files#r1401344892, the issue was that _T was being used incorrectly in _deserialize. Moving it to load() would avoid the need to change this TypeVar.

Copy link
Member

Choose a reason for hiding this comment

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

Per #2181 (comment) obj should be Any.

@@ -510,7 +511,7 @@ def _serialize(self, obj: _T | typing.Iterable[_T], *, many: bool = False):
if many and obj is not None:
return [
self._serialize(d, many=False)
for d in typing.cast(typing.Iterable[_T], obj)
for d in typing.cast(typing.Iterable[dict], obj)
Copy link
Member

Choose a reason for hiding this comment

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

related to comment above: i think this was already correct

Copy link
Member

@deckar01 deckar01 Nov 21, 2023

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.

Per #2181 (comment) this should be Any and the cast is not necessary.

@@ -584,7 +585,7 @@ def _deserialize(
partial=None,
unknown=RAISE,
index=None,
) -> _T | list[_T]:
) -> dict | list[dict]:
Copy link
Member

Choose a reason for hiding this comment

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

related to above: _deserialize is expected to return an arbitrary object or list of objects, not dictionaries.

Copy link
Member

@deckar01 deckar01 Nov 21, 2023

Choose a reason for hiding this comment

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

ret_d = self.dict_class() ... return ret_d implies that the return type of _deserialize() is the return type of dict_class() (Mapping). That change reveals that our usage of the return value actually depends on the dict interface. The fact that the type checker doesn't error on key access to a TypeVar which is effectively Any, seems like a mypy bug, but maybe it just needs explicit test coverage with a concrete type.

Enveloping occurs in _do_load(), so the return type of _deserialize() should not be constrained by the TypeVar. Now that I understand the purpose of the TypeVar it looks like it should be moved to load() rather than removed.

Copy link
Member

Choose a reason for hiding this comment

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

When I tested my proposed change mypy errored with "A function returning TypeVar should receive at least one argument containing the same TypeVar". I thought _T was trying to enforce a generic type between two methods, but it can't actually do that. The type vars were only used to cast list items in recursive calls.

Since none of these methods actually pass through an input type, Any should be used and those recursive cast operations are unnecessary.

@lafrech
Copy link
Member Author

lafrech commented May 28, 2024

I tried to apply the changes discussed in comment. I'm not 100% sure I got everything right.

mypy seems to be happy with those changes.

@lafrech
Copy link
Member Author

lafrech commented Oct 1, 2024

I just fixed _deserialize as per #2181 (comment).

I think I applied all suggested changes.

@deckar01 deckar01 self-requested a review October 3, 2024 16:27
@@ -501,7 +502,9 @@ def _call_and_store(getter_func, data, *, field_name, error_store, index=None):
return error.valid_data or missing
return value

def _serialize(self, obj: _T | typing.Iterable[_T], *, many: bool = False):
def _serialize(
self, obj: typing.Any | typing.Iterable[dict], *, many: bool = False
Copy link
Member Author

Choose a reason for hiding this comment

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

obj: typing.Any | typing.Iterable[dict]

should have been

obj: typing.Any | typing.Iterable[typing.Any]

which anyway can be simplified as

obj: typing.Any

from mypy perspective at least, for a human | typing.Iterable[typing.Any] indicates a clearer intent about the many case.

Copy link
Member Author

@lafrech lafrech Oct 7, 2024

Choose a reason for hiding this comment

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

I just force-pushed a modified commit with just obj: typing.Any.

@lafrech lafrech merged commit 0452065 into dev Oct 9, 2024
9 checks passed
@lafrech lafrech deleted the typing_type branch October 9, 2024 12:49
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.

3 participants