mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-17 13:57:25 +00:00
Remove parentheses around multiple exception types on Python 3.14+ (#20768)
Summary -- This PR implements the black preview style from https://github.com/psf/black/pull/4720. As of Python 3.14, you're allowed to omit the parentheses around groups of exceptions, as long as there's no `as` binding: **3.13** ```pycon Python 3.13.4 (main, Jun 4 2025, 17:37:06) [Clang 20.1.4 ] on linux Type "help", "copyright", "credits" or "license" for more information. >>> try: ... ... except (Exception, BaseException): ... ... Ellipsis >>> try: ... ... except Exception, BaseException: ... ... File "<python-input-1>", line 2 except Exception, BaseException: ... ^^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: multiple exception types must be parenthesized ``` **3.14** ```pycon Python 3.14.0rc2 (main, Sep 2 2025, 14:20:56) [Clang 20.1.4 ] on linux Type "help", "copyright", "credits" or "license" for more information. >>> try: ... ... except Exception, BaseException: ... ... Ellipsis >>> try: ... ... except (Exception, BaseException): ... ... Ellipsis >>> try: ... ... except Exception, BaseException as e: ... ... File "<python-input-2>", line 2 except Exception, BaseException as e: ... ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SyntaxError: multiple exception types must be parenthesized when using 'as' ``` I think this ended up being pretty straightforward, at least once Micha showed me where to start :) Test Plan -- New tests At first I thought we were deviating from black in how we handle comments within the exception type tuple, but I think this applies to how we format all tuples, not specifically with the new preview style.
This commit is contained in:
parent
1ed9b215b9
commit
591e9bbccb
8 changed files with 497 additions and 450 deletions
|
@ -1,427 +0,0 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/black/cases/remove_except_types_parens.py
|
||||
---
|
||||
## Input
|
||||
|
||||
```python
|
||||
# SEE PEP 758 FOR MORE DETAILS
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ValueError:
|
||||
pass
|
||||
|
||||
# parenthesis are removed
|
||||
try:
|
||||
pass
|
||||
except (ValueError):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError):
|
||||
pass
|
||||
|
||||
# parenthesis are removed
|
||||
try:
|
||||
pass
|
||||
except (ValueError) as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError) as e:
|
||||
pass
|
||||
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except (ValueError,):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError,):
|
||||
pass
|
||||
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except (ValueError,) as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError,) as e:
|
||||
pass
|
||||
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except ValueError, TypeError, KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ValueError, TypeError, KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
# parenthesis are removed
|
||||
try:
|
||||
pass
|
||||
except (ValueError, TypeError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError, TypeError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
# parenthesis are not removed
|
||||
try:
|
||||
pass
|
||||
except (ValueError, TypeError, KeyboardInterrupt) as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError, TypeError, KeyboardInterrupt) as e:
|
||||
pass
|
||||
|
||||
# parenthesis are removed
|
||||
try:
|
||||
pass
|
||||
except (ValueError if True else TypeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError if True else TypeError):
|
||||
pass
|
||||
|
||||
# inner except: parenthesis are removed
|
||||
# outer except: parenthsis are not removed
|
||||
try:
|
||||
try:
|
||||
pass
|
||||
except (TypeError, KeyboardInterrupt):
|
||||
pass
|
||||
except (ValueError,):
|
||||
pass
|
||||
|
||||
try:
|
||||
try:
|
||||
pass
|
||||
except* (TypeError, KeyboardInterrupt):
|
||||
pass
|
||||
except* (ValueError,):
|
||||
pass
|
||||
```
|
||||
|
||||
## Black Differences
|
||||
|
||||
```diff
|
||||
--- Black
|
||||
+++ Ruff
|
||||
@@ -74,12 +74,12 @@
|
||||
# parenthesis are removed
|
||||
try:
|
||||
pass
|
||||
-except ValueError, TypeError, KeyboardInterrupt:
|
||||
+except (ValueError, TypeError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
-except* ValueError, TypeError, KeyboardInterrupt:
|
||||
+except* (ValueError, TypeError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
# parenthesis are not removed
|
||||
@@ -109,7 +109,7 @@
|
||||
try:
|
||||
try:
|
||||
pass
|
||||
- except TypeError, KeyboardInterrupt:
|
||||
+ except (TypeError, KeyboardInterrupt):
|
||||
pass
|
||||
except (ValueError,):
|
||||
pass
|
||||
@@ -117,7 +117,7 @@
|
||||
try:
|
||||
try:
|
||||
pass
|
||||
- except* TypeError, KeyboardInterrupt:
|
||||
+ except* (TypeError, KeyboardInterrupt):
|
||||
pass
|
||||
except* (ValueError,):
|
||||
pass
|
||||
```
|
||||
|
||||
## Ruff Output
|
||||
|
||||
```python
|
||||
# SEE PEP 758 FOR MORE DETAILS
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ValueError:
|
||||
pass
|
||||
|
||||
# parenthesis are removed
|
||||
try:
|
||||
pass
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ValueError:
|
||||
pass
|
||||
|
||||
# parenthesis are removed
|
||||
try:
|
||||
pass
|
||||
except ValueError as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ValueError as e:
|
||||
pass
|
||||
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except (ValueError,):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError,):
|
||||
pass
|
||||
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except (ValueError,) as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError,) as e:
|
||||
pass
|
||||
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except ValueError, TypeError, KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ValueError, TypeError, KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
# parenthesis are removed
|
||||
try:
|
||||
pass
|
||||
except (ValueError, TypeError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError, TypeError, KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
# parenthesis are not removed
|
||||
try:
|
||||
pass
|
||||
except (ValueError, TypeError, KeyboardInterrupt) as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError, TypeError, KeyboardInterrupt) as e:
|
||||
pass
|
||||
|
||||
# parenthesis are removed
|
||||
try:
|
||||
pass
|
||||
except ValueError if True else TypeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ValueError if True else TypeError:
|
||||
pass
|
||||
|
||||
# inner except: parenthesis are removed
|
||||
# outer except: parenthsis are not removed
|
||||
try:
|
||||
try:
|
||||
pass
|
||||
except (TypeError, KeyboardInterrupt):
|
||||
pass
|
||||
except (ValueError,):
|
||||
pass
|
||||
|
||||
try:
|
||||
try:
|
||||
pass
|
||||
except* (TypeError, KeyboardInterrupt):
|
||||
pass
|
||||
except* (ValueError,):
|
||||
pass
|
||||
```
|
||||
|
||||
## Black Output
|
||||
|
||||
```python
|
||||
# SEE PEP 758 FOR MORE DETAILS
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ValueError:
|
||||
pass
|
||||
|
||||
# parenthesis are removed
|
||||
try:
|
||||
pass
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ValueError:
|
||||
pass
|
||||
|
||||
# parenthesis are removed
|
||||
try:
|
||||
pass
|
||||
except ValueError as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ValueError as e:
|
||||
pass
|
||||
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except (ValueError,):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError,):
|
||||
pass
|
||||
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except (ValueError,) as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError,) as e:
|
||||
pass
|
||||
|
||||
# remains unchanged
|
||||
try:
|
||||
pass
|
||||
except ValueError, TypeError, KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ValueError, TypeError, KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
# parenthesis are removed
|
||||
try:
|
||||
pass
|
||||
except ValueError, TypeError, KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ValueError, TypeError, KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
# parenthesis are not removed
|
||||
try:
|
||||
pass
|
||||
except (ValueError, TypeError, KeyboardInterrupt) as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (ValueError, TypeError, KeyboardInterrupt) as e:
|
||||
pass
|
||||
|
||||
# parenthesis are removed
|
||||
try:
|
||||
pass
|
||||
except ValueError if True else TypeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* ValueError if True else TypeError:
|
||||
pass
|
||||
|
||||
# inner except: parenthesis are removed
|
||||
# outer except: parenthsis are not removed
|
||||
try:
|
||||
try:
|
||||
pass
|
||||
except TypeError, KeyboardInterrupt:
|
||||
pass
|
||||
except (ValueError,):
|
||||
pass
|
||||
|
||||
try:
|
||||
try:
|
||||
pass
|
||||
except* TypeError, KeyboardInterrupt:
|
||||
pass
|
||||
except* (ValueError,):
|
||||
pass
|
||||
```
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
source: crates/ruff_python_formatter/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/try.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## Input
|
||||
```python
|
||||
|
@ -173,9 +172,61 @@ else:
|
|||
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
# These parens can be removed on 3.14+ but not earlier
|
||||
except (BaseException, Exception, ValueError):
|
||||
pass
|
||||
# But black won't remove these parentheses
|
||||
except (ZeroDivisionError,):
|
||||
pass
|
||||
except ( # We wrap these and preserve the parens
|
||||
BaseException, Exception, ValueError):
|
||||
pass
|
||||
except (
|
||||
BaseException,
|
||||
# Same with this comment
|
||||
Exception,
|
||||
ValueError
|
||||
):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
# They can also be omitted for `except*`
|
||||
except* (BaseException, Exception, ValueError):
|
||||
pass
|
||||
|
||||
# But parentheses are still required in the presence of an `as` binding
|
||||
try:
|
||||
pass
|
||||
except (BaseException, Exception, ValueError) as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (BaseException, Exception, ValueError) as e:
|
||||
pass
|
||||
```
|
||||
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
line-ending = LineFeed
|
||||
magic-trailing-comma = Respect
|
||||
docstring-code = Disabled
|
||||
docstring-code-line-width = "dynamic"
|
||||
preview = Disabled
|
||||
target_version = 3.13
|
||||
source_type = Python
|
||||
```
|
||||
|
||||
## Output
|
||||
```python
|
||||
try:
|
||||
pass
|
||||
|
@ -364,10 +415,50 @@ else:
|
|||
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
# These parens can be removed on 3.14+ but not earlier
|
||||
except (BaseException, Exception, ValueError):
|
||||
pass
|
||||
# But black won't remove these parentheses
|
||||
except (ZeroDivisionError,):
|
||||
pass
|
||||
except ( # We wrap these and preserve the parens
|
||||
BaseException,
|
||||
Exception,
|
||||
ValueError,
|
||||
):
|
||||
pass
|
||||
except (
|
||||
BaseException,
|
||||
# Same with this comment
|
||||
Exception,
|
||||
ValueError,
|
||||
):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
# They can also be omitted for `except*`
|
||||
except* (BaseException, Exception, ValueError):
|
||||
pass
|
||||
|
||||
# But parentheses are still required in the presence of an `as` binding
|
||||
try:
|
||||
pass
|
||||
except (BaseException, Exception, ValueError) as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (BaseException, Exception, ValueError) as e:
|
||||
pass
|
||||
```
|
||||
|
||||
|
||||
## Preview changes
|
||||
#### Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
|
@ -392,3 +483,294 @@ finally:
|
|||
|
||||
def f():
|
||||
```
|
||||
|
||||
|
||||
### Output 2
|
||||
```
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
line-ending = LineFeed
|
||||
magic-trailing-comma = Respect
|
||||
docstring-code = Disabled
|
||||
docstring-code-line-width = "dynamic"
|
||||
preview = Disabled
|
||||
target_version = 3.14
|
||||
source_type = Python
|
||||
```
|
||||
|
||||
```python
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except KeyError: # should remove brackets and be a single line
|
||||
pass
|
||||
|
||||
|
||||
try: # try
|
||||
pass
|
||||
# end of body
|
||||
# before except
|
||||
except (Exception, ValueError) as exc: # except line
|
||||
pass
|
||||
# before except 2
|
||||
except KeyError as key: # except line 2
|
||||
pass
|
||||
# in body 2
|
||||
# before else
|
||||
else:
|
||||
pass
|
||||
# before finally
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
# with line breaks
|
||||
try: # try
|
||||
pass
|
||||
# end of body
|
||||
|
||||
# before except
|
||||
except (Exception, ValueError) as exc: # except line
|
||||
pass
|
||||
|
||||
# before except 2
|
||||
except KeyError as key: # except line 2
|
||||
pass
|
||||
# in body 2
|
||||
|
||||
# before else
|
||||
else:
|
||||
pass
|
||||
|
||||
# before finally
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
# with line breaks
|
||||
try:
|
||||
pass
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (
|
||||
Exception,
|
||||
Exception,
|
||||
Exception,
|
||||
Exception,
|
||||
Exception,
|
||||
Exception,
|
||||
Exception,
|
||||
) as exc: # splits exception over multiple lines
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
a = 10 # trailing comment1
|
||||
b = 11 # trailing comment2
|
||||
|
||||
|
||||
# try/except*, mostly the same as try
|
||||
try: # try
|
||||
pass
|
||||
# end of body
|
||||
# before except
|
||||
except* (Exception, ValueError) as exc: # except line
|
||||
pass
|
||||
# before except 2
|
||||
except* KeyError as key: # except line 2
|
||||
pass
|
||||
# in body 2
|
||||
# before else
|
||||
else:
|
||||
pass
|
||||
# before finally
|
||||
finally:
|
||||
pass
|
||||
|
||||
# try and try star are statements with body
|
||||
# Minimized from https://github.com/python/cpython/blob/99b00efd5edfd5b26bf9e2a35cbfc96277fdcbb1/Lib/getpass.py#L68-L91
|
||||
try:
|
||||
try:
|
||||
pass
|
||||
finally:
|
||||
print(1) # issue7208
|
||||
except A:
|
||||
pass
|
||||
|
||||
try:
|
||||
f() # end-of-line last comment
|
||||
except RuntimeError:
|
||||
raise
|
||||
|
||||
try:
|
||||
|
||||
def f():
|
||||
pass
|
||||
# a
|
||||
except:
|
||||
|
||||
def f():
|
||||
pass
|
||||
# b
|
||||
else:
|
||||
|
||||
def f():
|
||||
pass
|
||||
# c
|
||||
finally:
|
||||
|
||||
def f():
|
||||
pass
|
||||
# d
|
||||
|
||||
|
||||
try:
|
||||
pass # a
|
||||
except ZeroDivisionError:
|
||||
pass # b
|
||||
except:
|
||||
pass # c
|
||||
else:
|
||||
pass # d
|
||||
finally:
|
||||
pass # e
|
||||
|
||||
try: # 1 preceding: any, following: first in body, enclosing: try
|
||||
print(1) # 2 preceding: last in body, following: fist in alt body, enclosing: try
|
||||
except (
|
||||
ZeroDivisionError
|
||||
): # 3 preceding: test, following: fist in alt body, enclosing: try
|
||||
print(2) # 4 preceding: last in body, following: fist in alt body, enclosing: exc
|
||||
except: # 5 preceding: last in body, following: fist in alt body, enclosing: try
|
||||
print(2) # 6 preceding: last in body, following: fist in alt body, enclosing: exc
|
||||
else: # 7 preceding: last in body, following: fist in alt body, enclosing: exc
|
||||
print(3) # 8 preceding: last in body, following: fist in alt body, enclosing: try
|
||||
finally: # 9 preceding: last in body, following: fist in alt body, enclosing: try
|
||||
print(3) # 10 preceding: last in body, following: any, enclosing: try
|
||||
|
||||
try:
|
||||
pass
|
||||
except (
|
||||
ZeroDivisionError
|
||||
# comment
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
|
||||
except ZeroDivisonError:
|
||||
pass
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
finally:
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
# These parens can be removed on 3.14+ but not earlier
|
||||
except (BaseException, Exception, ValueError):
|
||||
pass
|
||||
# But black won't remove these parentheses
|
||||
except (ZeroDivisionError,):
|
||||
pass
|
||||
except ( # We wrap these and preserve the parens
|
||||
BaseException,
|
||||
Exception,
|
||||
ValueError,
|
||||
):
|
||||
pass
|
||||
except (
|
||||
BaseException,
|
||||
# Same with this comment
|
||||
Exception,
|
||||
ValueError,
|
||||
):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
# They can also be omitted for `except*`
|
||||
except* (BaseException, Exception, ValueError):
|
||||
pass
|
||||
|
||||
# But parentheses are still required in the presence of an `as` binding
|
||||
try:
|
||||
pass
|
||||
except (BaseException, Exception, ValueError) as e:
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except* (BaseException, Exception, ValueError) as e:
|
||||
pass
|
||||
```
|
||||
|
||||
|
||||
#### Preview changes
|
||||
```diff
|
||||
--- Stable
|
||||
+++ Preview
|
||||
@@ -117,16 +117,19 @@
|
||||
def f():
|
||||
pass
|
||||
# a
|
||||
+
|
||||
except:
|
||||
|
||||
def f():
|
||||
pass
|
||||
# b
|
||||
+
|
||||
else:
|
||||
|
||||
def f():
|
||||
pass
|
||||
# c
|
||||
+
|
||||
finally:
|
||||
|
||||
def f():
|
||||
@@ -190,7 +193,7 @@
|
||||
try:
|
||||
pass
|
||||
# These parens can be removed on 3.14+ but not earlier
|
||||
-except (BaseException, Exception, ValueError):
|
||||
+except BaseException, Exception, ValueError:
|
||||
pass
|
||||
# But black won't remove these parentheses
|
||||
except (ZeroDivisionError,):
|
||||
@@ -212,7 +215,7 @@
|
||||
try:
|
||||
pass
|
||||
# They can also be omitted for `except*`
|
||||
-except* (BaseException, Exception, ValueError):
|
||||
+except* BaseException, Exception, ValueError:
|
||||
pass
|
||||
|
||||
# But parentheses are still required in the presence of an `as` binding
|
||||
```
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue