diff --git a/libcst/__init__.py b/libcst/__init__.py index 2a8e47b31..ef7224f07 100644 --- a/libcst/__init__.py +++ b/libcst/__init__.py @@ -4,6 +4,7 @@ # LICENSE file in the root directory of this source tree. from libcst._batched_visitor import BatchableCSTVisitor, visit_batched +from libcst._excep import CSTLogicError from libcst._exceptions import MetadataException, ParserSyntaxError from libcst._flatten_sentinel import FlattenSentinel from libcst._maybe_sentinel import MaybeSentinel @@ -238,6 +239,7 @@ "CSTNodeT", "CSTTransformer", "CSTValidationError", + "CSTLogicError", "CSTVisitor", "CSTVisitorT", "FlattenSentinel", diff --git a/libcst/_excep.py b/libcst/_excep.py new file mode 100644 index 000000000..87759a33c --- /dev/null +++ b/libcst/_excep.py @@ -0,0 +1,8 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + + +class CSTLogicError(Exception): + pass diff --git a/libcst/_nodes/base.py b/libcst/_nodes/base.py index d9689f8f8..2c5baa8f2 100644 --- a/libcst/_nodes/base.py +++ b/libcst/_nodes/base.py @@ -8,6 +8,7 @@ from dataclasses import dataclass, field, fields, replace from typing import Any, cast, ClassVar, Dict, List, Mapping, Sequence, TypeVar, Union +from libcst._excep import CSTLogicError from libcst._flatten_sentinel import FlattenSentinel from libcst._nodes.internal import CodegenState from libcst._removal_sentinel import RemovalSentinel @@ -237,7 +238,7 @@ def visit( # validate return type of the user-defined `visitor.on_leave` method if not isinstance(leave_result, (CSTNode, RemovalSentinel, FlattenSentinel)): - raise Exception( + raise CSTValidationError( "Expected a node of type CSTNode or a RemovalSentinel, " + f"but got a return value of {type(leave_result).__name__}" ) @@ -383,7 +384,7 @@ def deep_replace( new_tree = self.visit(_ChildReplacementTransformer(old_node, new_node)) if isinstance(new_tree, (FlattenSentinel, RemovalSentinel)): # The above transform never returns *Sentinel, so this isn't possible - raise Exception("Logic error, cannot get a *Sentinel here!") + raise CSTLogicError("Logic error, cannot get a *Sentinel here!") return new_tree def deep_remove( @@ -400,7 +401,7 @@ def deep_remove( if isinstance(new_tree, FlattenSentinel): # The above transform never returns FlattenSentinel, so this isn't possible - raise Exception("Logic error, cannot get a FlattenSentinel here!") + raise CSTLogicError("Logic error, cannot get a FlattenSentinel here!") return new_tree @@ -422,7 +423,7 @@ def with_deep_changes( new_tree = self.visit(_ChildWithChangesTransformer(old_node, changes)) if isinstance(new_tree, (FlattenSentinel, RemovalSentinel)): # This is impossible with the above transform. - raise Exception("Logic error, cannot get a *Sentinel here!") + raise CSTLogicError("Logic error, cannot get a *Sentinel here!") return new_tree def __eq__(self: _CSTNodeSelfT, other: object) -> bool: diff --git a/libcst/_nodes/expression.py b/libcst/_nodes/expression.py index 75f7da133..4a6e90e9c 100644 --- a/libcst/_nodes/expression.py +++ b/libcst/_nodes/expression.py @@ -18,6 +18,7 @@ from typing import Callable, Generator, Literal, Optional, Sequence, Union from libcst._add_slots import add_slots +from libcst._excep import CSTLogicError from libcst._maybe_sentinel import MaybeSentinel from libcst._nodes.base import CSTCodegenError, CSTNode, CSTValidationError from libcst._nodes.internal import ( @@ -666,7 +667,7 @@ def quote(self) -> StringQuoteLiteral: if len(quote) not in {1, 3}: # We shouldn't get here due to construction validation logic, # but handle the case anyway. - raise Exception(f"Invalid string {self.value}") + raise CSTLogicError(f"Invalid string {self.value}") # pyre-ignore We know via the above validation that we will only # ever return one of the four string literals. @@ -1010,7 +1011,7 @@ def _validate(self) -> None: elif isinstance(right, FormattedString): rightbytes = "b" in right.prefix else: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") if leftbytes != rightbytes: raise CSTValidationError("Cannot concatenate string and bytes.") @@ -1688,7 +1689,7 @@ def _codegen_impl( if default_indicator == "->": state.add_token(" ") else: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") # Now, output the indicator and the rest of the annotation state.add_token(default_indicator) diff --git a/libcst/_nodes/statement.py b/libcst/_nodes/statement.py index 1cb9221fe..cc1b76342 100644 --- a/libcst/_nodes/statement.py +++ b/libcst/_nodes/statement.py @@ -10,6 +10,7 @@ from typing import Literal, Optional, Pattern, Sequence, Union from libcst._add_slots import add_slots +from libcst._excep import CSTLogicError from libcst._maybe_sentinel import MaybeSentinel from libcst._nodes.base import CSTNode, CSTValidationError from libcst._nodes.expression import ( @@ -1161,12 +1162,10 @@ def _validate(self) -> None: ) try: self.evaluated_name - except Exception as e: - if str(e) == "Logic error!": - raise CSTValidationError( - "The imported name must be a valid qualified name." - ) - raise e + except CSTLogicError as e: + raise CSTValidationError( + "The imported name must be a valid qualified name." + ) from e def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "ImportAlias": return ImportAlias( @@ -1195,7 +1194,7 @@ def _name(self, node: CSTNode) -> str: elif isinstance(node, Attribute): return f"{self._name(node.value)}.{node.attr.value}" else: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") @property def evaluated_name(self) -> str: diff --git a/libcst/_nodes/tests/test_funcdef.py b/libcst/_nodes/tests/test_funcdef.py index 087dde19c..65a0ff07a 100644 --- a/libcst/_nodes/tests/test_funcdef.py +++ b/libcst/_nodes/tests/test_funcdef.py @@ -1052,7 +1052,9 @@ def _parse_statement_force_38(code: str) -> cst.BaseCompoundStatement: code, config=cst.PartialParserConfig(python_version="3.8") ) if not isinstance(statement, cst.BaseCompoundStatement): - raise Exception("This function is expecting to parse compound statements only!") + raise ValueError( + "This function is expecting to parse compound statements only!" + ) return statement diff --git a/libcst/_nodes/tests/test_namedexpr.py b/libcst/_nodes/tests/test_namedexpr.py index bddd4f3d2..6ebcf978d 100644 --- a/libcst/_nodes/tests/test_namedexpr.py +++ b/libcst/_nodes/tests/test_namedexpr.py @@ -22,7 +22,9 @@ def _parse_statement_force_38(code: str) -> cst.BaseCompoundStatement: code, config=cst.PartialParserConfig(python_version="3.8") ) if not isinstance(statement, cst.BaseCompoundStatement): - raise Exception("This function is expecting to parse compound statements only!") + raise ValueError( + "This function is expecting to parse compound statements only!" + ) return statement diff --git a/libcst/_nodes/tests/test_removal_behavior.py b/libcst/_nodes/tests/test_removal_behavior.py index 9b1bf6193..709b26f5b 100644 --- a/libcst/_nodes/tests/test_removal_behavior.py +++ b/libcst/_nodes/tests/test_removal_behavior.py @@ -95,7 +95,7 @@ def test_removal_pass_behavior( self, before: str, after: str, visitor: Type[CSTTransformer] ) -> None: if before.endswith("\n") or after.endswith("\n"): - raise Exception("Test cases should not be newline-terminated!") + raise ValueError("Test cases should not be newline-terminated!") # Test doesn't have newline termination case before_module = parse_module(before) diff --git a/libcst/_parser/base_parser.py b/libcst/_parser/base_parser.py index ef9e15192..8da4844d5 100644 --- a/libcst/_parser/base_parser.py +++ b/libcst/_parser/base_parser.py @@ -103,7 +103,7 @@ def __init__( def parse(self) -> _NodeT: # Ensure that we don't re-use parsers. if self.__was_parse_called: - raise Exception("Each parser object may only be used to parse once.") + raise ValueError("Each parser object may only be used to parse once.") self.__was_parse_called = True for token in self.tokens: diff --git a/libcst/_parser/conversions/expression.py b/libcst/_parser/conversions/expression.py index 1a46de2af..0ce0b92d5 100644 --- a/libcst/_parser/conversions/expression.py +++ b/libcst/_parser/conversions/expression.py @@ -12,7 +12,8 @@ Intnumber as INTNUMBER_RE, ) -from libcst._exceptions import PartialParserSyntaxError +from libcst._excep import CSTLogicError +from libcst._exceptions import ParserSyntaxError, PartialParserSyntaxError from libcst._maybe_sentinel import MaybeSentinel from libcst._nodes.expression import ( Arg, @@ -327,7 +328,12 @@ def convert_boolop( # Convert all of the operations that have no precedence in a loop for op, rightexpr in grouper(rightexprs, 2): if op.string not in BOOLOP_TOKEN_LUT: - raise Exception(f"Unexpected token '{op.string}'!") + raise ParserSyntaxError( + f"Unexpected token '{op.string}'!", + lines=config.lines, + raw_line=0, + raw_column=0, + ) leftexpr = BooleanOperation( left=leftexpr, # pyre-ignore Pyre thinks that the type of the LUT is CSTNode. @@ -420,7 +426,12 @@ def convert_comp_op( ) else: # this should be unreachable - raise Exception(f"Unexpected token '{op.string}'!") + raise ParserSyntaxError( + f"Unexpected token '{op.string}'!", + lines=config.lines, + raw_line=0, + raw_column=0, + ) else: # A two-token comparison leftcomp, rightcomp = children @@ -451,7 +462,12 @@ def convert_comp_op( ) else: # this should be unreachable - raise Exception(f"Unexpected token '{leftcomp.string} {rightcomp.string}'!") + raise ParserSyntaxError( + f"Unexpected token '{leftcomp.string} {rightcomp.string}'!", + lines=config.lines, + raw_line=0, + raw_column=0, + ) @with_production("star_expr", "'*' expr") @@ -493,7 +509,12 @@ def convert_binop( # Convert all of the operations that have no precedence in a loop for op, rightexpr in grouper(rightexprs, 2): if op.string not in BINOP_TOKEN_LUT: - raise Exception(f"Unexpected token '{op.string}'!") + raise ParserSyntaxError( + f"Unexpected token '{op.string}'!", + lines=config.lines, + raw_line=0, + raw_column=0, + ) leftexpr = BinaryOperation( left=leftexpr, # pyre-ignore Pyre thinks that the type of the LUT is CSTNode. @@ -540,7 +561,12 @@ def convert_factor( ) ) else: - raise Exception(f"Unexpected token '{op.string}'!") + raise ParserSyntaxError( + f"Unexpected token '{op.string}'!", + lines=config.lines, + raw_line=0, + raw_column=0, + ) return WithLeadingWhitespace( UnaryOperation(operator=opnode, expression=factor.value), op.whitespace_before @@ -651,7 +677,7 @@ def convert_atom_expr_trailer( ) else: # This is an invalid trailer, so lets give up - raise Exception("Logic error!") + raise CSTLogicError() return WithLeadingWhitespace(atom, whitespace_before) @@ -870,9 +896,19 @@ def convert_atom_basic( Imaginary(child.string), child.whitespace_before ) else: - raise Exception(f"Unparseable number {child.string}") + raise ParserSyntaxError( + f"Unparseable number {child.string}", + lines=config.lines, + raw_line=0, + raw_column=0, + ) else: - raise Exception(f"Logic error, unexpected token {child.type.name}") + raise ParserSyntaxError( + f"Logic error, unexpected token {child.type.name}", + lines=config.lines, + raw_line=0, + raw_column=0, + ) @with_production("atom_squarebrackets", "'[' [testlist_comp_list] ']'") @@ -1447,7 +1483,7 @@ def convert_arg_assign_comp_for( if equal.string == ":=": val = convert_namedexpr_test(config, children) if not isinstance(val, WithLeadingWhitespace): - raise Exception( + raise TypeError( f"convert_namedexpr_test returned {val!r}, not WithLeadingWhitespace" ) return Arg(value=val.value) diff --git a/libcst/_parser/conversions/params.py b/libcst/_parser/conversions/params.py index 9ac7f1d16..ba9bb5610 100644 --- a/libcst/_parser/conversions/params.py +++ b/libcst/_parser/conversions/params.py @@ -6,6 +6,7 @@ from typing import Any, List, Optional, Sequence, Union +from libcst._excep import CSTLogicError from libcst._exceptions import PartialParserSyntaxError from libcst._maybe_sentinel import MaybeSentinel from libcst._nodes.expression import ( @@ -121,7 +122,7 @@ def add_param( # Example code: # def fn(*abc, *): ... # This should be unreachable, the grammar already disallows it. - raise Exception( + raise ValueError( "Cannot have multiple star ('*') markers in a single argument " + "list." ) @@ -136,7 +137,7 @@ def add_param( # Example code: # def fn(foo, /, *, /, bar): ... # This should be unreachable, the grammar already disallows it. - raise Exception( + raise ValueError( "Cannot have multiple slash ('/') markers in a single argument " + "list." ) @@ -168,7 +169,7 @@ def add_param( # Example code: # def fn(**kwargs, trailing=None) # This should be unreachable, the grammar already disallows it. - raise Exception("Cannot have any arguments after a kwargs expansion.") + raise ValueError("Cannot have any arguments after a kwargs expansion.") elif ( isinstance(param.star, str) and param.star == "*" and param.default is None ): @@ -181,7 +182,7 @@ def add_param( # Example code: # def fn(*first, *second): ... # This should be unreachable, the grammar already disallows it. - raise Exception( + raise ValueError( "Expected a keyword argument but found a starred positional " + "argument expansion." ) @@ -197,13 +198,13 @@ def add_param( # Example code: # def fn(**first, **second) # This should be unreachable, the grammar already disallows it. - raise Exception( + raise ValueError( "Multiple starred keyword argument expansions are not allowed in a " + "single argument list" ) else: # The state machine should never end up here. - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") return current_param diff --git a/libcst/_parser/conversions/statement.py b/libcst/_parser/conversions/statement.py index 608f002f0..a98038805 100644 --- a/libcst/_parser/conversions/statement.py +++ b/libcst/_parser/conversions/statement.py @@ -6,7 +6,8 @@ from typing import Any, Dict, List, Optional, Sequence, Tuple, Type -from libcst._exceptions import PartialParserSyntaxError +from libcst._excep import CSTLogicError +from libcst._exceptions import ParserSyntaxError, PartialParserSyntaxError from libcst._maybe_sentinel import MaybeSentinel from libcst._nodes.expression import ( Annotation, @@ -283,7 +284,9 @@ def convert_annassign(config: ParserConfig, children: Sequence[Any]) -> Any: whitespace_after=parse_simple_whitespace(config, equal.whitespace_after), ) else: - raise Exception("Invalid parser state!") + raise ParserSyntaxError( + "Invalid parser state!", lines=config.lines, raw_line=0, raw_column=0 + ) return AnnAssignPartial( annotation=Annotation( @@ -319,7 +322,13 @@ def convert_annassign(config: ParserConfig, children: Sequence[Any]) -> Any: def convert_augassign(config: ParserConfig, children: Sequence[Any]) -> Any: op, expr = children if op.string not in AUGOP_TOKEN_LUT: - raise Exception(f"Unexpected token '{op.string}'!") + raise ParserSyntaxError( + f"Unexpected token '{op.string}'!", + lines=config.lines, + raw_line=0, + raw_column=0, + ) + return AugAssignPartial( # pyre-ignore Pyre seems to think that the value of this LUT is CSTNode operator=AUGOP_TOKEN_LUT[op.string]( @@ -447,7 +456,7 @@ def convert_import_relative(config: ParserConfig, children: Sequence[Any]) -> An # This should be the dotted name, and we can't get more than # one, but lets be sure anyway if dotted_name is not None: - raise Exception("Logic error!") + raise CSTLogicError() dotted_name = child return ImportRelativePartial(relative=tuple(dots), module=dotted_name) @@ -644,7 +653,7 @@ def convert_raise_stmt(config: ParserConfig, children: Sequence[Any]) -> Any: item=source.value, ) else: - raise Exception("Logic error!") + raise CSTLogicError() return WithLeadingWhitespace( Raise(whitespace_after_raise=whitespace_after_raise, exc=exc, cause=cause), @@ -893,7 +902,7 @@ def convert_try_stmt(config: ParserConfig, children: Sequence[Any]) -> Any: if isinstance(clause, Token): if clause.string == "else": if orelse is not None: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") orelse = Else( leading_lines=parse_empty_lines(config, clause.whitespace_before), whitespace_before_colon=parse_simple_whitespace( @@ -903,7 +912,7 @@ def convert_try_stmt(config: ParserConfig, children: Sequence[Any]) -> Any: ) elif clause.string == "finally": if finalbody is not None: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") finalbody = Finally( leading_lines=parse_empty_lines(config, clause.whitespace_before), whitespace_before_colon=parse_simple_whitespace( @@ -912,7 +921,7 @@ def convert_try_stmt(config: ParserConfig, children: Sequence[Any]) -> Any: body=suite, ) else: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") elif isinstance(clause, ExceptClausePartial): handlers.append( ExceptHandler( @@ -927,7 +936,7 @@ def convert_try_stmt(config: ParserConfig, children: Sequence[Any]) -> Any: ) ) else: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") return Try( leading_lines=parse_empty_lines(config, trytoken.whitespace_before), @@ -1333,7 +1342,7 @@ def convert_asyncable_stmt(config: ParserConfig, children: Sequence[Any]) -> Any asynchronous=asyncnode, leading_lines=leading_lines ) else: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") @with_production("suite", "simple_stmt_suite | indented_suite") diff --git a/libcst/_parser/grammar.py b/libcst/_parser/grammar.py index 8e6ade59a..ee65ef72f 100644 --- a/libcst/_parser/grammar.py +++ b/libcst/_parser/grammar.py @@ -319,7 +319,7 @@ def validate_grammar() -> None: production_name = fn_productions[0].name expected_name = f"convert_{production_name}" if fn.__name__ != expected_name: - raise Exception( + raise ValueError( f"The conversion function for '{production_name}' " + f"must be called '{expected_name}', not '{fn.__name__}'." ) @@ -330,7 +330,7 @@ def _get_version_comparison(version: str) -> Tuple[str, PythonVersionInfo]: return (version[:2], parse_version_string(version[2:].strip())) if version[:1] in (">", "<"): return (version[:1], parse_version_string(version[1:].strip())) - raise Exception(f"Invalid version comparison specifier '{version}'") + raise ValueError(f"Invalid version comparison specifier '{version}'") def _compare_versions( @@ -350,7 +350,7 @@ def _compare_versions( return actual_version > requested_version if comparison == "<": return actual_version < requested_version - raise Exception(f"Invalid version comparison specifier '{comparison}'") + raise ValueError(f"Invalid version comparison specifier '{comparison}'") def _should_include( @@ -405,7 +405,7 @@ def get_nonterminal_conversions( if not _should_include_future(fn_production.future, future_imports): continue if fn_production.name in conversions: - raise Exception( + raise ValueError( f"Found duplicate '{fn_production.name}' production in grammar" ) conversions[fn_production.name] = fn diff --git a/libcst/_parser/parso/pgen2/generator.py b/libcst/_parser/parso/pgen2/generator.py index 4e20e89d7..69d6e9415 100644 --- a/libcst/_parser/parso/pgen2/generator.py +++ b/libcst/_parser/parso/pgen2/generator.py @@ -259,7 +259,7 @@ def generate_grammar(bnf_grammar: str, token_namespace: Any) -> Grammar[Any]: _calculate_tree_traversal(rule_to_dfas) if start_nonterminal is None: - raise Exception("could not find starting nonterminal!") + raise ValueError("could not find starting nonterminal!") return Grammar(start_nonterminal, rule_to_dfas, reserved_strings) diff --git a/libcst/_parser/parso/python/tokenize.py b/libcst/_parser/parso/python/tokenize.py index bfd159dda..1f57fbff2 100644 --- a/libcst/_parser/parso/python/tokenize.py +++ b/libcst/_parser/parso/python/tokenize.py @@ -36,6 +36,7 @@ from dataclasses import dataclass from typing import Dict, Generator, Iterable, Optional, Pattern, Set, Tuple +from libcst._excep import CSTLogicError from libcst._parser.parso.python.token import PythonTokenTypes from libcst._parser.parso.utils import PythonVersionInfo, split_lines @@ -522,14 +523,14 @@ def dedent_if_necessary(start): if contstr: # continued string if endprog is None: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") endmatch = endprog.match(line) if endmatch: pos = endmatch.end(0) if contstr_start is None: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") if stashed is not None: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") yield PythonToken(STRING, contstr + line[:pos], contstr_start, prefix) contstr = "" contline = None @@ -547,7 +548,7 @@ def dedent_if_necessary(start): ) if string: if stashed is not None: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") yield PythonToken( FSTRING_STRING, string, @@ -572,7 +573,7 @@ def dedent_if_necessary(start): pos += quote_length if fstring_end_token is not None: if stashed is not None: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") yield fstring_end_token continue @@ -885,12 +886,12 @@ def dedent_if_necessary(start): if contstr: # continued string if endprog is None: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") endmatch = endprog.match(line) if endmatch: pos = endmatch.end(0) if contstr_start is None: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") yield PythonToken(STRING, contstr + line[:pos], contstr_start, prefix) contstr = "" contline = None diff --git a/libcst/_parser/production_decorator.py b/libcst/_parser/production_decorator.py index 41a817f8f..d5ba52deb 100644 --- a/libcst/_parser/production_decorator.py +++ b/libcst/_parser/production_decorator.py @@ -39,7 +39,7 @@ def inner(fn: _NonterminalConversionT) -> _NonterminalConversionT: # pyre-ignore: Pyre doesn't think that fn has a __name__ attribute fn_name = fn.__name__ if not fn_name.startswith("convert_"): - raise Exception( + raise ValueError( "A function with a production must be named 'convert_X', not " + f"'{fn_name}'." ) diff --git a/libcst/_parser/py_whitespace_parser.py b/libcst/_parser/py_whitespace_parser.py index b1fd9b5ed..6b6573a65 100644 --- a/libcst/_parser/py_whitespace_parser.py +++ b/libcst/_parser/py_whitespace_parser.py @@ -5,6 +5,7 @@ from typing import List, Optional, Sequence, Tuple, Union +from libcst import CSTLogicError, ParserSyntaxError from libcst._nodes.whitespace import ( Comment, COMMENT_RE, @@ -103,10 +104,13 @@ def parse_trailing_whitespace( ) -> TrailingWhitespace: trailing_whitespace = _parse_trailing_whitespace(config, state) if trailing_whitespace is None: - raise Exception( + raise ParserSyntaxError( "Internal Error: Failed to parse TrailingWhitespace. This should never " + "happen because a TrailingWhitespace is never optional in the grammar, " - + "so this error should've been caught by parso first." + + "so this error should've been caught by parso first.", + lines=config.lines, + raw_line=state.line, + raw_column=state.column, ) return trailing_whitespace @@ -177,7 +181,9 @@ def _parse_indent( if state.column == len(line_str) and state.line == len(config.lines): # We're at EOF, treat this as a failed speculative parse return False - raise Exception("Internal Error: Column should be 0 when parsing an indent.") + raise CSTLogicError( + "Internal Error: Column should be 0 when parsing an indent." + ) if line_str.startswith(absolute_indent, state.column): state.column += len(absolute_indent) return True @@ -206,7 +212,12 @@ def _parse_newline( newline_str = newline_match.group(0) state.column += len(newline_str) if state.column != len(line_str): - raise Exception("Internal Error: Found a newline, but it wasn't the EOL.") + raise ParserSyntaxError( + "Internal Error: Found a newline, but it wasn't the EOL.", + lines=config.lines, + raw_line=state.line, + raw_column=state.column, + ) if state.line < len(config.lines): # this newline was the end of a line, and there's another line, # therefore we should move to the next line diff --git a/libcst/codegen/gen_matcher_classes.py b/libcst/codegen/gen_matcher_classes.py index b7940f973..3e91f92b7 100644 --- a/libcst/codegen/gen_matcher_classes.py +++ b/libcst/codegen/gen_matcher_classes.py @@ -8,7 +8,7 @@ from typing import Generator, List, Optional, Sequence, Set, Tuple, Type, Union import libcst as cst -from libcst import ensure_type, parse_expression +from libcst import CSTLogicError, ensure_type, parse_expression from libcst.codegen.gather import all_libcst_nodes, typeclasses CST_DIR: Set[str] = set(dir(cst)) @@ -180,9 +180,9 @@ def visit_Subscript(self, node: cst.Subscript) -> None: # type blocks, even for sequence types. return if len(node.slice) != 1: - raise Exception( + raise ValueError( "Unexpected number of sequence elements inside Sequence type " - + "annotation!" + "annotation!" ) nodeslice = node.slice[0].slice if isinstance(nodeslice, cst.Index): @@ -346,10 +346,14 @@ def _get_clean_type_from_subscript( if typecst.value.deep_equals(cst.Name("Sequence")): # Lets attempt to widen the sequence type and alias it. if len(typecst.slice) != 1: - raise Exception("Logic error, Sequence shouldn't have more than one param!") + raise CSTLogicError( + "Logic error, Sequence shouldn't have more than one param!" + ) inner_type = typecst.slice[0].slice if not isinstance(inner_type, cst.Index): - raise Exception("Logic error, expecting Index for only Sequence element!") + raise CSTLogicError( + "Logic error, expecting Index for only Sequence element!" + ) inner_type = inner_type.value if isinstance(inner_type, cst.Subscript): @@ -357,7 +361,7 @@ def _get_clean_type_from_subscript( elif isinstance(inner_type, (cst.Name, cst.SimpleString)): clean_inner_type = _get_clean_type_from_expression(aliases, inner_type) else: - raise Exception("Logic error, unexpected type in Sequence!") + raise CSTLogicError("Logic error, unexpected type in Sequence!") return _get_wrapped_union_type( typecst.deep_replace(inner_type, clean_inner_type), @@ -397,7 +401,7 @@ def _get_clean_type_and_aliases( elif isinstance(typecst, (cst.Name, cst.SimpleString)): clean_type = _get_clean_type_from_expression(aliases, typecst) else: - raise Exception("Logic error, unexpected top level type!") + raise CSTLogicError("Logic error, unexpected top level type!") # Now, insert OneOf/AllOf and MatchIfTrue into unions so we can typecheck their usage. # This allows us to put OneOf[SomeType] or MatchIfTrue[cst.SomeType] into any diff --git a/libcst/codemod/_cli.py b/libcst/codemod/_cli.py index 8bfc11f89..72a4abc64 100644 --- a/libcst/codemod/_cli.py +++ b/libcst/codemod/_cli.py @@ -47,7 +47,7 @@ def invoke_formatter(formatter_args: Sequence[str], code: AnyStr) -> AnyStr: # Make sure there is something to run if len(formatter_args) == 0: - raise Exception("No formatter configured but code formatting requested.") + raise ValueError("No formatter configured but code formatting requested.") # Invoke the formatter, giving it the code as stdin and assuming the formatted # code comes from stdout. @@ -574,7 +574,7 @@ def parallel_exec_transform_with_prettyprint( # noqa: C901 ) if jobs < 1: - raise Exception("Must have at least one job to process!") + raise ValueError("Must have at least one job to process!") if total == 0: return ParallelTransformResult(successes=0, failures=0, skips=0, warnings=0) diff --git a/libcst/codemod/_codemod.py b/libcst/codemod/_codemod.py index c0c3b2c76..e267f1545 100644 --- a/libcst/codemod/_codemod.py +++ b/libcst/codemod/_codemod.py @@ -56,9 +56,9 @@ def module(self) -> Module: """ module = self.context.module if module is None: - raise Exception( + raise ValueError( f"Attempted access of {self.__class__.__name__}.module outside of " - + "transform_module()." + "transform_module()." ) return module diff --git a/libcst/codemod/_visitor.py b/libcst/codemod/_visitor.py index ab915c493..892488387 100644 --- a/libcst/codemod/_visitor.py +++ b/libcst/codemod/_visitor.py @@ -6,7 +6,7 @@ from typing import Mapping import libcst as cst -from libcst import MetadataDependent +from libcst import MetadataDependent, MetadataException from libcst.codemod._codemod import Codemod from libcst.codemod._context import CodemodContext from libcst.matchers import MatcherDecoratableTransformer, MatcherDecoratableVisitor @@ -69,14 +69,14 @@ def __init__(self, context: CodemodContext) -> None: if dependencies: wrapper = self.context.wrapper if wrapper is None: - raise Exception( + raise MetadataException( f"Attempting to instantiate {self.__class__.__name__} outside of " + "an active transform. This means that metadata hasn't been " + "calculated and we cannot successfully create this visitor." ) for dep in dependencies: if dep not in wrapper._metadata: - raise Exception( + raise MetadataException( f"Attempting to access metadata {dep.__name__} that was not a " + "declared dependency of parent transform! This means it is " + "not possible to compute this value. Please ensure that all " @@ -101,7 +101,7 @@ def module(self) -> cst.Module: """ module = self.context.module if module is None: - raise Exception( + raise ValueError( f"Attempted access of {self.__class__.__name__}.module outside of " + "transform_module()." ) diff --git a/libcst/codemod/commands/convert_format_to_fstring.py b/libcst/codemod/commands/convert_format_to_fstring.py index ab98c0ea0..f98d42e51 100644 --- a/libcst/codemod/commands/convert_format_to_fstring.py +++ b/libcst/codemod/commands/convert_format_to_fstring.py @@ -9,6 +9,8 @@ import libcst as cst import libcst.matchers as m +from libcst._excep import CSTLogicError +from libcst._exceptions import ParserSyntaxError from libcst.codemod import ( CodemodContext, ContextAwareTransformer, @@ -23,7 +25,7 @@ def _get_lhs(field: cst.BaseExpression) -> cst.BaseExpression: elif isinstance(field, (cst.Attribute, cst.Subscript)): return _get_lhs(field.value) else: - raise Exception("Unsupported node type!") + raise TypeError("Unsupported node type!") def _find_expr_from_field_name( @@ -48,7 +50,7 @@ def _find_expr_from_field_name( if isinstance(lhs, cst.Integer): index = int(lhs.value) if index < 0 or index >= len(args): - raise Exception(f"Logic error, arg sequence {index} out of bounds!") + raise CSTLogicError(f"Logic error, arg sequence {index} out of bounds!") elif isinstance(lhs, cst.Name): for i, arg in enumerate(args): kw = arg.keyword @@ -58,10 +60,12 @@ def _find_expr_from_field_name( index = i break if index is None: - raise Exception(f"Logic error, arg name {lhs.value} out of bounds!") + raise CSTLogicError(f"Logic error, arg name {lhs.value} out of bounds!") if index is None: - raise Exception(f"Logic error, unsupported fieldname expression {fieldname}!") + raise CSTLogicError( + f"Logic error, unsupported fieldname expression {fieldname}!" + ) # Format it! return field_expr.deep_replace(lhs, args[index].value) @@ -158,9 +162,11 @@ def _get_tokens( # noqa: C901 format_accum += char if in_brackets > 0: - raise Exception("Stray { in format string!") + raise ParserSyntaxError( + "Stray { in format string!", lines=[string], raw_line=0, raw_column=0 + ) if format_accum: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") # Yield the last bit of information yield (prefix, None, None, None) @@ -188,7 +194,7 @@ class SwitchStringQuotesTransformer(ContextAwareTransformer): def __init__(self, context: CodemodContext, avoid_quote: str) -> None: super().__init__(context) if avoid_quote not in {'"', "'"}: - raise Exception("Must specify either ' or \" single quote to avoid.") + raise ValueError("Must specify either ' or \" single quote to avoid.") self.avoid_quote: str = avoid_quote self.replace_quote: str = '"' if avoid_quote == "'" else "'" @@ -296,7 +302,7 @@ def leave_Call( # noqa: C901 ) in format_spec_tokens: if spec_format_spec is not None: # This shouldn't be possible, we don't allow it in the spec! - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") if spec_literal_text: format_spec_parts.append( cst.FormattedStringText(spec_literal_text) diff --git a/libcst/codemod/commands/convert_percent_format_to_fstring.py b/libcst/codemod/commands/convert_percent_format_to_fstring.py index 9908a5b65..aa0c65191 100644 --- a/libcst/codemod/commands/convert_percent_format_to_fstring.py +++ b/libcst/codemod/commands/convert_percent_format_to_fstring.py @@ -53,12 +53,12 @@ def leave_SimpleString( original_node.prefix + quo + original_node.raw_value + quo ) if escaped_string.evaluated_value != original_node.evaluated_value: - raise Exception( + raise ValueError( f"Failed to escape string:\n original:{original_node.value}\n escaped:{escaped_string.value}" ) else: return escaped_string - raise Exception( + raise ValueError( f"Cannot find a good quote for escaping the SimpleString: {original_node.value}" ) return original_node diff --git a/libcst/codemod/commands/fix_pyre_directives.py b/libcst/codemod/commands/fix_pyre_directives.py index c3ab41b7d..f90e7f375 100644 --- a/libcst/codemod/commands/fix_pyre_directives.py +++ b/libcst/codemod/commands/fix_pyre_directives.py @@ -7,6 +7,7 @@ import libcst import libcst.matchers as m +from libcst._excep import CSTLogicError from libcst.codemod import CodemodContext, VisitorBasedCodemodCommand from libcst.helpers import insert_header_comments @@ -29,12 +30,12 @@ def __init__(self, context: CodemodContext) -> None: def visit_Module_header(self, node: libcst.Module) -> None: if self.in_module_header: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") self.in_module_header = True def leave_Module_header(self, node: libcst.Module) -> None: if not self.in_module_header: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") self.in_module_header = False def leave_EmptyLine( diff --git a/libcst/codemod/commands/rename.py b/libcst/codemod/commands/rename.py index 4bd0ee3dc..861f6c776 100644 --- a/libcst/codemod/commands/rename.py +++ b/libcst/codemod/commands/rename.py @@ -121,7 +121,7 @@ def leave_Import( import_alias_name = import_alias.name import_alias_full_name = get_full_name_for_node(import_alias_name) if import_alias_full_name is None: - raise Exception("Could not parse full name for ImportAlias.name node.") + raise ValueError("Could not parse full name for ImportAlias.name node.") if isinstance(import_alias_name, cst.Name) and self.old_name.startswith( import_alias_full_name + "." @@ -249,7 +249,7 @@ def leave_Attribute( ) -> Union[cst.Name, cst.Attribute]: full_name_for_node = get_full_name_for_node(original_node) if full_name_for_node is None: - raise Exception("Could not parse full name for Attribute node.") + raise ValueError("Could not parse full name for Attribute node.") full_replacement_name = self.gen_replacement(full_name_for_node) # If a node has no associated QualifiedName, we are still inside an import statement. @@ -320,7 +320,7 @@ def gen_name_or_attr_node( ) -> Union[cst.Attribute, cst.Name]: name_or_attr_node: cst.BaseExpression = cst.parse_expression(dotted_expression) if not isinstance(name_or_attr_node, (cst.Name, cst.Attribute)): - raise Exception( + raise ValueError( "`parse_expression()` on dotted path returned non-Attribute-or-Name." ) return name_or_attr_node diff --git a/libcst/codemod/visitors/_add_imports.py b/libcst/codemod/visitors/_add_imports.py index f734af5cd..eeab43ae3 100644 --- a/libcst/codemod/visitors/_add_imports.py +++ b/libcst/codemod/visitors/_add_imports.py @@ -7,7 +7,7 @@ from typing import Dict, List, Optional, Sequence, Set, Tuple, Union import libcst -from libcst import matchers as m, parse_statement +from libcst import CSTLogicError, matchers as m, parse_statement from libcst._nodes.statement import Import, ImportFrom, SimpleStatementLine from libcst.codemod._context import CodemodContext from libcst.codemod._visitor import ContextAwareTransformer @@ -107,7 +107,7 @@ def _get_imports_from_context( ) -> List[ImportItem]: imports = context.scratch.get(AddImportsVisitor.CONTEXT_KEY, []) if not isinstance(imports, list): - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") return imports @staticmethod @@ -136,7 +136,7 @@ def add_needed_import( """ if module == "__future__" and obj is None: - raise Exception("Cannot import __future__ directly!") + raise ValueError("Cannot import __future__ directly!") imports = AddImportsVisitor._get_imports_from_context(context) imports.append(ImportItem(module, obj, asname, relative)) context.scratch[AddImportsVisitor.CONTEXT_KEY] = imports @@ -157,9 +157,9 @@ def __init__( # Verify that the imports are valid for imp in imps: if imp.module == "__future__" and imp.obj_name is None: - raise Exception("Cannot import __future__ directly!") + raise ValueError("Cannot import __future__ directly!") if imp.module == "__future__" and imp.alias is not None: - raise Exception("Cannot import __future__ objects with aliases!") + raise ValueError("Cannot import __future__ objects with aliases!") # Resolve relative imports if we have a module name imps = [imp.resolve_relative(self.context.full_package_name) for imp in imps] diff --git a/libcst/codemod/visitors/_remove_imports.py b/libcst/codemod/visitors/_remove_imports.py index 559401275..56d900dfc 100644 --- a/libcst/codemod/visitors/_remove_imports.py +++ b/libcst/codemod/visitors/_remove_imports.py @@ -6,6 +6,7 @@ from typing import Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union import libcst as cst +from libcst._excep import CSTLogicError from libcst.codemod._context import CodemodContext from libcst.codemod._visitor import ContextAwareTransformer, ContextAwareVisitor from libcst.codemod.visitors._gather_unused_imports import GatherUnusedImportsVisitor @@ -45,7 +46,7 @@ def _remove_imports_from_importfrom_stmt( self.context.full_package_name, import_node ) if module_name is None: - raise Exception("Cannot look up absolute module from relative import!") + raise ValueError("Cannot look up absolute module from relative import!") # We know any local names will refer to this as an alias if # there is one, and as the original name if there is not one @@ -72,7 +73,9 @@ def _visit_name_attr_alike(self, node: Union[cst.Name, cst.Attribute]) -> None: # Look up the scope for this node, remove the import that caused it to exist. metadata_wrapper = self.context.wrapper if metadata_wrapper is None: - raise Exception("Cannot look up import, metadata is not computed for node!") + raise ValueError( + "Cannot look up import, metadata is not computed for node!" + ) scope_provider = metadata_wrapper.resolve(ScopeProvider) try: scope = scope_provider[node] @@ -185,7 +188,7 @@ def _get_imports_from_context( ) -> List[Tuple[str, Optional[str], Optional[str]]]: unused_imports = context.scratch.get(RemoveImportsVisitor.CONTEXT_KEY, []) if not isinstance(unused_imports, list): - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") return unused_imports @staticmethod @@ -255,7 +258,7 @@ def remove_unused_import_by_node( context.full_package_name, node ) if module_name is None: - raise Exception("Cannot look up absolute module from relative import!") + raise ValueError("Cannot look up absolute module from relative import!") for import_alias in names: RemoveImportsVisitor.remove_unused_import( context, diff --git a/libcst/display/text.py b/libcst/display/text.py index 3c6dc2883..0e2700096 100644 --- a/libcst/display/text.py +++ b/libcst/display/text.py @@ -8,7 +8,7 @@ import dataclasses from typing import List, Sequence -from libcst import CSTNode +from libcst import CSTLogicError, CSTNode from libcst.helpers import filter_node_fields _DEFAULT_INDENT: str = " " @@ -84,7 +84,7 @@ def _node_repr_recursive( # noqa: C901 else: child_tokens.append("[]") else: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") # Handle indentation and trailing comma. split_by_line = "".join(child_tokens).split("\n") diff --git a/libcst/helpers/_template.py b/libcst/helpers/_template.py index e3f915a55..e205e0af5 100644 --- a/libcst/helpers/_template.py +++ b/libcst/helpers/_template.py @@ -45,12 +45,12 @@ def unmangled_name(var: str) -> Optional[str]: def mangle_template(template: str, template_vars: Set[str]) -> str: if TEMPLATE_PREFIX in template or TEMPLATE_SUFFIX in template: - raise Exception("Cannot parse a template containing reserved strings") + raise ValueError("Cannot parse a template containing reserved strings") for var in template_vars: original = f"{{{var}}}" if original not in template: - raise Exception( + raise ValueError( f'Template string is missing a reference to "{var}" referred to in kwargs' ) template = template.replace(original, mangled_name(var)) @@ -142,7 +142,7 @@ def __init__( name for name in template_replacements if name not in supported_vars } if unsupported_vars: - raise Exception( + raise ValueError( f'Template replacement for "{next(iter(unsupported_vars))}" is unsupported' ) @@ -350,7 +350,7 @@ def __init__(self, template_vars: Set[str]) -> None: def visit_Name(self, node: cst.Name) -> None: for var in self.template_vars: if node.value == mangled_name(var): - raise Exception(f'Template variable "{var}" was not replaced properly') + raise ValueError(f'Template variable "{var}" was not replaced properly') def unmangle_nodes( @@ -424,8 +424,8 @@ def parse_template_statement( if not isinstance( new_statement, (cst.SimpleStatementLine, cst.BaseCompoundStatement) ): - raise Exception( - f"Expected a statement but got a {new_statement.__class__.__name__}!" + raise TypeError( + f"Expected a statement but got a {new_statement.__class__.__qualname__}!" ) new_statement.visit(TemplateChecker({name for name in template_replacements})) return new_statement diff --git a/libcst/helpers/common.py b/libcst/helpers/common.py index 16c77669a..dee73aa4c 100644 --- a/libcst/helpers/common.py +++ b/libcst/helpers/common.py @@ -19,7 +19,7 @@ def ensure_type(node: object, nodetype: Type[T]) -> T: """ if not isinstance(node, nodetype): - raise Exception( - f"Expected a {nodetype.__name__} but got a {node.__class__.__name__}!" + raise ValueError( + f"Expected a {nodetype.__name__} but got a {node.__class__.__qualname__}!" ) return node diff --git a/libcst/helpers/expression.py b/libcst/helpers/expression.py index beb5f3248..5ae016cf7 100644 --- a/libcst/helpers/expression.py +++ b/libcst/helpers/expression.py @@ -38,5 +38,5 @@ def get_full_name_for_node_or_raise(node: Union[str, cst.CSTNode]) -> str: """ full_name = get_full_name_for_node(node) if full_name is None: - raise Exception(f"Not able to parse full name for: {node}") + raise ValueError(f"Not able to parse full name for: {node}") return full_name diff --git a/libcst/helpers/module.py b/libcst/helpers/module.py index 37e6af088..2b2973bf7 100644 --- a/libcst/helpers/module.py +++ b/libcst/helpers/module.py @@ -80,7 +80,7 @@ def get_absolute_module_for_import_or_raise( ) -> str: module = get_absolute_module_for_import(current_module, import_node) if module is None: - raise Exception(f"Unable to compute absolute module for {import_node}") + raise ValueError(f"Unable to compute absolute module for {import_node}") return module @@ -121,7 +121,7 @@ def get_absolute_module_from_package_for_import_or_raise( ) -> str: module = get_absolute_module_from_package_for_import(current_package, import_node) if module is None: - raise Exception(f"Unable to compute absolute module for {import_node}") + raise ValueError(f"Unable to compute absolute module for {import_node}") return module diff --git a/libcst/matchers/_matcher_base.py b/libcst/matchers/_matcher_base.py index 039694a5f..260f8f138 100644 --- a/libcst/matchers/_matcher_base.py +++ b/libcst/matchers/_matcher_base.py @@ -29,7 +29,7 @@ import libcst import libcst.metadata as meta -from libcst import FlattenSentinel, MaybeSentinel, RemovalSentinel +from libcst import CSTLogicError, FlattenSentinel, MaybeSentinel, RemovalSentinel from libcst._metadata_dependent import LazyValue @@ -143,7 +143,7 @@ def __init__(self, *options: Union[_MatcherTypeT, "TypeOf[_MatcherTypeT]"]) -> N for option in options: if isinstance(option, TypeOf): if option.initalized: - raise Exception( + raise ValueError( "Cannot chain an uninitalized TypeOf with an initalized one" ) actual_options.extend(option._raw_options) @@ -213,7 +213,7 @@ def __init__(self, *options: Union[_MatcherT, "OneOf[_MatcherT]"]) -> None: actual_options: List[_MatcherT] = [] for option in options: if isinstance(option, AllOf): - raise Exception("Cannot use AllOf and OneOf in combination!") + raise ValueError("Cannot use AllOf and OneOf in combination!") elif isinstance(option, (OneOf, TypeOf)): actual_options.extend(option.options) else: @@ -234,7 +234,7 @@ def __or__(self, other: _OtherNodeT) -> "OneOf[Union[_MatcherT, _OtherNodeT]]": return OneOf(self, other) def __and__(self, other: _OtherNodeT) -> NoReturn: - raise Exception("Cannot use AllOf and OneOf in combination!") + raise ValueError("Cannot use AllOf and OneOf in combination!") def __invert__(self) -> "AllOf[_MatcherT]": # Invert using De Morgan's Law so we don't have to complicate types. @@ -286,9 +286,9 @@ def __init__(self, *options: Union[_MatcherT, "AllOf[_MatcherT]"]) -> None: actual_options: List[_MatcherT] = [] for option in options: if isinstance(option, OneOf): - raise Exception("Cannot use AllOf and OneOf in combination!") + raise ValueError("Cannot use AllOf and OneOf in combination!") elif isinstance(option, TypeOf): - raise Exception("Cannot use AllOf and TypeOf in combination!") + raise ValueError("Cannot use AllOf and TypeOf in combination!") elif isinstance(option, AllOf): actual_options.extend(option.options) else: @@ -306,7 +306,7 @@ def options(self) -> Sequence[_MatcherT]: # pyre-fixme[15]: `__or__` overrides method defined in `type` inconsistently. def __or__(self, other: _OtherNodeT) -> NoReturn: - raise Exception("Cannot use AllOf and OneOf in combination!") + raise ValueError("Cannot use AllOf and OneOf in combination!") def __and__(self, other: _OtherNodeT) -> "AllOf[Union[_MatcherT, _OtherNodeT]]": return AllOf(self, other) @@ -431,7 +431,7 @@ def __and__(self, other: _OtherNodeT) -> "AllOf[Union[_MatcherT, _OtherNodeT]]": # that are captured with an and, either all of them will be assigned the # same node, or none of them. It makes more sense to move the SaveMatchedNode # up to wrap the AllOf. - raise Exception( + raise ValueError( ( "Cannot use AllOf with SavedMatchedNode children! Instead, you should " + "use SaveMatchedNode(AllOf(options...))." @@ -447,10 +447,10 @@ def __getattr__(self, key: str) -> object: def __invert__(self) -> "_MatcherT": # This doesn't make sense. We don't want to capture a node only if it # doesn't match, since this will never capture anything. - raise Exception( + raise ValueError( ( "Cannot invert a SaveMatchedNode. Instead you should wrap SaveMatchedNode " - + "around your inversion itself" + "around your inversion itself" ) ) @@ -761,7 +761,9 @@ def __init__( n: int, ) -> None: if n < 0: - raise Exception(f"{self.__class__.__name__} n attribute must be positive") + raise ValueError( + f"{self.__class__.__qualname__} n attribute must be positive" + ) self._n: int = n self._matcher: Union[_MatcherT, DoNotCareSentinel] = matcher @@ -784,13 +786,13 @@ def matcher(self) -> Union[_MatcherT, DoNotCareSentinel]: # pyre-fixme[15]: `__or__` overrides method defined in `type` inconsistently. def __or__(self, other: object) -> NoReturn: - raise Exception("AtLeastN cannot be used in a OneOf matcher") + raise ValueError("AtLeastN cannot be used in a OneOf matcher") def __and__(self, other: object) -> NoReturn: - raise Exception("AtLeastN cannot be used in an AllOf matcher") + raise ValueError("AtLeastN cannot be used in an AllOf matcher") def __invert__(self) -> NoReturn: - raise Exception("Cannot invert an AtLeastN matcher!") + raise ValueError("Cannot invert an AtLeastN matcher!") def __repr__(self) -> str: if self._n == 0: @@ -863,7 +865,9 @@ def __init__( n: int, ) -> None: if n < 0: - raise Exception(f"{self.__class__.__name__} n attribute must be positive") + raise ValueError( + f"{self.__class__.__qualname__} n attribute must be positive" + ) self._n: int = n self._matcher: Union[_MatcherT, DoNotCareSentinel] = matcher @@ -887,13 +891,13 @@ def matcher(self) -> Union[_MatcherT, DoNotCareSentinel]: # pyre-fixme[15]: `__or__` overrides method defined in `type` inconsistently. def __or__(self, other: object) -> NoReturn: - raise Exception("AtMostN cannot be used in a OneOf matcher") + raise ValueError("AtMostN cannot be used in a OneOf matcher") def __and__(self, other: object) -> NoReturn: - raise Exception("AtMostN cannot be used in an AllOf matcher") + raise ValueError("AtMostN cannot be used in an AllOf matcher") def __invert__(self) -> NoReturn: - raise Exception("Cannot invert an AtMostN matcher!") + raise ValueError("Cannot invert an AtMostN matcher!") def __repr__(self) -> str: if self._n == 1: @@ -1158,7 +1162,7 @@ def _sequence_matches( # noqa: C901 else: # There are no other types of wildcard consumers, but we're making # pyre happy with that fact. - raise Exception(f"Logic error unrecognized wildcard {type(matcher)}!") + raise CSTLogicError(f"Logic error unrecognized wildcard {type(matcher)}!") elif isinstance(matcher, _ExtractMatchingNode): # See if the raw matcher matches. If it does, capture the sequence we matched and store it. result = _sequence_matches( @@ -1354,7 +1358,7 @@ def _metadata_matches( # noqa: C901 return None return {} if actual_value == metadata.value else None else: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") def _node_matches( # noqa: C901 @@ -1918,7 +1922,7 @@ def replace( elif isinstance(tree, meta.MetadataWrapper): return tree.module.deep_clone() else: - raise Exception("Logic error!") + raise CSTLogicError("Logic error!") if isinstance(tree, meta.MetadataWrapper) and metadata_resolver is None: # Provide a convenience for calling replace directly on a MetadataWrapper. @@ -1935,5 +1939,5 @@ def replace( new_tree = tree.visit(replacer) if isinstance(new_tree, FlattenSentinel): # The above transform never returns FlattenSentinel, so this isn't possible - raise Exception("Logic error, cannot get a FlattenSentinel here!") + raise CSTLogicError("Logic error, cannot get a FlattenSentinel here!") return new_tree diff --git a/libcst/metadata/base_provider.py b/libcst/metadata/base_provider.py index 2e03416f0..0de6a80c9 100644 --- a/libcst/metadata/base_provider.py +++ b/libcst/metadata/base_provider.py @@ -78,7 +78,7 @@ def __init__(self, cache: object = None) -> None: self._computed: MutableMapping["CSTNode", MaybeLazyMetadataT] = {} if self.gen_cache and cache is None: # The metadata provider implementation is responsible to store and use cache. - raise Exception( + raise ValueError( f"Cache is required for initializing {self.__class__.__name__}." ) self.cache = cache diff --git a/libcst/metadata/full_repo_manager.py b/libcst/metadata/full_repo_manager.py index 770ba1f63..ab6430d83 100644 --- a/libcst/metadata/full_repo_manager.py +++ b/libcst/metadata/full_repo_manager.py @@ -85,7 +85,7 @@ def get_cache_for_path(self, path: str) -> Mapping["ProviderT", object]: MetadataWrapper(module, cache=manager.get_cache_for_path("a.py")) """ if path not in self._paths: - raise Exception( + raise ValueError( "The path needs to be in paths parameter when constructing FullRepoManager for efficient batch processing." ) # Make sure that the cache is available to us. If the user called diff --git a/libcst/metadata/tests/test_type_inference_provider.py b/libcst/metadata/tests/test_type_inference_provider.py index 50ca34584..a0a70a8c8 100644 --- a/libcst/metadata/tests/test_type_inference_provider.py +++ b/libcst/metadata/tests/test_type_inference_provider.py @@ -63,17 +63,11 @@ class TypeInferenceProviderTest(UnitTest): @classmethod def setUpClass(cls) -> None: os.chdir(TEST_SUITE_PATH) - try: - subprocess.run(["pyre", "-n", "start", "--no-watchman"]) - except subprocess.TimeoutExpired as exc: - raise exc + subprocess.run(["pyre", "-n", "start", "--no-watchman"]) @classmethod def tearDownClass(cls) -> None: - try: - subprocess.run(["pyre", "-n", "stop"], cwd=TEST_SUITE_PATH) - except subprocess.TimeoutExpired as exc: - raise exc + subprocess.run(["pyre", "-n", "stop"], cwd=TEST_SUITE_PATH) @data_provider( ((TEST_SUITE_PATH / "simple_class.py", TEST_SUITE_PATH / "simple_class.json"),) diff --git a/libcst/metadata/type_inference_provider.py b/libcst/metadata/type_inference_provider.py index f00c97b6a..8a90c26b9 100644 --- a/libcst/metadata/type_inference_provider.py +++ b/libcst/metadata/type_inference_provider.py @@ -14,6 +14,11 @@ from libcst.metadata.position_provider import PositionProvider +class TypeInferenceError(Exception): + """An attempt to access inferred type annotation + (through Pyre Query API) failed.""" + + class Position(TypedDict): line: int column: int @@ -60,17 +65,19 @@ def gen_cache( ) -> Mapping[str, object]: params = ",".join(f"path='{root_path / path}'" for path in paths) cmd_args = ["pyre", "--noninteractive", "query", f"types({params})"] - try: - stdout, stderr, return_code = run_command(cmd_args, timeout=timeout) - except subprocess.TimeoutExpired as exc: - raise exc - if return_code != 0: - raise Exception(f"stderr:\n {stderr}\nstdout:\n {stdout}") + result = subprocess.run( + cmd_args, capture_output=True, timeout=timeout, text=True + ) + try: - resp = json.loads(stdout)["response"] + result.check_returncode() + resp = json.loads(result.stdout)["response"] except Exception as e: - raise Exception(f"{e}\n\nstderr:\n {stderr}\nstdout:\n {stdout}") + raise TypeInferenceError( + f"{e}\n\nstderr:\n {result.stderr}\nstdout:\n {result.stdout}" + ) from e + return {path: _process_pyre_data(data) for path, data in zip(paths, resp)} def __init__(self, cache: PyreData) -> None: @@ -104,13 +111,6 @@ def visit_Call(self, node: cst.Call) -> Optional[bool]: self._parse_metadata(node) -def run_command( - cmd_args: List[str], timeout: Optional[int] = None -) -> Tuple[str, str, int]: - process = subprocess.run(cmd_args, capture_output=True, timeout=timeout) - return process.stdout.decode(), process.stderr.decode(), process.returncode - - class RawPyreData(TypedDict): path: str types: Sequence[InferredType] diff --git a/libcst/tool.py b/libcst/tool.py index 3c00ba8d6..abf68cbf4 100644 --- a/libcst/tool.py +++ b/libcst/tool.py @@ -21,7 +21,7 @@ import yaml -from libcst import LIBCST_VERSION, parse_module, PartialParserConfig +from libcst import CSTLogicError, LIBCST_VERSION, parse_module, PartialParserConfig from libcst._parser.parso.utils import parse_version_string from libcst.codemod import ( CodemodCommand, @@ -191,7 +191,7 @@ def _find_and_load_config(proc_name: str) -> Dict[str, Any]: requires_config = bool(os.environ.get("LIBCST_TOOL_REQUIRE_CONFIG", "")) if requires_config and not found_config: - raise Exception( + raise FileNotFoundError( f"Did not find a {CONFIG_FILE_NAME} in current directory or any " + "parent directory! Perhaps you meant to run this command from a " + "configured subdirectory, or you need to initialize a new project " @@ -390,7 +390,7 @@ def _codemod_impl(proc_name: str, command_args: List[str]) -> int: # noqa: C901 # full-repo metadata since there is no path. if any(p == "-" for p in args.path): if len(args.path) > 1: - raise Exception("Cannot specify multiple paths when reading from stdin!") + raise ValueError("Cannot specify multiple paths when reading from stdin!") print("Codemodding from stdin", file=sys.stderr) oldcode = sys.stdin.read() @@ -477,7 +477,7 @@ def __init__(self, comment: str, *, newlines: bool = False) -> None: def _serialize_impl(self, key: str, value: object) -> str: if not isinstance(value, list): - raise Exception("Can only serialize lists!") + raise ValueError("Can only serialize lists!") if self.newlines: values = [f"- {v!r}" for v in value] return f"{key}:{os.linesep}{os.linesep.join(values)}" @@ -538,7 +538,7 @@ def _initialize_impl(proc_name: str, command_args: List[str]) -> int: # For safety, verify that it parses to the identical file. actual_config = yaml.safe_load(config_str) if actual_config != default_config: - raise Exception("Logic error, serialization is invalid!") + raise CSTLogicError("Logic error, serialization is invalid!") config_file = os.path.abspath(os.path.join(args.path, CONFIG_FILE_NAME)) with open(config_file, "w") as fp: