Skip to content

Commit ca1f81f

Browse files
Avoid raising bare Exception (#1168)
* Keep old exception messages (avoid breaking-changes for users relying on exception messages) * Move ``get_expected_str`` out of _exceptions.py, where it does not belong, to its own file in _parser/_parsing_check.py
1 parent e12eef5 commit ca1f81f

39 files changed

Lines changed: 305 additions & 215 deletions

libcst/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# LICENSE file in the root directory of this source tree.
55

66
from libcst._batched_visitor import BatchableCSTVisitor, visit_batched
7-
from libcst._exceptions import MetadataException, ParserSyntaxError
7+
from libcst._exceptions import CSTLogicError, MetadataException, ParserSyntaxError
88
from libcst._flatten_sentinel import FlattenSentinel
99
from libcst._maybe_sentinel import MaybeSentinel
1010
from libcst._metadata_dependent import MetadataDependent
@@ -242,6 +242,7 @@
242242
"CSTVisitorT",
243243
"FlattenSentinel",
244244
"MaybeSentinel",
245+
"CSTLogicError",
245246
"MetadataException",
246247
"ParserSyntaxError",
247248
"PartialParserConfig",

libcst/_exceptions.py

Lines changed: 5 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,59 +4,22 @@
44
# LICENSE file in the root directory of this source tree.
55

66
from enum import auto, Enum
7-
from typing import Any, Callable, final, Iterable, Optional, Sequence, Tuple, Union
7+
from typing import Any, Callable, final, Optional, Sequence, Tuple
88

9-
from libcst._parser.parso.pgen2.generator import ReservedString
10-
from libcst._parser.parso.python.token import PythonTokenTypes, TokenType
11-
from libcst._parser.types.token import Token
129
from libcst._tabs import expand_tabs
1310

14-
_EOF_STR: str = "end of file (EOF)"
15-
_INDENT_STR: str = "an indent"
16-
_DEDENT_STR: str = "a dedent"
11+
1712
_NEWLINE_CHARS: str = "\r\n"
1813

1914

2015
class EOFSentinel(Enum):
2116
EOF = auto()
2217

2318

24-
def get_expected_str(
25-
encountered: Union[Token, EOFSentinel],
26-
expected: Union[Iterable[Union[TokenType, ReservedString]], EOFSentinel],
27-
) -> str:
28-
if (
29-
isinstance(encountered, EOFSentinel)
30-
or encountered.type is PythonTokenTypes.ENDMARKER
31-
):
32-
encountered_str = _EOF_STR
33-
elif encountered.type is PythonTokenTypes.INDENT:
34-
encountered_str = _INDENT_STR
35-
elif encountered.type is PythonTokenTypes.DEDENT:
36-
encountered_str = _DEDENT_STR
37-
else:
38-
encountered_str = repr(encountered.string)
39-
40-
if isinstance(expected, EOFSentinel):
41-
expected_names = [_EOF_STR]
42-
else:
43-
expected_names = sorted(
44-
[
45-
repr(el.name) if isinstance(el, TokenType) else repr(el.value)
46-
for el in expected
47-
]
48-
)
19+
class CSTLogicError(Exception):
20+
"""General purpose internal error within LibCST itself."""
4921

50-
if len(expected_names) > 10:
51-
# There's too many possibilities, so it's probably not useful to list them.
52-
# Instead, let's just abbreviate the message.
53-
return f"Unexpectedly encountered {encountered_str}."
54-
else:
55-
if len(expected_names) == 1:
56-
expected_str = expected_names[0]
57-
else:
58-
expected_str = f"{', '.join(expected_names[:-1])}, or {expected_names[-1]}"
59-
return f"Encountered {encountered_str}, but expected {expected_str}."
22+
pass
6023

6124

6225
# pyre-fixme[2]: 'Any' type isn't pyre-strict.

libcst/_nodes/base.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from dataclasses import dataclass, field, fields, replace
99
from typing import Any, cast, ClassVar, Dict, List, Mapping, Sequence, TypeVar, Union
1010

11+
from libcst import CSTLogicError
1112
from libcst._flatten_sentinel import FlattenSentinel
1213
from libcst._nodes.internal import CodegenState
1314
from libcst._removal_sentinel import RemovalSentinel
@@ -237,7 +238,7 @@ def visit(
237238

238239
# validate return type of the user-defined `visitor.on_leave` method
239240
if not isinstance(leave_result, (CSTNode, RemovalSentinel, FlattenSentinel)):
240-
raise Exception(
241+
raise CSTValidationError(
241242
"Expected a node of type CSTNode or a RemovalSentinel, "
242243
+ f"but got a return value of {type(leave_result).__name__}"
243244
)
@@ -382,7 +383,7 @@ def deep_replace(
382383
new_tree = self.visit(_ChildReplacementTransformer(old_node, new_node))
383384
if isinstance(new_tree, (FlattenSentinel, RemovalSentinel)):
384385
# The above transform never returns *Sentinel, so this isn't possible
385-
raise Exception("Logic error, cannot get a *Sentinel here!")
386+
raise CSTLogicError("Logic error, cannot get a *Sentinel here!")
386387
return new_tree
387388

388389
def deep_remove(
@@ -399,7 +400,7 @@ def deep_remove(
399400

400401
if isinstance(new_tree, FlattenSentinel):
401402
# The above transform never returns FlattenSentinel, so this isn't possible
402-
raise Exception("Logic error, cannot get a FlattenSentinel here!")
403+
raise CSTLogicError("Logic error, cannot get a FlattenSentinel here!")
403404

404405
return new_tree
405406

@@ -421,7 +422,7 @@ def with_deep_changes(
421422
new_tree = self.visit(_ChildWithChangesTransformer(old_node, changes))
422423
if isinstance(new_tree, (FlattenSentinel, RemovalSentinel)):
423424
# This is impossible with the above transform.
424-
raise Exception("Logic error, cannot get a *Sentinel here!")
425+
raise CSTLogicError("Logic error, cannot get a *Sentinel here!")
425426
return new_tree
426427

427428
def __eq__(self: _CSTNodeSelfT, other: object) -> bool:

libcst/_nodes/expression.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
)
1818
from typing import Callable, Generator, Literal, Optional, Sequence, Union
1919

20+
from libcst import CSTLogicError
21+
2022
from libcst._add_slots import add_slots
2123
from libcst._maybe_sentinel import MaybeSentinel
2224
from libcst._nodes.base import CSTCodegenError, CSTNode, CSTValidationError
@@ -666,7 +668,7 @@ def quote(self) -> StringQuoteLiteral:
666668
if len(quote) not in {1, 3}:
667669
# We shouldn't get here due to construction validation logic,
668670
# but handle the case anyway.
669-
raise Exception(f"Invalid string {self.value}")
671+
raise CSTLogicError(f"Invalid string {self.value}")
670672

671673
# pyre-ignore We know via the above validation that we will only
672674
# ever return one of the four string literals.
@@ -1010,7 +1012,7 @@ def _validate(self) -> None:
10101012
elif isinstance(right, FormattedString):
10111013
rightbytes = "b" in right.prefix
10121014
else:
1013-
raise Exception("Logic error!")
1015+
raise CSTLogicError("Logic error!")
10141016
if leftbytes != rightbytes:
10151017
raise CSTValidationError("Cannot concatenate string and bytes.")
10161018

@@ -1688,7 +1690,7 @@ def _codegen_impl(
16881690
if default_indicator == "->":
16891691
state.add_token(" ")
16901692
else:
1691-
raise Exception("Logic error!")
1693+
raise CSTLogicError("Logic error!")
16921694

16931695
# Now, output the indicator and the rest of the annotation
16941696
state.add_token(default_indicator)

libcst/_nodes/statement.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from dataclasses import dataclass, field
1010
from typing import Literal, Optional, Pattern, Sequence, Union
1111

12+
from libcst import CSTLogicError
13+
1214
from libcst._add_slots import add_slots
1315
from libcst._maybe_sentinel import MaybeSentinel
1416
from libcst._nodes.base import CSTNode, CSTValidationError
@@ -1165,12 +1167,10 @@ def _validate(self) -> None:
11651167
)
11661168
try:
11671169
self.evaluated_name
1168-
except Exception as e:
1169-
if str(e) == "Logic error!":
1170-
raise CSTValidationError(
1171-
"The imported name must be a valid qualified name."
1172-
)
1173-
raise e
1170+
except CSTLogicError as e:
1171+
raise CSTValidationError(
1172+
"The imported name must be a valid qualified name."
1173+
) from e
11741174

11751175
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "ImportAlias":
11761176
return ImportAlias(
@@ -1199,7 +1199,7 @@ def _name(self, node: CSTNode) -> str:
11991199
elif isinstance(node, Attribute):
12001200
return f"{self._name(node.value)}.{node.attr.value}"
12011201
else:
1202-
raise Exception("Logic error!")
1202+
raise CSTLogicError("Logic error!")
12031203

12041204
@property
12051205
def evaluated_name(self) -> str:

libcst/_nodes/tests/test_funcdef.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1052,7 +1052,9 @@ def _parse_statement_force_38(code: str) -> cst.BaseCompoundStatement:
10521052
code, config=cst.PartialParserConfig(python_version="3.8")
10531053
)
10541054
if not isinstance(statement, cst.BaseCompoundStatement):
1055-
raise Exception("This function is expecting to parse compound statements only!")
1055+
raise ValueError(
1056+
"This function is expecting to parse compound statements only!"
1057+
)
10561058
return statement
10571059

10581060

libcst/_nodes/tests/test_namedexpr.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ def _parse_statement_force_38(code: str) -> cst.BaseCompoundStatement:
2222
code, config=cst.PartialParserConfig(python_version="3.8")
2323
)
2424
if not isinstance(statement, cst.BaseCompoundStatement):
25-
raise Exception("This function is expecting to parse compound statements only!")
25+
raise ValueError(
26+
"This function is expecting to parse compound statements only!"
27+
)
2628
return statement
2729

2830

libcst/_nodes/tests/test_removal_behavior.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ def test_removal_pass_behavior(
9595
self, before: str, after: str, visitor: Type[CSTTransformer]
9696
) -> None:
9797
if before.endswith("\n") or after.endswith("\n"):
98-
raise Exception("Test cases should not be newline-terminated!")
98+
raise ValueError("Test cases should not be newline-terminated!")
9999

100100
# Test doesn't have newline termination case
101101
before_module = parse_module(before)

libcst/_parser/_parsing_check.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
#
3+
# This source code is licensed under the MIT license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
from typing import Iterable, Union
7+
8+
from libcst._exceptions import EOFSentinel
9+
from libcst._parser.parso.pgen2.generator import ReservedString
10+
from libcst._parser.parso.python.token import PythonTokenTypes, TokenType
11+
from libcst._parser.types.token import Token
12+
13+
_EOF_STR: str = "end of file (EOF)"
14+
_INDENT_STR: str = "an indent"
15+
_DEDENT_STR: str = "a dedent"
16+
17+
18+
def get_expected_str(
19+
encountered: Union[Token, EOFSentinel],
20+
expected: Union[Iterable[Union[TokenType, ReservedString]], EOFSentinel],
21+
) -> str:
22+
if (
23+
isinstance(encountered, EOFSentinel)
24+
or encountered.type is PythonTokenTypes.ENDMARKER
25+
):
26+
encountered_str = _EOF_STR
27+
elif encountered.type is PythonTokenTypes.INDENT:
28+
encountered_str = _INDENT_STR
29+
elif encountered.type is PythonTokenTypes.DEDENT:
30+
encountered_str = _DEDENT_STR
31+
else:
32+
encountered_str = repr(encountered.string)
33+
34+
if isinstance(expected, EOFSentinel):
35+
expected_names = [_EOF_STR]
36+
else:
37+
expected_names = sorted(
38+
[
39+
repr(el.name) if isinstance(el, TokenType) else repr(el.value)
40+
for el in expected
41+
]
42+
)
43+
44+
if len(expected_names) > 10:
45+
# There's too many possibilities, so it's probably not useful to list them.
46+
# Instead, let's just abbreviate the message.
47+
return f"Unexpectedly encountered {encountered_str}."
48+
else:
49+
if len(expected_names) == 1:
50+
expected_str = expected_names[0]
51+
else:
52+
expected_str = f"{', '.join(expected_names[:-1])}, or {expected_names[-1]}"
53+
return f"Encountered {encountered_str}, but expected {expected_str}."

libcst/_parser/base_parser.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,8 @@
2626
from dataclasses import dataclass, field
2727
from typing import Generic, Iterable, List, Sequence, TypeVar, Union
2828

29-
from libcst._exceptions import (
30-
EOFSentinel,
31-
get_expected_str,
32-
ParserSyntaxError,
33-
PartialParserSyntaxError,
34-
)
29+
from libcst._exceptions import EOFSentinel, ParserSyntaxError, PartialParserSyntaxError
30+
from libcst._parser._parsing_check import get_expected_str
3531
from libcst._parser.parso.pgen2.generator import DFAState, Grammar, ReservedString
3632
from libcst._parser.parso.python.token import TokenType
3733
from libcst._parser.types.token import Token
@@ -103,7 +99,7 @@ def __init__(
10399
def parse(self) -> _NodeT:
104100
# Ensure that we don't re-use parsers.
105101
if self.__was_parse_called:
106-
raise Exception("Each parser object may only be used to parse once.")
102+
raise ValueError("Each parser object may only be used to parse once.")
107103
self.__was_parse_called = True
108104

109105
for token in self.tokens:

0 commit comments

Comments
 (0)