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

[pylint] redefined-loop-name (W2901) #3022

Merged
merged 12 commits into from Feb 22, 2023
Merged

[pylint] redefined-loop-name (W2901) #3022

merged 12 commits into from Feb 22, 2023

Conversation

matthewlloyd
Copy link
Contributor

Slightly broadens W2901 to cover with statements too.

Closes #2972.

Slightly broadens W2901 to cover `with` statements too.

Closes #2972.
@sbrugman
Copy link
Contributor

sbrugman commented Feb 19, 2023

Perhaps update the pylint issue with your findings, quite some nice insights (since it will be closed when merging this)

@matthewlloyd
Copy link
Contributor Author

matthewlloyd commented Feb 19, 2023

Perhaps update the pylint issue with your findings, quite some nice insights (since it will be closed when merging this)

Done, and opened a new issue with the potential PyCharm-derived inspections: #3040

.map(|range| checker.locator.slice(range))
.collect();

for outer_name in &outer_names {
Copy link
Member

Choose a reason for hiding this comment

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

(Just out of interest, how closely does this match Pylint's own implementation? Did you look at the Pylint source, or is it implemented from scratch?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was implemented from scratch, but after I learned Pylint has it too, I did look at the Pylint source later to compare. Pylint has a slightly more efficient algorithm, using a single recursion over the AST, keeping track of defined names using a stack, and pushing and popping the names on entering and leaving each for loop. This could probably be done with a deeper integration into ast.rs.

Examples:

```
test.py:2:9: PLW2901 Outer for loop variable `i` overwritten by for loop target with same name
test.py:6:9: PLW2901 Outer with statement variable `i` overwritten by for loop target with same name
test.py:12:5: PLW2901 Outer for loop variable `i` overwritten by assignment target with same name
```
@charliermarsh
Copy link
Member

So I just ran this over Zulip as a smoke test, and it looks like the rule is currently flagging usages like this:

for email in verified_emails:
    existing_account = common_get_active_user(email, realm, {})
    if existing_account is not None:
        existing_account_emails.append(email)
        avatars[email] = avatar_url(existing_account)

Also attribute assignments, like scope.fingerprint = ["worker-timeout", self.queue_name].

@matthewlloyd
Copy link
Contributor Author

matthewlloyd commented Feb 20, 2023

Oh gosh, great point! I will know to run on some larger codebases when testing in future. Agreed, we need to avoid the lexer and traverse the AST of the target. It's really interesting learning new intricacies of Python syntax. I had never considered these, for example:

>>> d = {}
>>> for d["i"] in range(2):
...     print(d["i"])
...
0
1
>>> a = [None]
>>> for a[0] in range(2):
...     print(a[0])
...
0
1
>>> i = 0
>>> for a[i] in range(2):
...     print(a[i])
...     i = 0  # no error
...
0
1

I'll try to think through all these edge cases, add tests, and get this fixed later today or tomorrow.

@charliermarsh
Copy link
Member

No rush! Sorry for not noticing this earlier :) Let me know if you have questions as you work through it.

@matthewlloyd
Copy link
Contributor Author

Ran this new version on Zulip - still quite a few "false positives", only a few of which are IMO bad code smells. The most noisy false positives tend to be of the form:

for line in file:
    line = line.strip()  # PLW2901

which are flagged by Pylint's redefined-loop-name extension and by my code here, but which in practice are of course commonplace and harmless, and where the developer has to jump through an annoying hoop to avoid the warning e.g. using a new stripped_line name. I'm in two minds about whether these should always be flagged, or whether reuse by direct assignment should be optional or under a different code.

On the other extreme we have this (from queue_processors.py) which does seem worth flagging, since another developer may not remember Python's unique scope rules and might add code that uses the outer scope variable without realizing it has been overwritten:

        with configure_scope() as scope:
            scope.set_context(
              # ...
            )
            if isinstance(exception, WorkerTimeoutError):
                with sentry_sdk.push_scope() as scope:  # PLW2901
                    scope.fingerprint = ["worker-timeout", self.queue_name]
                    logging.exception(exception, stack_info=True)
            else:
                logging.exception(
                    "Problem handling data on queue %s", self.queue_name, stack_info=True
                )

Similarly we have cases like this one (from test_openapi.py) where the code works and is correct, but again another developer may add code to the outer loop expecting element to still be from diff and get tripped up:

        for element in diff:
            vname = element[0]
            for element in openapi_params:  # PLW2901
                if element[0] == vname:
                    opvtype = element[1]
                    break
            for element in function_params:  # PLW2901
                if element[0] == vname:
                    fdvtype = element[1]
                    break

Finally we have an example in between (from rocketchat.py), where the name reuse is gratuitous, and while not an actual type error, arguably a notional error in regards to the "semantic type" of reaction_dict (by which I mean what keys are present and what the associated values represent). A developer adding code to the outer loop seems unlikely get tripped up here, but I'd say it's still worth flagging since finding a new name improves readability:

    for reaction_dict in reactions:
        emoji_name = reaction_dict["name"]
        user_id = reaction_dict["user_id"]
        # Check in realm emoji
        if emoji_name in realmemoji:
          # ...

        reaction_id = NEXT_ID("reaction")
        reaction = Reaction(
          # ...
        )

        reaction_dict = model_to_dict(reaction, exclude=["message", "user_profile"])  # PLW2901
        reaction_dict["message"] = message_id
        reaction_dict["user_profile"] = user_id
        total_reactions.append(reaction_dict)

@charliermarsh
Copy link
Member

Awesome! Will review this shortly.

@charliermarsh
Copy link
Member

I agree with the analysis in your comment. One other lens to apply here: where does this version deviate from Pylint, and where is it the same? (The less it deviates, the less strongly I feel about things that might feel excessive in practice.) (I know you mention that Pylint flags the first example -- wondering if it's the case for the others too.)

@matthewlloyd
Copy link
Contributor Author

I agree with the analysis in your comment. One other lens to apply here: where does this version deviate from Pylint, and where is it the same? (The less it deviates, the less strongly I feel about things that might feel excessive in practice.) (I know you mention that Pylint flags the first example -- wondering if it's the case for the others too.)

Good question. As the code currently stands, for the Zulip codebase, this version exactly matches Pylint with the only difference being this version works not just for for but also for with statements.

This version finds 35 (34 for + 1 with):

zulip/manage.py:90:21: PLW2901 Outer for loop variable `app` overwritten by inner assignment target with same name
zulip/manage.py:92:21: PLW2901 Outer for loop variable `app` overwritten by inner assignment target with same name
zulip/scripts/lib/check_rabbitmq_queue.py:142:9: PLW2901 Outer for loop variable `line` overwritten by inner assignment target with same name
zulip/scripts/lib/setup_venv.py:110:13: PLW2901 Outer for loop variable `package` overwritten by inner assignment target with same name
zulip/scripts/lib/setup_venv.py:114:17: PLW2901 Outer for loop variable `package` overwritten by inner assignment target with same name
zulip/scripts/lib/setup_venv.py:116:9: PLW2901 Outer for loop variable `package` overwritten by inner assignment target with same name
zulip/scripts/lib/zulip_tools.py:429:13: PLW2901 Outer for loop variable `line` overwritten by inner assignment target with same name
zulip/tools/lib/capitalization.py:235:9: PLW2901 Outer for loop variable `text` overwritten by inner assignment target with same name
zulip/tools/lib/html_branches.py:46:17: PLW2901 Outer for loop variable `lst` overwritten by inner assignment target with same name
zulip/tools/lib/test_script.py:99:9: PLW2901 Outer for loop variable `file` overwritten by inner assignment target with same name
zulip/zerver/data_import/rocketchat.py:363:9: PLW2901 Outer for loop variable `reaction_dict` overwritten by inner assignment target with same name
zulip/zerver/lib/ccache.py:112:13: PLW2901 Outer for loop variable `tlv` overwritten by inner assignment target with same name
zulip/zerver/lib/events.py:1084:25: PLW2901 Outer for loop variable `sub` overwritten by inner assignment target with same name
zulip/zerver/lib/markdown/__init__.py:1273:21: PLW2901 Outer for loop variable `found_url` overwritten by inner assignment target with same name
zulip/zerver/lib/narrow.py:643:17: PLW2901 Outer for loop variable `term` overwritten by inner assignment target with same name
zulip/zerver/lib/narrow.py:644:17: PLW2901 Outer for loop variable `term` overwritten by inner assignment target with same name
zulip/zerver/lib/test_runner.py:335:21: PLW2901 Outer for loop variable `test_name` overwritten by inner assignment target with same name
zulip/zerver/lib/test_runner.py:348:29: PLW2901 Outer for loop variable `test_name` overwritten by inner assignment target with same name
zulip/zerver/management/commands/enqueue_file.py:17:9: PLW2901 Outer for loop variable `line` overwritten by inner assignment target with same name
zulip/zerver/management/commands/import.py:88:13: PLW2901 Outer for loop variable `path` overwritten by inner assignment target with same name
zulip/zerver/management/commands/makemessages.py:168:17: PLW2901 Outer for loop variable `match` overwritten by inner assignment target with same name
zulip/zerver/management/commands/makemessages.py:169:17: PLW2901 Outer for loop variable `match` overwritten by inner assignment target with same name
zulip/zerver/migrations/0102_convert_muted_topic.py:32:13: PLW2901 Outer for loop variable `stream_name` overwritten by inner assignment target with same name
zulip/zerver/migrations/0102_convert_muted_topic.py:53:13: PLW2901 Outer for loop variable `stream_name` overwritten by inner assignment target with same name
zulip/zerver/migrations/0423_fix_email_gateway_attachment_owner.py:113:13: PLW2901 Outer for loop variable `attachment` overwritten by inner assignment target with same name
zulip/zerver/openapi/openapi.py:468:17: PLW2901 Outer for loop variable `error` overwritten by inner assignment target with same name
zulip/zerver/openapi/test_curl_examples.py:86:17: PLW2901 Outer for loop variable `curl_command_text` overwritten by inner assignment target with same name
zulip/zerver/tests/test_decorators.py:2036:17: PLW2901 Outer for loop variable `line` overwritten by inner assignment target with same name
zulip/zerver/tests/test_openapi.py:376:17: PLW2901 Outer for loop variable `element` overwritten by inner for loop target with same name
zulip/zerver/tests/test_openapi.py:380:17: PLW2901 Outer for loop variable `element` overwritten by inner for loop target with same name
zulip/zerver/tests/test_openapi.py:439:13: PLW2901 Outer for loop variable `defval` overwritten by inner assignment target with same name
zulip/zerver/views/invite.py:104:13: PLW2901 Outer for loop variable `email` overwritten by inner assignment target with same name
zulip/zerver/webhooks/bitbucket2/view.py:437:13: PLW2901 Outer for loop variable `change` overwritten by inner assignment target with same name
zulip/zerver/worker/queue_processors.py:378:49: PLW2901 Outer with statement variable `scope` overwritten by inner with statement target with same name

Pylint finds 34 (34 for):

zulip/manage.py:90:20: W2901: Redefining 'app' from loop (line 88) (redefined-loop-name)
zulip/manage.py:92:20: W2901: Redefining 'app' from loop (line 88) (redefined-loop-name)
zulip/scripts/lib/check_rabbitmq_queue.py:142:8: W2901: Redefining 'line' from loop (line 141) (redefined-loop-name)
zulip/scripts/lib/setup_venv.py:110:12: W2901: Redefining 'package' from loop (line 104) (redefined-loop-name)
zulip/scripts/lib/setup_venv.py:114:16: W2901: Redefining 'package' from loop (line 104) (redefined-loop-name)
zulip/scripts/lib/setup_venv.py:116:8: W2901: Redefining 'package' from loop (line 104) (redefined-loop-name)
zulip/scripts/lib/zulip_tools.py:429:12: W2901: Redefining 'line' from loop (line 428) (redefined-loop-name)
zulip/tools/lib/capitalization.py:235:8: W2901: Redefining 'text' from loop (line 234) (redefined-loop-name)
zulip/tools/lib/html_branches.py:46:16: W2901: Redefining 'lst' from loop (line 42) (redefined-loop-name)
zulip/tools/lib/test_script.py:99:8: W2901: Redefining 'file' from loop (line 98) (redefined-loop-name)
zulip/zerver/data_import/rocketchat.py:363:8: W2901: Redefining 'reaction_dict' from loop (line 341) (redefined-loop-name)
zulip/zerver/lib/ccache.py:112:12: W2901: Redefining 'tlv' from loop (line 106) (redefined-loop-name)
zulip/zerver/lib/events.py:1084:24: W2901: Redefining 'sub' from loop (line 1081) (redefined-loop-name)
zulip/zerver/lib/markdown/__init__.py:1273:20: W2901: Redefining 'found_url' from loop (line 1244) (redefined-loop-name)
zulip/zerver/lib/narrow.py:643:16: W2901: Redefining 'term' from loop (line 641) (redefined-loop-name)
zulip/zerver/lib/narrow.py:644:16: W2901: Redefining 'term' from loop (line 641) (redefined-loop-name)
zulip/zerver/lib/test_runner.py:335:20: W2901: Redefining 'test_name' from loop (line 332) (redefined-loop-name)
zulip/zerver/lib/test_runner.py:348:28: W2901: Redefining 'test_name' from loop (line 332) (redefined-loop-name)
zulip/zerver/management/commands/enqueue_file.py:17:8: W2901: Redefining 'line' from loop (line 16) (redefined-loop-name)
zulip/zerver/management/commands/import.py:88:12: W2901: Redefining 'path' from loop (line 87) (redefined-loop-name)
zulip/zerver/management/commands/makemessages.py:168:16: W2901: Redefining 'match' from loop (line 167) (redefined-loop-name)
zulip/zerver/management/commands/makemessages.py:169:16: W2901: Redefining 'match' from loop (line 167) (redefined-loop-name)
zulip/zerver/migrations/0102_convert_muted_topic.py:32:12: W2901: Redefining 'stream_name' from loop (line 31) (redefined-loop-name)
zulip/zerver/migrations/0102_convert_muted_topic.py:53:12: W2901: Redefining 'stream_name' from loop (line 52) (redefined-loop-name)
zulip/zerver/migrations/0423_fix_email_gateway_attachment_owner.py:113:12: W2901: Redefining 'attachment' from loop (line 42) (redefined-loop-name)
zulip/zerver/openapi/openapi.py:468:16: W2901: Redefining 'error' from loop (line 447) (redefined-loop-name)
zulip/zerver/openapi/test_curl_examples.py:86:16: W2901: Redefining 'curl_command_text' from loop (line 85) (redefined-loop-name)
zulip/zerver/tests/test_decorators.py:2036:16: W2901: Redefining 'line' from loop (line 2035) (redefined-loop-name)
zulip/zerver/tests/test_openapi.py:376:12: W2901: Redefining 'element' from loop (line 374) (redefined-loop-name)
zulip/zerver/tests/test_openapi.py:380:12: W2901: Redefining 'element' from loop (line 374) (redefined-loop-name)
zulip/zerver/tests/test_openapi.py:439:12: W2901: Redefining 'defval' from loop (line 438) (redefined-loop-name)
zulip/zerver/views/invite.py:104:12: W2901: Redefining 'email' from loop (line 101) (redefined-loop-name)
zulip/zerver/webhooks/bitbucket2/view.py:437:12: W2901: Redefining 'change' from loop (line 433) (redefined-loop-name)

@charliermarsh
Copy link
Member

Ok cool, I'm happy with that level of parity then. I'll move on to reviewing the actual code.

@matthewlloyd
Copy link
Contributor Author

This raises a couple of questions in my mind learning Ruff's "best practices":

  • Are rules in different sets allowed to overlap?
  • Do reimplementations of rules from other packages need to exactly match them in semantics?

If rules can overlap, perhaps one way out of the dilemma would be to have two rules which share the same codebase:

  • PLW2901, which exactly matches Pylint:
    • for loops only, no with
    • reuse as a for loop variable is disallowed (signal)
    • reuse in assignment is disallowed (noisy)
  • RUF???, which offers a more useful and practical compromise:
    • for loops and with statements
    • reuse as a for loop or with variable is disallowed (enhanced signal)
    • reuse in assignment is allowed (cut the noise, but possibly with an option for pedants to disallow these too)

@charliermarsh
Copy link
Member

Good questions!

Are rules in different sets allowed to overlap?

It's not strictly forbidden but we try to avoid this. In fact over time we've actually removed some rules that turned out to be duplicates.

Do reimplementations of rules from other packages need to exactly match them in semantics?

No, but we try to be intentional about deviations (so the upstream package is seen as the reference implementation, and any deviations should be intentional).

We often deviate in the case of clear bugs or Pareto improvements (strictly fewer false positives or false negatives).

There's also some precedent for deviating with Pylint rules specifically. For example, we changed the defaults in the magic-value checks to avoid flagging comparisons to string and bytes, which felt noisy. (That's another strategy: deviate by way of configuration.)

fn assignment_targets_from_expr<'a, U>(
expr: &'a Expr<U>,
dummy_variable_rgx: &'a HashableRegex,
) -> Box<dyn Iterator<Item = &'a Expr<U>> + 'a> {
Copy link
Member

Choose a reason for hiding this comment

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

Another approach would be to pass in a mutable vector, and have the various arms push to it. Would that work? Perhaps not as nice conceptually, but a bit simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Certainly could, but may be less efficient overall because it will require an additional temporary vector for each call. This is because the outputs of these methods can't be pushed directly to the InnerForWithAssignTargetsVisitor.assignment_targets vector as they first need to be paired with the appropriate BindingKind variant by the caller, which depends on the call path; so the intermediate Vec<Expr> would be immediately discarded each time.

On the other hand, I suppose the Boxes also necessitate a heap allocation, albeit one that has a fixed size and doesn't get expanded...

An alternative would be to pass in both a &mut Vec, and also the BindingKind, to these methods, and have them push ExprWithBindingKind into the vector.

@charliermarsh charliermarsh enabled auto-merge (squash) February 22, 2023 03:20
@charliermarsh charliermarsh added the rule Implementing or modifying a lint rule label Feb 22, 2023
@charliermarsh charliermarsh merged commit 97338e4 into astral-sh:main Feb 22, 2023
@matthewlloyd
Copy link
Contributor Author

Thanks again for the thorough and educational review and the quick merge.

@charliermarsh
Copy link
Member

Thank you for all the work! Have been really impressed by your thoroughness and attention to detail, not to mention the quality of your code.

@matthewlloyd matthewlloyd deleted the pylint-redefined-loop-name branch February 22, 2023 03:44
bruxisma pushed a commit to ixm-one/pytest-cmake-presets that referenced this pull request Feb 22, 2023
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [ruff](https://togithub.com/charliermarsh/ruff) | `^0.0.251` ->
`^0.0.252` |
[![age](https://badges.renovateapi.com/packages/pypi/ruff/0.0.252/age-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://badges.renovateapi.com/packages/pypi/ruff/0.0.252/adoption-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://badges.renovateapi.com/packages/pypi/ruff/0.0.252/compatibility-slim/0.0.251)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://badges.renovateapi.com/packages/pypi/ruff/0.0.252/confidence-slim/0.0.251)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>charliermarsh/ruff</summary>

###
[`v0.0.252`](https://togithub.com/charliermarsh/ruff/releases/tag/v0.0.252)

[Compare
Source](https://togithub.com/charliermarsh/ruff/compare/v0.0.251...v0.0.252)

<!-- Release notes generated using configuration in .github/release.yml
at main -->

#### What's Changed

##### Rules

- \[`pylint`] `redefined-loop-name` (`W2901`) by
[@&#8203;matthewlloyd](https://togithub.com/matthewlloyd) in
[astral-sh/ruff#3022
- \[`pylint`] ` logging-too-many-args ` (`E1205`) by
[@&#8203;md384](https://togithub.com/md384) in
[astral-sh/ruff#3084
- \[`pylint`] ` logging-too-few-args ` (`E1206`) by
[@&#8203;md384](https://togithub.com/md384) in
[astral-sh/ruff#3084

##### Bug Fixes

- Include file permissions in cache key by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3104
- Skip EXE001 and EXE002 rules on Windows by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3111
- Mark `typing.assert_never` as no return by
[@&#8203;bluetech](https://togithub.com/bluetech) in
[astral-sh/ruff#3121
- Use file-specific quote for C408 by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3128
- Avoid match statement misidentification in token rules by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3129
- Upgrade RustPython to handle trailing commas in map patterns by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3130
- Avoid useless-else-on-loop for break within match by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3136
- Fix isort `no-lines-before` preceded by an empty section by
[@&#8203;bluetech](https://togithub.com/bluetech) in
[astral-sh/ruff#3139
- Support shell expansion for --config argument by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3107
- Fix =/== error in `ManualDictLookup` by
[@&#8203;Rupt](https://togithub.com/Rupt) in
[astral-sh/ruff#3117
- Include match in nested block check by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3137
- Upgrade RustPython to match new flattened exports by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3141

#### New Contributors

- [@&#8203;md384](https://togithub.com/md384) made their first
contribution in
[astral-sh/ruff#3084
- [@&#8203;Rupt](https://togithub.com/Rupt) made their first
contribution in
[astral-sh/ruff#3117
- [@&#8203;marijncv](https://togithub.com/marijncv) made their first
contribution in
[astral-sh/ruff#3133

**Full Changelog**:
astral-sh/ruff@v0.0.251...v0.0.252

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://app.renovatebot.com/dashboard#github/ixm-one/pytest-cmake-presets).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNC4xNDguMCIsInVwZGF0ZWRJblZlciI6IjM0LjE0OC4wIn0=-->

Signed-off-by: Renovate Bot <bot@renovateapp.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
@henryiii
Copy link
Contributor

henryiii commented Feb 27, 2023

Was this intended to trigger when the with is outside the loop?

with open("something") as f:
    for line in f:
        line = line.strip()

Produces:

$ pipx run ruff --select PL tmp.py --isolated
tmp.py:3:9: PLW2901 Outer for loop variable `line` overwritten by inner assignment target
Found 1 error.

This does not produce a warning with pylint. From what I gathered above, this is deviating from pylint intentionally, but the with was supposed to be between the assignment and the loop?

I could go through and change the places where this happens, but it seems like a common and harmless pattern?

(If this was a unique warning number for with's, I'd probably ignore it, since very few blocks in Python make scope, including things that probably should. :) )

@matthewlloyd
Copy link
Contributor Author

matthewlloyd commented Feb 27, 2023

This does not produce a warning with pylint. From what I gathered above, this is deviating from pylint intentionally, but the with was supposed to be between the assignment and the loop?

This doesn't produce a warning with pylint by default because the entire W2901 check is optional and only enabled if the pylint.extensions.redefined_loop_name plugin is loaded. However, if you do load that plugin, you'll find that PyLint does produce an error on the third line here, yes, matching the behavior we decided to add to Ruff.

https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/redefined-loop-name.html

Note that the presence of the outer with is irrelevant here, and both PyLint and this version will produce an error on line = line.strip() even if you have only the for loop.

Ultimately it's a matter of personal preference. When I wrote this code, I was thinking I would add an option to disable the error for simple reassignments within a loop, since it seemed over the top. Then when I ran it on my codebase and weighed the pros and cons, I realized I would rather change my harmless habit (that wasn't really so harmless!), and instead write:

for raw_line in f:
    line = raw_line.strip()

The reasons being:

  • Unless you always strip the line every time you iterate over the lines in a file, it's easy to misremember whether your line variable represents the line with or without the newline, and end up e.g. printing or writing the newline to another file when it is not wanted or vice versa. Establishing a convention (or some similar convention) where line is newline-stripped and raw_line contains the newline makes this error less likely.
  • It's impossible for a linter tool to distinguish between the above "harmless" case, and more dangerous cases where the loop variable is reassigned accidentally:
for i in range(10):
    # ... many lines of code
    i = 10  # possibly buried in nested code
    # ... many lines of code
    print(i)   # developer expected `i` to contain the original iteration value

We'd have establish a whitelist of loop variable names that are allowed to be reassigned to, or a privileged position within the loop where it is permitted (e.g. the first line only). However that complicates the rule and is also fragile since it's impossible to cover every case.

IMHO it's better to just flag it as an error, and rename the variable or add a noqa as needed.

@matthewlloyd
Copy link
Contributor Author

Practically speaking one option would be to separate out "assignment within a loop" from "reuse as another loop variable" into two separate rules, e.g. PLW2901A and PLW2901, so users could choose to ignore PLW2901A while still catching more obviously unintentional errors like this as PLW2901:

for i in range(10):
    for i in range(10):  # PLW2901
        pass

@henryiii
Copy link
Contributor

Ahh, I missed that it was an optional pylint flag! I could have sworn I've seen that error before, and I'm pretty bad about digging up the optional pylint extensions.

Since it's not triggering due to the with, I'm fine with it. The wording of the error (with "outer") is a tiny bit confusing - if it just said it was a variable reassignment for the loop, I'd have understood it wasn't a false positive. I think. :)

@matthewlloyd
Copy link
Contributor Author

if it just said it was a variable reassignment for the loop, I'd have understood it wasn't a false positive.

That's a great point, we should perhaps only use the word "outer" when there are two loops involved. Thanks for your feedback!

@henryiii
Copy link
Contributor

we should perhaps only use the word "outer" when there are two loops involved

That would be great!

renovate bot added a commit to allenporter/flux-local that referenced this pull request Mar 1, 2023
[![Mend
Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com)

This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
| [ruff](https://togithub.com/charliermarsh/ruff) | `==0.0.247` ->
`==0.0.253` |
[![age](https://badges.renovateapi.com/packages/pypi/ruff/0.0.253/age-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://badges.renovateapi.com/packages/pypi/ruff/0.0.253/adoption-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://badges.renovateapi.com/packages/pypi/ruff/0.0.253/compatibility-slim/0.0.247)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://badges.renovateapi.com/packages/pypi/ruff/0.0.253/confidence-slim/0.0.247)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>charliermarsh/ruff</summary>

###
[`v0.0.253`](https://togithub.com/charliermarsh/ruff/releases/tag/v0.0.253)

[Compare
Source](https://togithub.com/charliermarsh/ruff/compare/v0.0.252...v0.0.253)

<!-- Release notes generated using configuration in .github/release.yml
at 386ca7c9a1bb7ebeb1457b605695c6a09e67092b -->

#### What's Changed

##### Rules

- \[`pyupgrade`] Avoid rewriting any PEP 604 runtime annotations by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3217
- \[`pycodestyle`] Missing whitespace after keyword by
[@&#8203;carlosmiei](https://togithub.com/carlosmiei) in
[astral-sh/ruff#3225
- \[`pycodestyle`] trailing-whitespace, blank-line-contains-whitespace
(W291, W293) by [@&#8203;mknaw](https://togithub.com/mknaw) in
[astral-sh/ruff#3122
- \[`flake8-pyi`]: PYI009, PYI010, PYI021 by
[@&#8203;sbdchd](https://togithub.com/sbdchd) in
[astral-sh/ruff#3230
- \[`flake8-pyi`]: PYI011, PYI014 by
[@&#8203;sbdchd](https://togithub.com/sbdchd) in
[astral-sh/ruff#3238
- \[`flake8-django`] DJ003, DJ006, DJ007 by
[@&#8203;lkh42t](https://togithub.com/lkh42t) in
[astral-sh/ruff#3236
- \[`pylint`] Implement pylint's `else-if-used` rule (`PLR5501`) by
[@&#8203;chanman3388](https://togithub.com/chanman3388) in
[astral-sh/ruff#3231
- \[`pylint`] W0603: global-statement by
[@&#8203;igozali](https://togithub.com/igozali) in
[astral-sh/ruff#3227
- \[`flake8-pie`] Unnecessary list comprehension, with autofix (PIE802)
by [@&#8203;matthewlloyd](https://togithub.com/matthewlloyd) in
[astral-sh/ruff#3149

##### Settings

- Allow ruff.toml file to be dot-prefixed (as .ruff.toml) by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3221
- \[`pydocstyle`]: Implement `ignore-decorators` by
[@&#8203;edgarrmondragon](https://togithub.com/edgarrmondragon) in
[astral-sh/ruff#3229

##### Bug Fixes

- Avoid suggesting 'is' for constant literals by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3146
- Omit non-.py\[i] files from module naming rules by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3153
- Bind star patterns in match statements by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3169
- Update RustPython to support \*tuple annotations by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3178
- Use `writeln` with --show-settings by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3180
- Avoid boolean-trap rules for ConfigParser get() methods by
[@&#8203;monosans](https://togithub.com/monosans) in
[astral-sh/ruff#3209
- Avoid flagging logging-too-few-args with no arguments by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3220
- Expand the range of the COM812 autofix to include the preceding token
by [@&#8203;matthewlloyd](https://togithub.com/matthewlloyd) in
[astral-sh/ruff#3241
- Avoid flagging Pylint logging rules with starred arguments by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3244
- Avoid flagging unfixable `TypedDict` and `NamedTuple` definitions by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3148
- Fix ExceptionGroup F821 false positive by
[@&#8203;JonathanPlasse](https://togithub.com/JonathanPlasse) in
[astral-sh/ruff#3167
- Avoid autofixing some PT violations when comments are present by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3198
- Exclude globsets for --show-settings by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3201
- \[`flake8-tidy-imports`] fix autofix for relative imports by
[@&#8203;sciyoshi](https://togithub.com/sciyoshi) in
[astral-sh/ruff#3197
- Fix Markdown errors in docs by
[@&#8203;JonathanPlasse](https://togithub.com/JonathanPlasse) in
[astral-sh/ruff#3187
- Normalize treatment of aliased and unaliased imports by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3216
- Avoid EXE001 and EXE002 errors from stdin input by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3218
- \[bandit]: Do not treat "passed" as "password" for
`S105`/`S106`/`S107` by
[@&#8203;edgarrmondragon](https://togithub.com/edgarrmondragon) in
[astral-sh/ruff#3222

#### New Contributors

- [@&#8203;mknaw](https://togithub.com/mknaw) made their first
contribution in
[astral-sh/ruff#3122
- [@&#8203;monosans](https://togithub.com/monosans) made their first
contribution in
[astral-sh/ruff#3209
- [@&#8203;lkh42t](https://togithub.com/lkh42t) made their first
contribution in
[astral-sh/ruff#3236
- [@&#8203;igozali](https://togithub.com/igozali) made their first
contribution in
[astral-sh/ruff#3227

**Full Changelog**:
astral-sh/ruff@v0.0.252...v0.0.253

###
[`v0.0.252`](https://togithub.com/charliermarsh/ruff/releases/tag/v0.0.252)

[Compare
Source](https://togithub.com/charliermarsh/ruff/compare/v0.0.251...v0.0.252)

<!-- Release notes generated using configuration in .github/release.yml
at main -->

#### What's Changed

##### Rules

- \[`pylint`] `redefined-loop-name` (`W2901`) by
[@&#8203;matthewlloyd](https://togithub.com/matthewlloyd) in
[astral-sh/ruff#3022
- \[`pylint`] ` logging-too-many-args ` (`E1205`) by
[@&#8203;md384](https://togithub.com/md384) in
[astral-sh/ruff#3084
- \[`pylint`] ` logging-too-few-args ` (`E1206`) by
[@&#8203;md384](https://togithub.com/md384) in
[astral-sh/ruff#3084

##### Bug Fixes

- Include file permissions in cache key by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3104
- Skip EXE001 and EXE002 rules on Windows by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3111
- Mark `typing.assert_never` as no return by
[@&#8203;bluetech](https://togithub.com/bluetech) in
[astral-sh/ruff#3121
- Use file-specific quote for C408 by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3128
- Avoid match statement misidentification in token rules by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3129
- Upgrade RustPython to handle trailing commas in map patterns by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3130
- Avoid useless-else-on-loop for break within match by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3136
- Fix isort `no-lines-before` preceded by an empty section by
[@&#8203;bluetech](https://togithub.com/bluetech) in
[astral-sh/ruff#3139
- Support shell expansion for --config argument by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3107
- Fix =/== error in `ManualDictLookup` by
[@&#8203;Rupt](https://togithub.com/Rupt) in
[astral-sh/ruff#3117
- Include match in nested block check by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3137
- Upgrade RustPython to match new flattened exports by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3141

#### New Contributors

- [@&#8203;md384](https://togithub.com/md384) made their first
contribution in
[astral-sh/ruff#3084
- [@&#8203;Rupt](https://togithub.com/Rupt) made their first
contribution in
[astral-sh/ruff#3117
- [@&#8203;marijncv](https://togithub.com/marijncv) made their first
contribution in
[astral-sh/ruff#3133

**Full Changelog**:
astral-sh/ruff@v0.0.251...v0.0.252

###
[`v0.0.251`](https://togithub.com/charliermarsh/ruff/releases/tag/v0.0.251)

[Compare
Source](https://togithub.com/charliermarsh/ruff/compare/v0.0.250...v0.0.251)

<!-- Release notes generated using configuration in .github/release.yml
at main -->

#### What's Changed

##### Bug Fixes

- Create bindings for `MatchAs` patterns by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3098
- Avoid prefer-list-builtin for lambdas with `*args` or `**kwargs` by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3102

**Full Changelog**:
astral-sh/ruff@v0.0.250...v0.0.251

###
[`v0.0.250`](https://togithub.com/charliermarsh/ruff/releases/tag/v0.0.250)

[Compare
Source](https://togithub.com/charliermarsh/ruff/compare/v0.0.249...v0.0.250)

<!-- Release notes generated using configuration in .github/release.yml
at main -->

#### What's Changed

**Ruff now supports all Python 3.10 and 3.11 language features**,
including:

- Structural Pattern Patching (`match` statements) ([PEP
634](https://peps.python.org/pep-0634/#class-patterns))
- Exception Groups (`except*` statements) ([PEP
654](https://peps.python.org/pep-0654/))

##### Rules

- \[`flake8-bugbear`] Add B029 (`except-with-empty-tuple`) from
flake8-bugbear by [@&#8203;carlosmiei](https://togithub.com/carlosmiei)
in
[astral-sh/ruff#3068
- \[`flake8-bugbear`] Add B032 (`unintentional-type-annotation`) from
flake8\_bugbear by [@&#8203;carlosmiei](https://togithub.com/carlosmiei)
in
[astral-sh/ruff#3085
- \[`tryceratops`]: Add TRY401 (`verbose-log-messages`) by
[@&#8203;colin99d](https://togithub.com/colin99d) in
[astral-sh/ruff#3036
- \[`flake8-simplify`]: Add SIM116 (`manual-dict-lookup`) by
[@&#8203;colin99d](https://togithub.com/colin99d) in
[astral-sh/ruff#2767

##### Features

- Add support for TryStar by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3089
- Add support for structural pattern matching by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3047

##### Bug Fixes

- \[`flake8-pytest`] Use LibCST to fix chained assertions by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3087
- \[`flake8-boolean-trap`] Avoid boolean-trap rules for positional-only
builtin calls by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3075
- \[`flake8-boolean-trap`] Ignore setters in flake8-boolean-trap by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3092
- \[`flake8-return`] Omit `while-True` loops from implicit return
enforcement by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3076

#### New Contributors

- [@&#8203;carlosmiei](https://togithub.com/carlosmiei) made their first
contribution in
[astral-sh/ruff#3068

**Full Changelog**:
astral-sh/ruff@v0.0.249...v0.0.250

###
[`v0.0.249`](https://togithub.com/charliermarsh/ruff/releases/tag/v0.0.249)

[Compare
Source](https://togithub.com/charliermarsh/ruff/compare/v0.0.248...v0.0.249)

<!-- Release notes generated using configuration in .github/release.yml
at 4cfa350112a82fb631909fc555588f3da8ba5750 -->

#### What's Changed

##### Bug Fixes

- Relax constraints on pep8-naming module validation by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3043
- Do not autofix `E731` in class bodies by
[@&#8203;JoshKarpel](https://togithub.com/JoshKarpel) in
[astral-sh/ruff#3050
- Avoid assert() to assert statement conversion in expressions by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3062

#### New Contributors

- [@&#8203;matthewlloyd](https://togithub.com/matthewlloyd) made their
first contribution in
[astral-sh/ruff#3048
- [@&#8203;JoshKarpel](https://togithub.com/JoshKarpel) made their first
contribution in
[astral-sh/ruff#3050

**Full Changelog**:
astral-sh/ruff@v0.0.248...v0.0.249

###
[`v0.0.248`](https://togithub.com/charliermarsh/ruff/releases/tag/v0.0.248)

[Compare
Source](https://togithub.com/charliermarsh/ruff/compare/v0.0.247...v0.0.248)

<!-- Release notes generated using configuration in .github/release.yml
at main -->

#### What's Changed

##### Rules

- \[`numpy`] numpy-legacy-random by
[@&#8203;sbrugman](https://togithub.com/sbrugman) in
[astral-sh/ruff#2960
- \[`pycodestyle`] autofix useless semicolons by
[@&#8203;sbrugman](https://togithub.com/sbrugman) in
[astral-sh/ruff#3001
- \[`pep8-naming`] Implement `flake8-module-naming` by
[@&#8203;sbrugman](https://togithub.com/sbrugman) in
[astral-sh/ruff#2855
- \[`flake8-self`] Ignore namedtuple methods in flake8-self by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2998
- \[`flake8-simplify`] Merge convert-loop-to-any & convert-loop-to-all
to reimplemented-builtin by
[@&#8203;not-my-profile](https://togithub.com/not-my-profile) in
[astral-sh/ruff#2903
- \[`ruff`] Add support for `ensure_future` for RUF006 by
[@&#8203;Lunarmagpie](https://togithub.com/Lunarmagpie) in
[astral-sh/ruff#2943
- \[`pylint`] error when `__init__` returns a value by
[@&#8203;r3m0t](https://togithub.com/r3m0t) in
[astral-sh/ruff#3007
- \[`flake8-pytest-style`] autofix for composite-assertion (PT018) by
[@&#8203;sbrugman](https://togithub.com/sbrugman) in
[astral-sh/ruff#2732
- \[`flake8-tidy-imports`] extend autofix of relative imports by
[@&#8203;sbrugman](https://togithub.com/sbrugman) in
[astral-sh/ruff#2990

##### Settings

- Add support for file-scoped `noqa` directives by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2978
- Add configuration option for C408 to allow dict calls with keyword
arguments. by [@&#8203;manueljacob](https://togithub.com/manueljacob) in
[astral-sh/ruff#2977
- feat(isort): Implement isort.force_to_top by
[@&#8203;spaceone](https://togithub.com/spaceone) in
[astral-sh/ruff#2877

##### Bug Fixes

- Fix add-required-import with multi-line offsets by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2946
- Support positional messages in assertion rewrites by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3002
- Avoid false-positives for break in with by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#3032
- Avoid trying to fix implicit returns with control flow by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2962
- Handle non-from **future** imports by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2974
- Enforce D403 on methods by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2992
- Avoid zero-indexed column for IOError by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2995
- Fix for F541 unescape f-string by
[@&#8203;sbrugman](https://togithub.com/sbrugman) in
[astral-sh/ruff#2971
- Avoid raising `B027` violations in `.pyi` files by
[@&#8203;JonathanPlasse](https://togithub.com/JonathanPlasse) in
[astral-sh/ruff#3016

#### New Contributors

- [@&#8203;Lunarmagpie](https://togithub.com/Lunarmagpie) made their
first contribution in
[astral-sh/ruff#2943
- [@&#8203;manueljacob](https://togithub.com/manueljacob) made their
first contribution in
[astral-sh/ruff#2966
- [@&#8203;mwtoews](https://togithub.com/mwtoews) made their first
contribution in
[astral-sh/ruff#2973
- [@&#8203;ortem](https://togithub.com/ortem) made their first
contribution in
[astral-sh/ruff#2976
- [@&#8203;thatlittleboy](https://togithub.com/thatlittleboy) made their
first contribution in
[astral-sh/ruff#3027
- [@&#8203;r3m0t](https://togithub.com/r3m0t) made their first
contribution in
[astral-sh/ruff#3007

**Full Changelog**:
astral-sh/ruff@v0.0.247...v0.0.248

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR has been generated by [Mend
Renovate](https://www.mend.io/free-developer-tools/renovate/). View
repository job log
[here](https://app.renovatebot.com/dashboard#github/allenporter/flux-local).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNC4xNTMuMiIsInVwZGF0ZWRJblZlciI6IjM0LjE1My4yIn0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rule Implementing or modifying a lint rule
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Proposed check: reuse of loop variable in nested loop
5 participants