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

add generics type parameters to ImmutableMultiDict #1449

Merged
merged 26 commits into from
May 24, 2022

Conversation

adriangb
Copy link
Member

@adriangb adriangb commented Jan 29, 2022

Slice of #1403

The goal here is to make these data structures more reusable and fix some currently under specified type annotations that make type checkers unhappy downstream

@tomchristie
Copy link
Member

In the trade-off between readability vs. type-strictness I'm personally happy enough to stick with the under-defined "Any" cases, because I just find it so awkwardly complicated to read through the annotations. (And because Any in these cases isn't incorrect, it's just under-specifying.)

I don't have strong objections to it, but just gonna pitch that one out there.

@adriangb
Copy link
Member Author

Yeah, I see that side of this as well.

What for me makes this worth it is that:

  1. These data structures have a lot of downstream consumers (both inside of Starlette and in anything that depends on Starlette
  2. We're unlikely to change these classes much, they're pretty stable.
  3. We also gain some readability, for example looking at class FormData(ImmutableMultiDict[str, Union[str, UploadFile]) you'll be able to tell that it's a mapping of string field names to strings or UploadFiles without looking at the docs (and IDEs will also give you this info)

So we do give up some readability in Starlette's codebase, but we will be improving legibility for downstream classes and consumers of the codebase.

But that's just my thought process / personal bias, I'll understand if others are not of the same opinion.

@adriangb
Copy link
Member Author

adriangb commented Jan 31, 2022

As a motivating example of what this can do for users of Starlette and downstream packages:

from starlette.requests import Request

async def func(req: Request) -> str:
    form = await req.form()
    file_or_string = form["field"]
    if isinstance(file_or_string, str):
        return file_or_string
    return (await file_or_string.read()).decode()

Running mypy on this:

test.py:9: error: Statement is unreachable  [unreachable]
Found 1 error in 1 file (checked 1 source file)

Because mypy thinks that file_or_string is always a string.

And from an IDE's perspective:

image

I get no auto-complete for .read(), thus no auto-complete for .decode() as well as several false positives like a redundant isinstance check and returning an unknown type.

On the other hand, this is what it looks like on #1403 (which this current PR is working towards):

image

IDE examples are on VSCode + Pylance (which is what a large percentage of devs use nowadays)

@tomchristie
Copy link
Member

Always appreciate seeing a good user-centred "this is why we might choose to make this change". 😎

@adriangb adriangb added clean up Refinement to existing functionality refactor Refactor code typing Type annotations or mypy issues and removed clean up Refinement to existing functionality refactor Refactor code labels Feb 2, 2022
@Kludex Kludex added this to the Version 0.20.0 milestone Apr 21, 2022
@Kludex Kludex mentioned this pull request Apr 22, 2022
2 tasks
@Kludex Kludex modified the milestones: Version 0.20.0, Version 0.21.0 Apr 28, 2022
@Kludex Kludex mentioned this pull request May 22, 2022
5 tasks
typing.List[typing.Tuple[typing.Any, typing.Any]],
"ImmutableMultiDict[_KT, _VT_co]",
typing.Mapping[_KT, _VT_co],
typing.Sequence[typing.Tuple[_KT, _VT_co]],
Copy link
Sponsor Member

@Kludex Kludex May 23, 2022

Choose a reason for hiding this comment

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

@@ -254,7 +261,7 @@ def __init__(
if kwargs:
value = (
ImmutableMultiDict(value).multi_items()
+ ImmutableMultiDict(kwargs).multi_items()
+ ImmutableMultiDict(kwargs).multi_items() # type: ignore[operator]
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

We don't know the returned value of the operator + when there's a sum of two ImmutableMultiDict. That's why we ignore this error here.

Copy link
Sponsor Member

@Kludex Kludex left a comment

Choose a reason for hiding this comment

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

This PR is clear to me, and after I've start using a more strict type checking environment, I do feel @adriangb 's pain.

I'm not sure if _T, _KT and _VT_co are the best names for those, even if this convention is spreadly used... Maybe DefaultType, KeyType and ValueType can improve the readability, and @tomchristie will see it with better eyes (?).

@Kludex Kludex requested a review from tomchristie May 23, 2022 05:43
@adriangb
Copy link
Member Author

Maybe DefaultType, KeyType and ValueType can improve the readability

I'm 👍 on this, happy to make the change or accept suggestions from either of you.

@adriangb
Copy link
Member Author

Rename done

) -> typing.Union[_ValueType, _DefaultType]:
... # pragma: no cover

def get(self, key: _KeyType, default: typing.Any = None) -> typing.Any:
Copy link
Member

Choose a reason for hiding this comment

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

https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes

I'm not quite sure if there are other benefits to implementing mixin methods like .get. MutableMapping already implements them. And they are already of the correct type.

image

Copy link
Member Author

Choose a reason for hiding this comment

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

So we can just delete get here! Great point. This makes this a PR that (maybe) changes behavior instead of just typing, but I think that's worth it 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank again for this, great catch!

Comment on lines -223 to +225
assert len(q.get("abc")) == 0
val = q.get("abc")
assert val is not None
assert len(val) == 0
Copy link
Member Author

Choose a reason for hiding this comment

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

This is just to make mypy happy

@@ -382,7 +382,7 @@ def update(
self._dict.update(value)


class QueryParams(ImmutableMultiDict):
class QueryParams(ImmutableMultiDict[str, str]):
Copy link
Member Author

@adriangb adriangb May 24, 2022

Choose a reason for hiding this comment

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

Just to make MyPy happy, previously it was picking up str from ImmutableMultiDict but now that ImmutableMultiDict is generic it was complaining about an unknown type.

Copy link
Member

Choose a reason for hiding this comment

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

Also means we now get QueryParams(...).get(...) to be a str, rather than Any. Which is neat and correct, right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep! There's a lot of downstream work from this (like actually fixing Form), but I wanted to keep this PR as narrowly scoped as possible. Hopefully we can approve and merge those downstream PRs quicker once the basic structure for stronger typing is added via this PR.

Copy link
Member

@florimondmanca florimondmanca left a comment

Choose a reason for hiding this comment

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

I do like this as well. I had already found myself in cases when the default typing on Form required me to do unecessary assert or isinstance checks to benefit from typing.

starlette/datastructures.py Show resolved Hide resolved
starlette/datastructures.py Outdated Show resolved Hide resolved
adriangb and others added 4 commits May 24, 2022 06:23
Co-authored-by: Florimond Manca <florimond.manca@protonmail.com>
Co-authored-by: Florimond Manca <florimond.manca@protonmail.com>
@adriangb adriangb merged commit 9b31073 into encode:master May 24, 2022
@adriangb adriangb deleted the generic-immutable-multi-dict branch May 24, 2022 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
typing Type annotations or mypy issues
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants