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

feat: fully type annotate datastructures.py #1403

Closed
wants to merge 9 commits into from

Conversation

adriangb
Copy link
Member

This adds annotations to datastructes.py.
Mainly this is making the mapping-like classes generic.
This means that FormData, Headers, etc. can be properly type annotated.

@adriangb
Copy link
Member Author

adriangb commented Jan 11, 2022

I'm not going to do it in this PR, but it would also be good to make the arguments to ImmutableMultiDict.get keyword only arguments like they are in the base Mapping classes, at least from a type annotation perspective (so static type checkers would break, but runtime would be the same, making this is mostly non-breaking change)

Comment on lines 136 to 139
@property
def client(self) -> Address:
host, port = self.scope.get("client") or (None, None)
return Address(host=host, port=port)
return Address(host=host, port=port) # type: ignore[arg-type]
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Hmmm... This is actually not ASGI compliant: https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope

client (Iterable[Unicode string, int]) – A two-item iterable of [host, port], where host is the remote host’s IPv4 or IPv6 address, and port is the remote port as an integer. Optional; if missing defaults to None.

Copy link
Member Author

@adriangb adriangb Jan 29, 2022

Choose a reason for hiding this comment

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

How is this not ASGI compliant? The NamedTuple is a tw-item utterable of [host, port]. It's the same thing we had before. In fact, I don't think we even need the # type: ignore. I just removed it in 417a3d7 and now there are 0 changes in this file.

Copy link
Sponsor Member

@Kludex Kludex Jan 29, 2022

Choose a reason for hiding this comment

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

Because Address is not supposed to accept (None, None), client should actually be Optional[Address].

My bad anyway, I should have mentioned that it's not because of your PR... I just noticed that it's currently wrong.

Copy link
Member Author

Choose a reason for hiding this comment

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

let's fix it in another PR then 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

Opened #1462

self, url: str = "", scope: Scope = None, **components: typing.Any
self,
url: str = "",
scope: typing.Optional[Scope] = None,
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Suggested change
scope: typing.Optional[Scope] = None,
scope: Scope = 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.

Could you explain this change request to me please, or reference some documentation on the subject? I understand that there is both a practical and semantic difference, but as far as I understand using Scope | None is now preferred if the default value is None. For example, see https://stackoverflow.com/questions/62732402/can-i-omit-optional-if-i-set-default-to-none

Comment on lines +234 to +240
@typing.overload
def __getitem__(self, index: int) -> str:
... # pragma: no cover

@typing.overload
def __getitem__(self, index: slice) -> typing.Sequence[str]:
... # pragma: no cover
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

If this PR doesn't get merged, you can also create a separate PR for this. 👍

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 may have to split this up

@@ -246,7 +270,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.

Why is not a valid operation?

Copy link
Member Author

Choose a reason for hiding this comment

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

because the type of value cannot be inferred from the expression value = args[0] if args or []

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 could be fixed with a bit of juggling of how this is handled, but I wanted to do this with the least amount of runtime changes necessary, so adding a type: ignore for now and maybe doing a rework in a future PR seems like the best bet

Comment on lines 308 to 321
@typing.overload
def get(self, key: _KT) -> _VT_co:
... # pragma: no cover

@typing.overload
def get(
self, key: _KT, default: typing.Optional[typing.Union[_VT_co, _T]] = ...
) -> typing.Union[_VT_co, _T]:
... # pragma: no cover

def get(self, key: _KT, default: typing.Any = None) -> typing.Any:
if key in self._dict:
return self._dict[key]
return typing.cast(_VT_co, self._dict[key])
return default
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Not sure if I fully understand the reason of what is written here. 🤔

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 saying:

  • If you do not provide a default, the return value is going to be of type _VT_co (so a value of the dictionary) (or an error)
  • If you do provide a default value, the return value is going to be either _VT_co (if the key is found) or the type of your default value (if the key is not found)

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 removed the typing.cast by giving ImmutableMultiDict._dict a type annotation

@adriangb
Copy link
Member Author

Closing in favor of:

Once / if #1449 gets merged, I'll make follow ups for MultiDict, Headers and FormData (which all depend on ImmutableMultiDict)

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.

None yet

2 participants