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

ClientSession.timeout has an incorrect typing #6917

Closed
1 task done
DevilXD opened this issue Sep 17, 2022 · 5 comments
Closed
1 task done

ClientSession.timeout has an incorrect typing #6917

DevilXD opened this issue Sep 17, 2022 · 5 comments
Labels

Comments

@DevilXD
Copy link
Contributor

DevilXD commented Sep 17, 2022

Describe the bug

The aiohttp.ClientSession.timeout attribute has a type of Union[object, aiohttp.ClientTimeout], however the code logic will never actually assign a bare object type to the self._timeout attribute, making this typing quite over-inclusive. Trying to use this attribute in typed code results in having to use cast(aiohttp.ClientTimeout, session.timeout), which is far from ideal considering one can just fix the typing in the library.

I ran into this while using Python 3.8.10, but the exact same explanation above applies to the current master branch (and the version I'm using of course), as shown by the snippets below.

3.8 branch __init__ parameter:

timeout: Union[object, ClientTimeout] = sentinel,

3.8 branch self._timeout assignment:

aiohttp/aiohttp/client.py

Lines 261 to 290 in 6243204

if timeout is sentinel:
self._timeout = DEFAULT_TIMEOUT
if read_timeout is not sentinel:
warnings.warn(
"read_timeout is deprecated, " "use timeout argument instead",
DeprecationWarning,
stacklevel=2,
)
self._timeout = attr.evolve(self._timeout, total=read_timeout)
if conn_timeout is not None:
self._timeout = attr.evolve(self._timeout, connect=conn_timeout)
warnings.warn(
"conn_timeout is deprecated, " "use timeout argument instead",
DeprecationWarning,
stacklevel=2,
)
else:
self._timeout = timeout # type: ignore[assignment]
if read_timeout is not sentinel:
raise ValueError(
"read_timeout and timeout parameters "
"conflict, please setup "
"timeout.read"
)
if conn_timeout is not None:
raise ValueError(
"conn_timeout and timeout parameters "
"conflict, please setup "
"timeout.connect"
)

Note the # type: ignore comment on L278 there - it's because the timeout is sentinel check does not narrow down the timeout type. The correct way to go about this would be to use a cast there instead of ignoring the issue like that.

3.8 branch timeout attribute declaration:

aiohttp/aiohttp/client.py

Lines 1029 to 1032 in 6243204

@property
def timeout(self) -> Union[object, ClientTimeout]:
"""Timeout for the session."""
return self._timeout

Master branch __init__ parameter:

timeout: Union[_SENTINEL, ClientTimeout, None] = sentinel,

Master branch self._timeout assignment:

aiohttp/aiohttp/client.py

Lines 260 to 263 in 52fa599

if timeout is sentinel or timeout is None:
self._timeout = DEFAULT_TIMEOUT
else:
self._timeout = timeout

Due to a different handling of the sentinel value via an Enum member, no cast is needed here.

Master branch timeout attribute declaration:

aiohttp/aiohttp/client.py

Lines 1008 to 1011 in 52fa599

@property
def timeout(self) -> Union[object, ClientTimeout]:
"""Timeout for the session."""
return self._timeout

The attribute type is still over-inclusive here though.

The solution would be quite simple:

    @property
    def timeout(self) -> ClientTimeout:
        """Timeout for the session."""
        return self._timeout

Please let me know if you'd welcome a PR for this. I'd like to get this backported back to 3.8 (that I'm using) if possible, but if not, just fixing it in the master branch so that it's correct going forward would be good enough for me.

To Reproduce

Utilize some kind of a type checker like MyPy.

import asyncio
import aiohttp

async def main:
    session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
    # read back the total time attribute
    total_time = session.timeout.total  # "object" type of "Union[object, ClientTimeout]" has no attribute "total"
    print(total_time)

asyncio.run(main())

Expected behavior

The attribute having only the aiohttp.ClientTimeout type and not requiring cast usage when accessing the attribute during library usage in user code.

Logs/tracebacks

Not applicable

Python Version

Python 3.8.10

aiohttp Version

Version: 3.8.1

multidict Version

Version: 6.0.2

yarl Version

Version: 1.7.2

OS

Windows

Related component

Client

Additional context

Related issues and PRs:

#4191
#4193

Code of Conduct

  • I agree to follow the aio-libs Code of Conduct
@DevilXD DevilXD added the bug label Sep 17, 2022
@Dreamsorcerer
Copy link
Member

I'll certainly take a look over it, if you create a PR.

The correct way to go about this would be to use a cast there instead of ignoring the issue like that.

A cast() is ignoring the issue. We try to avoid both casts and ignores where possible. In many cases, if you are working around a bug/limitation of mypy, the ignore option is safer (if fixed later, you are more likely to get an unused ignore error than a redundant cast error, plus it's easier to pick out these discrepancies when looking at the code base).

@DevilXD
Copy link
Contributor Author

DevilXD commented Sep 19, 2022

A cast() is ignoring the issue.

A cast is just a "manual" way of performing type narrowing. Yes, it kinda is ignoring the issue, but it can be made more specific than an ignore comment. Whatever type you cast the variable to, has to be a subtype of the type the variable already has, otherwise MyPy returns an error (as far as I can remember). I think this is better than a type: ignore[assignment] comment, because a cast wouldn't let you assign an int by accident there for example, while a comment like that leaves no complaints. That's just my take on it though.

I'll try to prepare a PR later today then, or later this week if I won't be able to do so today.

@Dreamsorcerer
Copy link
Member

A cast is just a "manual" way of performing type narrowing. Whatever type you cast the variable to, has to be a subtype of the type the variable already has.

No, it literally just replaces the type, thus throwing away type safety:

from typing import cast
foo = "apple"
reveal_type(cast(int, foo))
> mypy --strict test.py
test.py:3: note: Revealed type is "builtins.int"
Success: no issues found in 1 source file

There are certainly times where casting makes more sense, but both are inherently unsafe and are ignoring the actual issue.

@DevilXD
Copy link
Contributor Author

DevilXD commented Sep 19, 2022

That's weird, I remember getting an error before when trying to straight up "replace" a type like you just shown. Strange.

@DevilXD
Copy link
Contributor Author

DevilXD commented Sep 20, 2022

I have created #6923 to address this issue.

patchback bot pushed a commit that referenced this issue Sep 20, 2022
This is a follow-up PR to #6917, which fixes the overly inclusive typing
of `ClientSession.timeout` attribute, which can never actually end up as
an `object` instance. This leftover probably exists from the time when
there was the `_sentinel` value being assigned to the attribute (see
"3.8 branch `self._timeout` assignment" from the linked issue), hence
why it ended up this way.

For untyped code, there's absolutely no change — the `self._timeout`
attribute was never actually assigned the sentinel value to. Most of the
explanation already exists in #6917, here's the test code snippet from
there again:

```py
import asyncio
import aiohttp

async def main:
    session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
    # read back the total time attribute
    total_time = session.timeout.total  # "object" type of "Union[object, ClientTimeout]" has no attribute "total"
    print(total_time)

asyncio.run(main())
```

For typed code, the current solution would be to `assert
isinstance(session.timeout, aiohttp.ClientTimeout)` everywhere the
attribute is being accessed. This PR removes this "unnecessary
necessity".

Co-authored-by: Sviatoslav Sydorenko <wk@sydorenko.org.ua>

Fixes #6917
PR #6923

(cherry picked from commit 78fa040)
patchback bot pushed a commit that referenced this issue Sep 20, 2022
This is a follow-up PR to #6917, which fixes the overly inclusive typing
of `ClientSession.timeout` attribute, which can never actually end up as
an `object` instance. This leftover probably exists from the time when
there was the `_sentinel` value being assigned to the attribute (see
"3.8 branch `self._timeout` assignment" from the linked issue), hence
why it ended up this way.

For untyped code, there's absolutely no change — the `self._timeout`
attribute was never actually assigned the sentinel value to. Most of the
explanation already exists in #6917, here's the test code snippet from
there again:

```py
import asyncio
import aiohttp

async def main:
    session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
    # read back the total time attribute
    total_time = session.timeout.total  # "object" type of "Union[object, ClientTimeout]" has no attribute "total"
    print(total_time)

asyncio.run(main())
```

For typed code, the current solution would be to `assert
isinstance(session.timeout, aiohttp.ClientTimeout)` everywhere the
attribute is being accessed. This PR removes this "unnecessary
necessity".

Co-authored-by: Sviatoslav Sydorenko <wk@sydorenko.org.ua>

Fixes #6917
PR #6923

(cherry picked from commit 78fa040)
webknjaz pushed a commit that referenced this issue Sep 20, 2022
…om the `ClientSession.timeout` type (#6924)

**This is a backport of PR #6923 as merged into master
(78fa040).**

<!-- Thank you for your contribution! -->

## What do these changes do?

<!-- Please give a short brief about these changes. -->

This is a follow-up PR to #6917, which fixes the overly inclusive typing
of `ClientSession.timeout` attribute, which can never actually end up as
an `object` instance. This leftover probably exists from the time when
there was the `_sentinel` value being assigned to the attribute (see
"3.8 branch `self._timeout` assignment" from the linked issue), hence
why it ended up this way.

## Are there changes in behavior for the user?

<!-- Outline any notable behaviour for the end users. -->

For untyped code, there's absolutely no change - the `self._timeout`
attribute was never actually assigned the sentinel value to. Most of the
explanation already exists in #6917, here's the test code snippet from
there again:

```py
import asyncio
import aiohttp

async def main:
    session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
    # read back the total time attribute
    total_time = session.timeout.total  # "object" type of "Union[object, ClientTimeout]" has no attribute "total"
    print(total_time)

asyncio.run(main())
```

For typed code, the current solution would be to `assert
isinstance(session.timeout, aiohttp.ClientTimeout)` everywhere the
attribute is being accessed. This PR removes this "unnecessary
necessity".

## Related issue number

<!-- Are there any issues opened that will be resolved by merging this
change? -->

#6917

## Checklist

- [x] I think the code is well written
- [ ] Unit tests for the changes exist
- [x] Documentation reflects the changes (documentation never mentioned
an `object` instance being accessible from there even)
- [ ] If you provide code modification, please add yourself to
`CONTRIBUTORS.txt`
  * The format is &lt;Name&gt; &lt;Surname&gt;.
  * Please keep alphabetical order, the file is sorted by names.
- [ ] Add a new news fragment into the `CHANGES` folder
  * name it `<issue_id>.<type>` for example (588.bugfix)
* if you don't have an `issue_id` change it to the pr id after creating
the pr
  * ensure type is one of the following:
    * `.feature`: Signifying a new feature.
    * `.bugfix`: Signifying a bug fix.
    * `.doc`: Signifying a documentation improvement.
    * `.removal`: Signifying a deprecation or removal of public API.
* `.misc`: A ticket has been closed, but it is not of interest to users.
* Make sure to use full sentences with correct case and punctuation, for
example: "Fix issue with non-ascii contents in doctest text files."

Co-authored-by: DevilXD <DevilXD@users.noreply.github.com>
webknjaz pushed a commit that referenced this issue Sep 20, 2022
…om the `ClientSession.timeout` type (#6925)

**This is a backport of PR #6923 as merged into master
(78fa040).**

<!-- Thank you for your contribution! -->

## What do these changes do?

<!-- Please give a short brief about these changes. -->

This is a follow-up PR to #6917, which fixes the overly inclusive typing
of `ClientSession.timeout` attribute, which can never actually end up as
an `object` instance. This leftover probably exists from the time when
there was the `_sentinel` value being assigned to the attribute (see
"3.8 branch `self._timeout` assignment" from the linked issue), hence
why it ended up this way.

## Are there changes in behavior for the user?

<!-- Outline any notable behaviour for the end users. -->

For untyped code, there's absolutely no change - the `self._timeout`
attribute was never actually assigned the sentinel value to. Most of the
explanation already exists in #6917, here's the test code snippet from
there again:

```py
import asyncio
import aiohttp

async def main:
    session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
    # read back the total time attribute
    total_time = session.timeout.total  # "object" type of "Union[object, ClientTimeout]" has no attribute "total"
    print(total_time)

asyncio.run(main())
```

For typed code, the current solution would be to `assert
isinstance(session.timeout, aiohttp.ClientTimeout)` everywhere the
attribute is being accessed. This PR removes this "unnecessary
necessity".

## Related issue number

<!-- Are there any issues opened that will be resolved by merging this
change? -->

#6917

## Checklist

- [x] I think the code is well written
- [ ] Unit tests for the changes exist
- [x] Documentation reflects the changes (documentation never mentioned
an `object` instance being accessible from there even)
- [ ] If you provide code modification, please add yourself to
`CONTRIBUTORS.txt`
  * The format is &lt;Name&gt; &lt;Surname&gt;.
  * Please keep alphabetical order, the file is sorted by names.
- [ ] Add a new news fragment into the `CHANGES` folder
  * name it `<issue_id>.<type>` for example (588.bugfix)
* if you don't have an `issue_id` change it to the pr id after creating
the pr
  * ensure type is one of the following:
    * `.feature`: Signifying a new feature.
    * `.bugfix`: Signifying a bug fix.
    * `.doc`: Signifying a documentation improvement.
    * `.removal`: Signifying a deprecation or removal of public API.
* `.misc`: A ticket has been closed, but it is not of interest to users.
* Make sure to use full sentences with correct case and punctuation, for
example: "Fix issue with non-ascii contents in doctest text files."

Co-authored-by: DevilXD <DevilXD@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants