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

[pyupgrade]: Remove outdated sys.version_info blocks #2099

Merged
merged 62 commits into from Feb 2, 2023
Merged

[pyupgrade]: Remove outdated sys.version_info blocks #2099

merged 62 commits into from Feb 2, 2023

Conversation

colin99d
Copy link
Contributor

A part of #827. Opening this up as a draft for visibility.

@colin99d
Copy link
Contributor Author

colin99d commented Jan 25, 2023

@charliermarsh I am hitting a blocker and was wondering if you could offer some assistance. My issue is linting the code below:

if sys.version_info < (2,0):
    print("This script requires Python 2.0 or greater.")
elif sys.version_info < (2,6):
    print("This script requires Python 2.6 or greater.")
elif sys.version_info < (3,):
    print("This script requires Python 3.0 or greater.")
elif sys.version_info < (3,0):
    print("This script requires Python 3.0 or greater.")
elif sys.version_info < (3,12):
    print("Python 3.0 or 3.1 or 3.2")
else:
    print("Python 3")

Currently, my program lints every line, and removes it if the condition meets the requirements, this means we end up with the following:

elif sys.version_info < (3,12):
    print("Python 3.0 or 3.1 or 3.2")
else:
    print("Python 3")

As you can see, this is not a valid python statement. I tried creating checkers that either look at the parent, or see if the statement is an if to remove the "el" from the "elif" in the next line, however; since ruff goes through all elif and if statements before running again, the code is already broken before the next loop through. So essentially I need to know whether to convert an elif statement to an if statement.

Pyupgrade currently solves this issue by only removing one if or elif at a time, so to fix the statement above you would have to call pyupgrade four times.

Only running on if statements is not a viable solution here because sometimes the issue might only be on an elif statement. So far my best ideas would be to:

  1. Somehow "skip" the rest of the statement until the next run through,
  2. Save the state somehow, so it can know whether to skip (this seems like a pretty bad idea)
  3. Allow the statement to be converted to the bad format, and then run a separate linter at the end that can fix it. This seems like a REALLY bad idea because if the linter breaks anywhere else, we will wreck people's code

@colin99d
Copy link
Contributor Author

It looks like I have a second issue that could be easily fixed if I can skip fixing the rest of the statement until the next iteration. Is there any functionality like that?

@charliermarsh
Copy link
Member

We avoid applying fixes that "overlap" in the text. So one thing you could do is... when you go to fix one of these, generate a fix that applies to the entire if chain, even if you're only changing one of the statements within it.

So if you had this:

if sys.version_info < (2,0):
    print("This script requires Python 2.0 or greater.")
elif sys.version_info < (2,6):
    print("This script requires Python 2.6 or greater.")
elif sys.version_info < (3,):
    print("This script requires Python 3.0 or greater.")
elif sys.version_info < (3,0):
    print("This script requires Python 3.0 or greater.")
elif sys.version_info < (3,12):
    print("Python 3.0 or 3.1 or 3.2")
else:
    print("Python 3")

Then to fix the first one, you'd generate a Fix::replace with this text:

if sys.version_info < (2,6):
    print("This script requires Python 2.6 or greater.")
elif sys.version_info < (3,):
    print("This script requires Python 3.0 or greater.")
elif sys.version_info < (3,0):
    print("This script requires Python 3.0 or greater.")
elif sys.version_info < (3,12):
    print("Python 3.0 or 3.1 or 3.2")
else:
    print("Python 3")

All the fixes will overlap, and so it'll only apply one per iteration.

@colin99d
Copy link
Contributor Author

We avoid applying fixes that "overlap" in the text. So one thing you could do is... when you go to fix one of these, generate a fix that applies to the entire if chain, even if you're only changing one of the statements within it.

So if you had this:

if sys.version_info < (2,0):
    print("This script requires Python 2.0 or greater.")
elif sys.version_info < (2,6):
    print("This script requires Python 2.6 or greater.")
elif sys.version_info < (3,):
    print("This script requires Python 3.0 or greater.")
elif sys.version_info < (3,0):
    print("This script requires Python 3.0 or greater.")
elif sys.version_info < (3,12):
    print("Python 3.0 or 3.1 or 3.2")
else:
    print("Python 3")

Then to fix the first one, you'd generate a Fix::replace with this text:

if sys.version_info < (2,6):
    print("This script requires Python 2.6 or greater.")
elif sys.version_info < (3,):
    print("This script requires Python 3.0 or greater.")
elif sys.version_info < (3,0):
    print("This script requires Python 3.0 or greater.")
elif sys.version_info < (3,12):
    print("Python 3.0 or 3.1 or 3.2")
else:
    print("Python 3")

All the fixes will overlap, and so it'll only apply one per iteration.

Charlie this is absolutely perfect! Thank you so much!!!

@charliermarsh
Copy link
Member

No worries, sorry it took me a while to respond. It might take some work to find the outermost if parent, but hopefully it's at least possible.

@colin99d
Copy link
Contributor Author

@charliermarsh, I believe I found an issue with the rustpython_parser::lexer. When I run the following program in python, it runs fine:

import six

if True:
    if six.PY2:
        print("PY2")
    else:
        print("PY3")

However, when I attempt to use the lexer to get the tokens in the statement I get the following issue:
value: LexicalError { error: IndentationError, location: Location { row: 3, column: 4 } }. When printing the tokens out I get the following before the error occurs:

If
Name { name: "six" }
Dot
Name { name: "PY2" }
Colon
Newline
Indent
Name { name: "print" }
Lpar
String { value: "PY2", kind: String, triple_quoted: false }
Rpar
Newline

Based on this, and the position of the location where the error occurs, it looks like the parser is failing one the else token. Do you have any idea what could be going on here?

@charliermarsh
Copy link
Member

I think you need to dedent the entire block before lexing.

Right now, you're feeding it:

if six.PY2:
        print("PY2")
    else:
        print("PY3")

...since you're starting at the if. So it looks like invalid indentation.

(This is a guess, I admittedly didn't look at the code.)

@colin99d
Copy link
Contributor Author

I think you need to dedent the entire block before lexing.

Right now, you're feeding it:

if six.PY2:
        print("PY2")
    else:
        print("PY3")

...since you're starting at the if. So it looks like invalid indentation.

(This is a guess, I admittedly didn't look at the code.)

I think you are exactly right, is there an example somewhere in the code of this being done already? I noticed dedent does not work if the first line has no indent.

@charliermarsh charliermarsh changed the title Pyupgrade: Old code blocks [pyupgrade]: Remove outdated sys.version_info blocks Feb 1, 2023
@charliermarsh charliermarsh added the rule Implementing or modifying a lint rule label Feb 1, 2023
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.code(), path.display());
Copy link
Member

Choose a reason for hiding this comment

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

@sbrugman - Do you have any idea why these are failing on Windows? I tried with both to_string_lossy() and display() but no luck.

@charliermarsh
Copy link
Member

@colin99d - Did a pass over this, I was able to simplify a few things a little bit, but I also made it slightly more timid in some cases... Do you have any interest in looking through or even testing the code prior to merging?

@colin99d
Copy link
Contributor Author

colin99d commented Feb 1, 2023

@colin99d - Did a pass over this, I was able to simplify a few things a little bit, but I also made it slightly more timid in some cases... Do you have any interest in looking through or even testing the code prior to merging?

Would love to! I can do this tonight.

@colin99d
Copy link
Contributor Author

colin99d commented Feb 1, 2023

What is the reason for not refactoring single line if statements anymore? I am assuming there were some edge cases it was not handling well.

@charliermarsh
Copy link
Member

I thought it would just be simpler to not worry about them given that they're pretty rare (e.g. Black doesn't preserve them). It's possible that the implementation was totally sound... I just get worried about cases like multi-statement lines and continuations:

if True: a = 1; b = 2

if True: a = 1; \
  b = 2

But this isn't a rigorous answer at all. Maybe we should revisit.

@colin99d
Copy link
Contributor Author

colin99d commented Feb 1, 2023

I thought it would just be simpler to not worry about them given that they're pretty rare (e.g. Black doesn't preserve them). It's possible that the implementation was totally sound... I just get worried about cases like multi-statement lines and continuations:

if True: a = 1; b = 2

if True: a = 1; \
  b = 2

But this isn't a rigorous answer at all. Maybe we should revisit.

I have not seen this in practice before EVER, so maybe we ignore it, and if someone leaves an issue, and it gains some traction, we can invest into it.

@charliermarsh
Copy link
Member

There is one failing case right now, which I think we can only solve with LibCST -- something like this:

if True:
    if sys.version_info >= (3, 9):
        expected_error = [
"<stdin>:1:5: Generator expression must be parenthesized",
"max(1 for i in range(10), key=lambda x: x+1)",
"    ^",
        ]
    elif PYPY:
        expected_error = []
    else:
        expected_error = []

Or even harder:

if True:
    if sys.version_info >= (3, 9):
        """this
        is valid"""

        """the indentation on
        this line is significant"""

        "this is" \
"allowed too"

        ("so is"
"this for some reason")

dedent isn't safe for these reasons.

@charliermarsh
Copy link
Member

(Fixed.)

@charliermarsh
Copy link
Member

Ok, I think this is good to go.

@charliermarsh
Copy link
Member

@colin99d - LMK if you wanna give this a read tonight or if I should go ahead and merge. It's not urgent -- I probably won't release tonight anyway.

@charliermarsh charliermarsh merged commit b032f50 into astral-sh:main Feb 2, 2023
@charliermarsh
Copy link
Member

Merging for now but LMK if you have any follow-up feedback.

@colin99d
Copy link
Contributor Author

colin99d commented Feb 2, 2023

Sorry, had something come up last night. Looks good to me!!!

@charliermarsh
Copy link
Member

All good, no prob at all!

renovate bot added a commit to ixm-one/pytest-cmake-presets that referenced this pull request Feb 3, 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.239` ->
`^0.0.240` |
[![age](https://badges.renovateapi.com/packages/pypi/ruff/0.0.240/age-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://badges.renovateapi.com/packages/pypi/ruff/0.0.240/adoption-slim)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://badges.renovateapi.com/packages/pypi/ruff/0.0.240/compatibility-slim/0.0.239)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://badges.renovateapi.com/packages/pypi/ruff/0.0.240/confidence-slim/0.0.239)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

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

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

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

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

#### What's Changed

##### Rules

- \[`pyupgrade`]: Remove outdated `sys.version_info` blocks by
[@&#8203;colin99d](https://togithub.com/colin99d) in
[astral-sh/ruff#2099
- \[`flake8-self`] Add Plugin and Rule `SLF001` by
[@&#8203;saadmk11](https://togithub.com/saadmk11) in
[astral-sh/ruff#2470
- \[`pylint`] Implement pylint's `too-many-statements` rule (`PLR0915`)
by [@&#8203;chanman3388](https://togithub.com/chanman3388) in
[astral-sh/ruff#2445

##### Settings

- \[`isort`] Support forced_separate by
[@&#8203;akx](https://togithub.com/akx) in
[astral-sh/ruff#2268
- \[`isort`] Add isort option lines-after-imports by
[@&#8203;reidswan](https://togithub.com/reidswan) in
[astral-sh/ruff#2440
- Allow non-ruff.toml-named files for --config by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2463

##### Bug Fixes

- Trigger, but don't fix, SIM rules if comments are present by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2450
- Only avoid PEP604 rewrites for pre-Python 3.10 code by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2449
- Use LibCST to reverse Yoda conditions by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2468
- fix: assertTrue()/assertFalse() fixer should not test for identity by
[@&#8203;spaceone](https://togithub.com/spaceone) in
[astral-sh/ruff#2476
- Treat `if 0:` and `if False:` as type-checking blocks by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2485
- Avoid iterating over body twice by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2439
- more builtin name checks when autofixing by
[@&#8203;spaceone](https://togithub.com/spaceone) in
[astral-sh/ruff#2430
- Respect parent noqa in --add-noqa by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2464
- Avoid removing un-selected codes when applying `--add-noqa` edits by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2465
- Carry-over `ignore` to next config layer if `select = []` by
[@&#8203;not-my-profile](https://togithub.com/not-my-profile) in
[astral-sh/ruff#2467
- Visit NamedExpr values before targets by
[@&#8203;charliermarsh](https://togithub.com/charliermarsh) in
[astral-sh/ruff#2484

#### New Contributors

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

**Full Changelog**:
astral-sh/ruff@v0.0.239...v0.0.240

</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:eyJjcmVhdGVkSW5WZXIiOiIzNC4xMTkuNSIsInVwZGF0ZWRJblZlciI6IjM0LjExOS41In0=-->

Signed-off-by: Renovate Bot <bot@renovateapp.com>
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.

None yet

3 participants