Issue 1600: Forbid await in for loop#3330
Conversation
| async def foo(): | ||
| for _ in range(1): | ||
| await some() | ||
| """ |
There was a problem hiding this comment.
Don't forget to test:
- list comp
[await x for x in ...] - set comp
- dict comp
- gen exprs
|
|
||
|
|
||
| @final | ||
| class AwaitInLoopViolation(ASTViolation): |
There was a problem hiding this comment.
This is not a consistency violation, this is a best practice.
|
|
||
| code_that_breaks = """ | ||
| async def foo(): | ||
| for _ in range(1): |
There was a problem hiding this comment.
Please, check that await in async for works.
|
Added comprehensions checking, checked for |
| await test_function() | ||
|
|
||
|
|
||
| async def test_await_in_list_comp(): | ||
| list_comprehension = [await test_function() for _ in range(10)] # noqa: WPS476 | ||
| set_comprehension = {await test_function() for _ in range(10)} # noqa: WPS476 | ||
| dict_comprehension = {some_hashable: await test_function() for some_hashable in range(10)} # noqa: WPS476 | ||
| generator_comprehension = (await test_function() for _ in range(10)) # noqa: WPS476 |
There was a problem hiding this comment.
| await test_function() | |
| async def test_await_in_list_comp(): | |
| list_comprehension = [await test_function() for _ in range(10)] # noqa: WPS476 | |
| set_comprehension = {await test_function() for _ in range(10)} # noqa: WPS476 | |
| dict_comprehension = {some_hashable: await test_function() for some_hashable in range(10)} # noqa: WPS476 | |
| generator_comprehension = (await test_function() for _ in range(10)) # noqa: WPS476 |
Just one error is fine.
| 'WPS473': 0, | ||
| 'WPS474': 1, | ||
| 'WPS475': 1, | ||
| 'WPS476': 5, |
There was a problem hiding this comment.
| 'WPS476': 5, | |
| 'WPS476': 1, |
|
|
||
| generator_comprehension = """ | ||
| async def foo(): | ||
| result = (await some() for i in range(10)) |
There was a problem hiding this comment.
Don't forget about async comprehensions :)
They should allow await
There was a problem hiding this comment.
I'm did not work with async Python as much as I wish 😢
Can you please correct me - async comprehensions it is something like this [await some_two() async for _ in some_one()]?
| There is a better way to control repeated coroutines in ``for`` loops. | ||
|
|
||
| Solution: | ||
| Using :function:`asyncio.gather` or :class:`asyncio.TaskGroup` |
There was a problem hiding this comment.
| Using :function:`asyncio.gather` or :class:`asyncio.TaskGroup` | |
| Use :function:`asyncio.gather`, | |
| :function:`asyncio.wait`, | |
| or :class:`asyncio.TaskGroup` |
| urls = [ | ||
| "https://wemake-python-styleguide.readthedocs.io/", | ||
| "https://flake8.pycqa.org/", | ||
| "https://docs.astral.sh/ruff/", | ||
| ] | ||
|
|
There was a problem hiding this comment.
| urls = [ | |
| "https://wemake-python-styleguide.readthedocs.io/", | |
| "https://flake8.pycqa.org/", | |
| "https://docs.astral.sh/ruff/", | |
| ] |
| async def request(): | ||
| tasks = [] | ||
| for url in urls: | ||
| tasks.append(parse_content(url)) | ||
| parsed_content = await asyncio.gather(*tasks) |
There was a problem hiding this comment.
| async def request(): | |
| tasks = [] | |
| for url in urls: | |
| tasks.append(parse_content(url)) | |
| parsed_content = await asyncio.gather(*tasks) | |
| async def request(): | |
| tasks = [parse_content(url) for url in urls] | |
| parsed_content = await asyncio.gather(*tasks) |
| if isinstance(node, ast.While | ast.AsyncFor): | ||
| return | ||
|
|
||
| for subnode in ast.walk(node): |
There was a problem hiding this comment.
I think that we should do the other way, we need to search for await and then find For in its parents. See nodes.py for a helper function.
There was a problem hiding this comment.
Not sure that I understand you correctly.
In this method we getting nodes that could be AnyLoop | AnyComprehension, so we already in the loop node. Why we need walk through parents? If we found await in child nodes it already triggered violation.
Sorry if it is dumb question I have't work with ast package until your project 🙃
Any way I rewrote method in last commits with walk through parents logic.
|
Well, yes, wps really make me suffer 😄 |
sobolevn
left a comment
There was a problem hiding this comment.
Thank you! We are almost there.
| ): | ||
| return | ||
|
|
||
| bad_loops = ( |
There was a problem hiding this comment.
we use class-level constants for tuples like this. proper name would be _forbidden_await_loops
| | ast.ListComp | ||
| | ast.SetComp | ||
| ) | ||
| await_sub_nodes = walk.get_subnodes_by_type(node, ast.Await) |
There was a problem hiding this comment.
Basically, that's why I said that it would be easier to visit await nodes first and then find forbidden parents from it. See
wemake-python-styleguide/wemake_python_styleguide/logic/walk.py
Lines 24 to 35 in 2c4b96e
Basically, you would just have an await node, pass it into container = get_closest_parent(node, self._forbidden_await_loops) and then check that the container is not an async comprehension. That's it. Three lines of code.
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## master #3330 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 357 358 +1
Lines 11708 11761 +53
Branches 801 803 +2
=========================================
+ Hits 11708 11761 +53 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…e-python-styleguide into forbid-await-in-for-loop
|
I'm change method as you said this is really decreese rows number 💯 Also I added test for multiply comprehensions in one including |
| if not isinstance(evaled, ast.Name) and bool(evaled): | ||
| self.add_violation(InfiniteWhileLoopViolation(node)) | ||
|
|
||
| def _check_await_inside_loop( |
There was a problem hiding this comment.
It would be easier to use visit_Await method and the call _check_await_inside_loop with ast.Await node :)
There was a problem hiding this comment.
Got it! I should put it in WrongLoopVisitor or somewhere else?
There was a problem hiding this comment.
if it fits the complexity rules - yes!
There was a problem hiding this comment.
It unfortunately did not fit D:
Moved it to new class.
| if node_parent is not None: | ||
| if isinstance(node_parent, AnyComprehension) and all( | ||
| comprehension.is_async | ||
| for comprehension in node_parent.generators | ||
| ): | ||
| return |
There was a problem hiding this comment.
| if node_parent is not None: | |
| if isinstance(node_parent, AnyComprehension) and all( | |
| comprehension.is_async | |
| for comprehension in node_parent.generators | |
| ): | |
| return | |
| if ( | |
| isinstance(node_parent, AnyComprehension) | |
| and all( | |
| comprehension.is_async | |
| for comprehension in node_parent.generators | |
| ): | |
| # async comprehensions are allowed to use `await` | |
| return |
There was a problem hiding this comment.
You want me to add this comment?
# async comprehensions are allowed to use await
There was a problem hiding this comment.
not only a comment, but change how ifs are nested
There was a problem hiding this comment.
change how ifs are nested
In this case tests are falling 😢
This will have to do some changes again, if it's ok I'll do it
|
Thank you! I have a lot of fun and learning new things doing this task. Hope I can help project solving another issues and bugs, if you don't mind 🤓 |
|
Sure, please, go ahead! |
I have made things!
Checklist
CHANGELOG.mdRelated issues
Closes #1600
Hi! I had a lot of pleasure while working on this issue.
I added new ASTViolation for forbid await in for loop, but not sure about docstring detail. If it need to content more informative, I'll add it.
About documentation I guess it fills automatically using docstrings in ASTViolation subclasses, so I mark as done third point in the checklist