# 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.
import inspect
import re
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Optional, Pattern, Sequence, Union
from libcst._add_slots import add_slots
from libcst._maybe_sentinel import MaybeSentinel
from libcst._nodes.base import CSTNode, CSTValidationError
from libcst._nodes.expression import (
_BaseParenthesizedNode,
Annotation,
Arg,
Asynchronous,
Attribute,
BaseAssignTargetExpression,
BaseDelTargetExpression,
BaseExpression,
Call,
ConcatenatedString,
ExpressionPosition,
From,
LeftCurlyBrace,
LeftParen,
LeftSquareBracket,
List,
Name,
Parameters,
RightCurlyBrace,
RightParen,
RightSquareBracket,
SimpleString,
Tuple,
)
from libcst._nodes.internal import (
CodegenState,
visit_body_sequence,
visit_optional,
visit_required,
visit_sentinel,
visit_sequence,
)
from libcst._nodes.op import (
AssignEqual,
BaseAugOp,
BitOr,
Comma,
Dot,
ImportStar,
Semicolon,
)
from libcst._nodes.whitespace import (
BaseParenthesizableWhitespace,
EmptyLine,
ParenthesizedWhitespace,
SimpleWhitespace,
TrailingWhitespace,
)
from libcst._visitors import CSTVisitorT
_INDENT_WHITESPACE_RE: Pattern[str] = re.compile(r"[ \f\t]+", re.UNICODE)
[docs]class BaseSuite(CSTNode, ABC):
"""
A dummy base-class for both :class:`SimpleStatementSuite` and :class:`IndentedBlock`.
This exists to simplify type definitions and isinstance checks.
A suite is a group of statements controlled by a clause. A suite can be one or
more semicolon-separated simple statements on the same line as the header,
following the header’s colon, or it can be one or more indented statements on
subsequent lines.
-- https://docs.python.org/3/reference/compound_stmts.html
"""
__slots__ = ()
body: Union[Sequence["BaseStatement"], Sequence["BaseSmallStatement"]]
class BaseStatement(CSTNode, ABC):
"""
A class that exists to allow for typing to specify that any statement is allowed
in a particular location.
"""
__slots__ = ()
[docs]class BaseSmallStatement(CSTNode, ABC):
"""
Encapsulates a small statement, like ``del`` or ``pass``, and optionally adds a
trailing semicolon. A small statement is always contained inside a
:class:`SimpleStatementLine` or :class:`SimpleStatementSuite`. This exists to
simplify type definitions and isinstance checks.
"""
__slots__ = ()
#: An optional semicolon that appears after a small statement. This is optional
#: for the last small statement in a :class:`SimpleStatementLine` or
#: :class:`SimpleStatementSuite`, but all other small statements inside a simple
#: statement must contain a semicolon to disambiguate multiple small statements
#: on the same line.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
@abstractmethod
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
...
[docs]@add_slots
@dataclass(frozen=True)
class Del(BaseSmallStatement):
"""
Represents a ``del`` statement. ``del`` is always followed by a target.
"""
#: The target expression will be deleted. This can be a name, a tuple,
#: an item of a list, an item of a dictionary, or an attribute.
target: BaseDelTargetExpression
#: The whitespace after the ``del`` keyword.
whitespace_after_del: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _validate(self) -> None:
if (
self.whitespace_after_del.empty
and not self.target._safe_to_use_with_word_operator(
ExpressionPosition.RIGHT
)
):
raise CSTValidationError("Must have at least one space after 'del'.")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Del":
return Del(
target=visit_required(self, "target", self.target, visitor),
whitespace_after_del=visit_required(
self, "whitespace_after_del", self.whitespace_after_del, visitor
),
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
state.add_token("del")
self.whitespace_after_del._codegen(state)
self.target._codegen(state)
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class Pass(BaseSmallStatement):
"""
Represents a ``pass`` statement.
"""
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Pass":
return Pass(
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor)
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
state.add_token("pass")
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class Break(BaseSmallStatement):
"""
Represents a ``break`` statement, which is used to break out of a :class:`For`
or :class:`While` loop early.
"""
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Break":
return Break(
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor)
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
state.add_token("break")
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class Continue(BaseSmallStatement):
"""
Represents a ``continue`` statement, which is used to skip to the next iteration
in a :class:`For` or :class:`While` loop.
"""
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Continue":
return Continue(
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor)
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
state.add_token("continue")
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class Return(BaseSmallStatement):
"""
Represents a ``return`` or a ``return x`` statement.
"""
#: The optional expression that will be evaluated and returned.
value: Optional[BaseExpression] = None
#: Optional whitespace after the ``return`` keyword before the optional
#: value expression.
whitespace_after_return: Union[
SimpleWhitespace, MaybeSentinel
] = MaybeSentinel.DEFAULT
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _validate(self) -> None:
value = self.value
if value is not None:
whitespace_after_return = self.whitespace_after_return
has_no_gap = (
not isinstance(whitespace_after_return, MaybeSentinel)
and whitespace_after_return.empty
)
if has_no_gap and not value._safe_to_use_with_word_operator(
ExpressionPosition.RIGHT
):
raise CSTValidationError("Must have at least one space after 'return'.")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Return":
return Return(
whitespace_after_return=visit_sentinel(
self, "whitespace_after_return", self.whitespace_after_return, visitor
),
value=visit_optional(self, "value", self.value, visitor),
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
state.add_token("return")
whitespace_after_return = self.whitespace_after_return
value = self.value
if isinstance(whitespace_after_return, MaybeSentinel):
if value is not None:
state.add_token(" ")
else:
whitespace_after_return._codegen(state)
if value is not None:
value._codegen(state)
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class Expr(BaseSmallStatement):
"""
An expression used as a statement, where the result is unused and unassigned.
The most common place you will find this is in function calls where the return
value is unneeded.
"""
#: The expression itself. Python will evaluate the expression but not assign
#: the result anywhere.
value: BaseExpression
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Expr":
return Expr(
value=visit_required(self, "value", self.value, visitor),
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
self.value._codegen(state)
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
class _BaseSimpleStatement(CSTNode, ABC):
"""
A simple statement is a series of small statements joined together by semicolons.
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
Whitespace between each small statement is owned by the small statements themselves.
It can be found on the required semicolon that will be attached to each non-terminal
small statement.
"""
__slots__ = ()
#: Sequence of small statements. All but the last statement are required to have
#: a semicolon.
body: Sequence[BaseSmallStatement]
#: Any trailing comment and the final ``NEWLINE``, which is part of small statement's
#: grammar.
trailing_whitespace: TrailingWhitespace
def _validate(self) -> None:
body = self.body
for small_stmt in body[:-1]:
if small_stmt.semicolon is None:
raise CSTValidationError(
"All but the last SmallStatement in a SimpleStatementLine or "
+ "SimpleStatementSuite must have a trailing semicolon. Otherwise, "
+ "there's no way to syntatically disambiguate each SmallStatement "
+ "on the same line."
)
def _codegen_impl(self, state: CodegenState) -> None:
body = self.body
if body:
laststmt = len(body) - 1
with state.record_syntactic_position(self, end_node=body[laststmt]):
for idx, stmt in enumerate(body):
stmt._codegen(state, default_semicolon=(idx != laststmt))
else:
# Empty simple statement blocks are not syntactically valid in Python
# unless they contain a 'pass' statement, so add one here.
with state.record_syntactic_position(self):
state.add_token("pass")
self.trailing_whitespace._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class SimpleStatementLine(_BaseSimpleStatement, BaseStatement):
"""
A simple statement that's part of an IndentedBlock or Module. A simple statement is
a series of small statements joined together by semicolons.
This isn't differentiated from a :class:`SimpleStatementSuite` in the grammar, but
because a :class:`SimpleStatementLine` can own additional whitespace that a
:class:`SimpleStatementSuite` doesn't have, we're differentiating it in the CST.
"""
#: Sequence of small statements. All but the last statement are required to have
#: a semicolon.
body: Sequence[BaseSmallStatement]
#: Sequence of empty lines appearing before this simple statement line.
leading_lines: Sequence[EmptyLine] = ()
#: Any optional trailing comment and the final ``NEWLINE`` at the end of the line.
trailing_whitespace: TrailingWhitespace = TrailingWhitespace.field()
def _visit_and_replace_children(
self, visitor: CSTVisitorT
) -> "SimpleStatementLine":
return SimpleStatementLine(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
body=visit_sequence(self, "body", self.body, visitor),
trailing_whitespace=visit_required(
self, "trailing_whitespace", self.trailing_whitespace, visitor
),
)
def _is_removable(self) -> bool:
# If we have an empty body, we are removable since we don't represent
# anything concrete.
return not self.body
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
_BaseSimpleStatement._codegen_impl(self, state)
[docs]@add_slots
@dataclass(frozen=True)
class SimpleStatementSuite(_BaseSimpleStatement, BaseSuite):
"""
A simple statement that's used as a suite. A simple statement is a series of small
statements joined together by semicolons. A suite is the thing that follows the
colon in a compound statement.
.. code-block::
if test:<leading_whitespace><body><trailing_whitespace>
This isn't differentiated from a :class:`SimpleStatementLine` in the grammar, but
because the two classes need to track different whitespace, we're differentiating
it in the CST.
"""
#: Sequence of small statements. All but the last statement are required to have
#: a semicolon.
body: Sequence[BaseSmallStatement]
#: The whitespace between the colon in the parent statement and the body.
leading_whitespace: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Any optional trailing comment and the final ``NEWLINE`` at the end of the line.
trailing_whitespace: TrailingWhitespace = TrailingWhitespace.field()
def _visit_and_replace_children(
self, visitor: CSTVisitorT
) -> "SimpleStatementSuite":
return SimpleStatementSuite(
leading_whitespace=visit_required(
self, "leading_whitespace", self.leading_whitespace, visitor
),
body=visit_sequence(self, "body", self.body, visitor),
trailing_whitespace=visit_required(
self, "trailing_whitespace", self.trailing_whitespace, visitor
),
)
def _codegen_impl(self, state: CodegenState) -> None:
self.leading_whitespace._codegen(state)
_BaseSimpleStatement._codegen_impl(self, state)
[docs]@add_slots
@dataclass(frozen=True)
class Else(CSTNode):
"""
An ``else`` clause that appears optionally after an :class:`If`, :class:`While`,
:class:`Try`, or :class:`For` statement.
This node does not match ``elif`` clauses in :class:`If` statements. It also
does not match the required ``else`` clause in an :class:`IfExp` expression
(``a = if b else c``).
"""
#: The body of else clause.
body: BaseSuite
#: Sequence of empty lines appearing before this compound statement line.
leading_lines: Sequence[EmptyLine] = ()
#: The whitespace appearing after the ``else`` keyword but before the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Else":
return Else(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
body=visit_required(self, "body", self.body, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
with state.record_syntactic_position(self, end_node=self.body):
state.add_token("else")
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)
[docs]class BaseCompoundStatement(BaseStatement, ABC):
"""
Encapsulates a compound statement, like ``if True: pass`` or ``while True: pass``.
This exists to simplify type definitions and isinstance checks.
Compound statements contain (groups of) other statements; they affect or control
the execution of those other statements in some way. In general, compound
statements span multiple lines, although in simple incarnations a whole compound
statement may be contained in one line.
-- https://docs.python.org/3/reference/compound_stmts.html
"""
__slots__ = ()
#: The body of this compound statement.
body: BaseSuite
#: Any empty lines or comments appearing before this statement.
leading_lines: Sequence[EmptyLine]
[docs]@add_slots
@dataclass(frozen=True)
class If(BaseCompoundStatement):
"""
An ``if`` statement. ``test`` holds a single test expression.
``elif`` clauses don’t have a special representation in the AST, but rather appear as
extra :class:`If` nodes within the ``orelse`` section of the previous one.
"""
#: The expression that, when evaluated, should give us a truthy/falsey value.
test: BaseExpression # TODO: should be a test_nocond
#: The body of this compound statement.
body: BaseSuite
#: An optional ``elif`` or ``else`` clause. :class:`If` signifies an ``elif`` block.
#: :class:`Else` signifies an ``else`` block. ``None`` signifies no ``else`` or
#:``elif`` block.
orelse: Union["If", Else, None] = None
#: Sequence of empty lines appearing before this compound statement line.
leading_lines: Sequence[EmptyLine] = ()
#: The whitespace appearing after the ``if`` keyword but before the test expression.
whitespace_before_test: SimpleWhitespace = SimpleWhitespace.field(" ")
#: The whitespace appearing after the test expression but before the colon.
whitespace_after_test: SimpleWhitespace = SimpleWhitespace.field("")
# TODO: _validate
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "If":
return If(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
whitespace_before_test=visit_required(
self, "whitespace_before_test", self.whitespace_before_test, visitor
),
test=visit_required(self, "test", self.test, visitor),
whitespace_after_test=visit_required(
self, "whitespace_after_test", self.whitespace_after_test, visitor
),
body=visit_required(self, "body", self.body, visitor),
orelse=visit_optional(self, "orelse", self.orelse, visitor),
)
def _codegen_impl(self, state: CodegenState, is_elif: bool = False) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
end_node = self.body if self.orelse is None else self.orelse
with state.record_syntactic_position(self, end_node=end_node):
state.add_token("elif" if is_elif else "if")
self.whitespace_before_test._codegen(state)
self.test._codegen(state)
self.whitespace_after_test._codegen(state)
state.add_token(":")
self.body._codegen(state)
orelse = self.orelse
if orelse is not None:
if isinstance(orelse, If): # special-case elif
orelse._codegen(state, is_elif=True)
else: # is an Else clause
orelse._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class IndentedBlock(BaseSuite):
"""
Represents a block of statements beginning with an ``INDENT`` token and ending in a
``DEDENT`` token. Used as the body of compound statements, such as an if statement's
body.
A common alternative to an :class:`IndentedBlock` is a :class:`SimpleStatementSuite`,
which can also be used as a :class:`BaseSuite`, meaning that it can be used as the
body of many compound statements.
An :class:`IndentedBlock` always occurs after a colon in a
:class:`BaseCompoundStatement`, so it owns the trailing whitespace for the compound
statement's clause.
.. code-block::
if test: # IndentedBlock's header
body
"""
#: Sequence of statements belonging to this indented block.
body: Sequence[BaseStatement]
#: Any optional trailing comment and the final ``NEWLINE`` at the end of the line.
header: TrailingWhitespace = TrailingWhitespace.field()
#: A string represents a specific indentation. A ``None`` value uses the modules's
#: default indentation. This is included because indentation is allowed to be
#: inconsistent across a file, just not ambiguously.
indent: Optional[str] = None
#: Any trailing comments or lines after the dedent that are owned by this indented
#: block. Statements own preceeding and same-line trailing comments, but not
#: trailing lines, so it falls on :class:`IndentedBlock` to own it. In the case
#: that a statement follows an :class:`IndentedBlock`, that statement will own the
#: comments and lines that are at the same indent as the statement, and this
#: :class:`IndentedBlock` will own the comments and lines that are indented further.
footer: Sequence[EmptyLine] = ()
def _validate(self) -> None:
indent = self.indent
if indent is not None:
if len(indent) == 0:
raise CSTValidationError(
"An indented block must have a non-zero width indent."
)
if _INDENT_WHITESPACE_RE.fullmatch(indent) is None:
raise CSTValidationError(
"An indent must be composed of only whitespace characters."
)
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "IndentedBlock":
return IndentedBlock(
header=visit_required(self, "header", self.header, visitor),
indent=self.indent,
body=visit_body_sequence(self, "body", self.body, visitor),
footer=visit_sequence(self, "footer", self.footer, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
self.header._codegen(state)
indent = self.indent
state.increase_indent(state.default_indent if indent is None else indent)
if self.body:
with state.record_syntactic_position(
self, start_node=self.body[0], end_node=self.body[-1]
):
for stmt in self.body:
# IndentedBlock is responsible for adjusting the current indentation level,
# but its children are responsible for actually adding that indentation to
# the token list.
stmt._codegen(state)
else:
# Empty indented blocks are not syntactically valid in Python unless
# they contain a 'pass' statement, so add one here.
state.add_indent_tokens()
with state.record_syntactic_position(self):
state.add_token("pass")
state.add_token(state.default_newline)
for f in self.footer:
f._codegen(state)
state.decrease_indent()
[docs]@add_slots
@dataclass(frozen=True)
class AsName(CSTNode):
"""
An ``as name`` clause inside an :class:`ExceptHandler`, :class:`ImportAlias` or
:class:`WithItem` node.
"""
#: Identifier that the parent node will be aliased to.
name: Union[Name, Tuple, List]
#: Whitespace between the parent node and the ``as`` keyword.
whitespace_before_as: BaseParenthesizableWhitespace = SimpleWhitespace.field(" ")
#: Whitespace between the ``as`` keyword and the name.
whitespace_after_as: BaseParenthesizableWhitespace = SimpleWhitespace.field(" ")
def _validate(self) -> None:
if self.whitespace_after_as.empty:
raise CSTValidationError(
"There must be at least one space between 'as' and name."
)
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "AsName":
return AsName(
whitespace_before_as=visit_required(
self, "whitespace_before_as", self.whitespace_before_as, visitor
),
name=visit_required(self, "name", self.name, visitor),
whitespace_after_as=visit_required(
self, "whitespace_after_as", self.whitespace_after_as, visitor
),
)
def _codegen_impl(self, state: CodegenState) -> None:
self.whitespace_before_as._codegen(state)
state.add_token("as")
self.whitespace_after_as._codegen(state)
self.name._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class ExceptHandler(CSTNode):
"""
An ``except`` clause that appears optionally after a :class:`Try` statement.
"""
#: The body of the except.
body: BaseSuite
#: The type of exception this catches. Can be a tuple in some cases,
#: or ``None`` if the code is catching all exceptions.
type: Optional[BaseExpression] = None
#: The optional name that a caught exception is assigned to.
name: Optional[AsName] = None
#: Sequence of empty lines appearing before this compound statement line.
leading_lines: Sequence[EmptyLine] = ()
#: The whitespace between the ``except`` keyword and the type attribute.
whitespace_after_except: SimpleWhitespace = SimpleWhitespace.field(" ")
#: The whitespace after any type or name node (whichever comes last) and
#: the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")
def _validate(self) -> None:
name = self.name
if self.type is None and name is not None:
raise CSTValidationError("Cannot have a name for an empty type.")
if name is not None and not isinstance(name.name, Name):
raise CSTValidationError(
"Must use a Name node for AsName name inside ExceptHandler."
)
type_ = self.type
if type_ is not None and self.whitespace_after_except.empty:
# Space is only required when the first char in `type` could start
# an identifier. In the most common cases, we want to allow
# grouping or tuple parens.
if isinstance(type_, Name) and not type_.lpar:
raise CSTValidationError(
"Must have at least one space after except when ExceptHandler has a type."
)
name = self.name
if (
type_ is not None
and name is not None
and name.whitespace_before_as.empty
and not type_._safe_to_use_with_word_operator(ExpressionPosition.LEFT)
):
raise CSTValidationError(
"Must have at least one space before as keyword in an except handler."
)
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "ExceptHandler":
return ExceptHandler(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
whitespace_after_except=visit_required(
self, "whitespace_after_except", self.whitespace_after_except, visitor
),
type=visit_optional(self, "type", self.type, visitor),
name=visit_optional(self, "name", self.name, visitor),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
body=visit_required(self, "body", self.body, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
with state.record_syntactic_position(self, end_node=self.body):
state.add_token("except")
self.whitespace_after_except._codegen(state)
typenode = self.type
if typenode is not None:
typenode._codegen(state)
namenode = self.name
if namenode is not None:
namenode._codegen(state)
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)
@add_slots
@dataclass(frozen=True)
class ExceptStarHandler(CSTNode):
"""
An ``except*`` clause that appears after a :class:`TryStar` statement.
"""
#: The body of the except.
body: BaseSuite
#: The type of exception this catches. Can be a tuple in some cases.
type: BaseExpression
#: The optional name that a caught exception is assigned to.
name: Optional[AsName] = None
#: Sequence of empty lines appearing before this compound statement line.
leading_lines: Sequence[EmptyLine] = ()
#: The whitespace between the ``except`` keyword and the star.
whitespace_after_except: SimpleWhitespace = SimpleWhitespace.field("")
#: The whitespace between the star and the type.
whitespace_after_star: SimpleWhitespace = SimpleWhitespace.field(" ")
#: The whitespace after any type or name node (whichever comes last) and
#: the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")
def _validate(self) -> None:
name = self.name
if name is not None and not isinstance(name.name, Name):
raise CSTValidationError(
"Must use a Name node for AsName name inside ExceptHandler."
)
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "ExceptStarHandler":
return ExceptStarHandler(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
whitespace_after_except=visit_required(
self, "whitespace_after_except", self.whitespace_after_except, visitor
),
whitespace_after_star=visit_required(
self, "whitespace_after_star", self.whitespace_after_star, visitor
),
type=visit_required(self, "type", self.type, visitor),
name=visit_optional(self, "name", self.name, visitor),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
body=visit_required(self, "body", self.body, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
with state.record_syntactic_position(self, end_node=self.body):
state.add_token("except")
self.whitespace_after_except._codegen(state)
state.add_token("*")
self.whitespace_after_star._codegen(state)
typenode = self.type
if typenode is not None:
typenode._codegen(state)
namenode = self.name
if namenode is not None:
namenode._codegen(state)
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class Finally(CSTNode):
"""
A ``finally`` clause that appears optionally after a :class:`Try` statement.
"""
#: The body of the except.
body: BaseSuite
#: Sequence of empty lines appearing before this compound statement line.
leading_lines: Sequence[EmptyLine] = ()
#: The whitespace that appears after the ``finally`` keyword but before
#: the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Finally":
return Finally(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
body=visit_required(self, "body", self.body, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
with state.record_syntactic_position(self, end_node=self.body):
state.add_token("finally")
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class Try(BaseCompoundStatement):
"""
A regular ``try`` statement that cannot contain :class:`ExceptStar` blocks. For
``try`` statements that can contain :class:`ExceptStar` blocks, see
:class:`TryStar`.
"""
#: The suite that is wrapped with a try statement.
body: BaseSuite
#: A list of zero or more exception handlers.
handlers: Sequence[ExceptHandler] = ()
#: An optional else case.
orelse: Optional[Else] = None
#: An optional finally case.
finalbody: Optional[Finally] = None
#: Sequence of empty lines appearing before this compound statement line.
leading_lines: Sequence[EmptyLine] = ()
#: The whitespace that appears after the ``try`` keyword but before
#: the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")
def _validate(self) -> None:
if len(self.handlers) == 0 and self.finalbody is None:
raise CSTValidationError(
"A Try statement must have at least one ExceptHandler or Finally"
)
if len(self.handlers) == 0 and self.orelse is not None:
raise CSTValidationError(
"A Try statement must have at least one ExceptHandler in order "
+ "to have an Else."
)
# Check bare excepts are always at the last position
if any(handler.type is None for handler in self.handlers[:-1]):
raise CSTValidationError("The bare except: handler must be the last one.")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Try":
return Try(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
body=visit_required(self, "body", self.body, visitor),
handlers=visit_sequence(self, "handlers", self.handlers, visitor),
orelse=visit_optional(self, "orelse", self.orelse, visitor),
finalbody=visit_optional(self, "finalbody", self.finalbody, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
end_node = self.body
if len(self.handlers) > 0:
end_node = self.handlers[-1]
orelse = self.orelse
end_node = end_node if orelse is None else orelse
finalbody = self.finalbody
end_node = end_node if finalbody is None else finalbody
with state.record_syntactic_position(self, end_node=end_node):
state.add_token("try")
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)
for handler in self.handlers:
handler._codegen(state)
if orelse is not None:
orelse._codegen(state)
if finalbody is not None:
finalbody._codegen(state)
@add_slots
@dataclass(frozen=True)
class TryStar(BaseCompoundStatement):
"""
A ``try`` statement with ``except*`` blocks.
"""
#: The suite that is wrapped with a try statement.
body: BaseSuite
#: A list of one or more exception handlers.
handlers: Sequence[ExceptStarHandler]
#: An optional else case.
orelse: Optional[Else] = None
#: An optional finally case.
finalbody: Optional[Finally] = None
#: Sequence of empty lines appearing before this compound statement line.
leading_lines: Sequence[EmptyLine] = ()
#: The whitespace that appears after the ``try`` keyword but before
#: the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")
def _validate(self) -> None:
if len(self.handlers) == 0:
raise CSTValidationError(
"A TryStar statement must have at least one ExceptHandler"
)
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "TryStar":
return TryStar(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
body=visit_required(self, "body", self.body, visitor),
handlers=visit_sequence(self, "handlers", self.handlers, visitor),
orelse=visit_optional(self, "orelse", self.orelse, visitor),
finalbody=visit_optional(self, "finalbody", self.finalbody, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
end_node = self.handlers[-1]
orelse = self.orelse
end_node = end_node if orelse is None else orelse
finalbody = self.finalbody
end_node = end_node if finalbody is None else finalbody
with state.record_syntactic_position(self, end_node=end_node):
state.add_token("try")
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)
for handler in self.handlers:
handler._codegen(state)
if orelse is not None:
orelse._codegen(state)
if finalbody is not None:
finalbody._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class ImportAlias(CSTNode):
"""
An import, with an optional :class:`AsName`. Used in both :class:`Import` and
:class:`ImportFrom` to specify a single import out of another module.
"""
#: Name or Attribute node representing the object we are importing.
name: Union[Attribute, Name]
#: Local alias we will import the above object as.
asname: Optional[AsName] = None
#: Any trailing comma that appears after this import. This is optional for the
#: last :class:`ImportAlias` in a :class:`Import` or :class:`ImportFrom`, but all
#: other import aliases inside an import must contain a comma to disambiguate
#: multiple imports.
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
def _validate(self) -> None:
asname = self.asname
if asname is not None:
if not isinstance(asname.name, Name):
raise CSTValidationError(
"Must use a Name node for AsName name inside ImportAlias."
)
if asname.whitespace_before_as.empty:
raise CSTValidationError(
"Must have at least one space before as keyword in an ImportAlias."
)
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
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "ImportAlias":
return ImportAlias(
name=visit_required(self, "name", self.name, visitor),
asname=visit_optional(self, "asname", self.asname, visitor),
comma=visit_sentinel(self, "comma", self.comma, visitor),
)
def _codegen_impl(self, state: CodegenState, default_comma: bool = False) -> None:
with state.record_syntactic_position(self):
self.name._codegen(state)
asname = self.asname
if asname is not None:
asname._codegen(state)
comma = self.comma
if comma is MaybeSentinel.DEFAULT and default_comma:
state.add_token(", ")
elif isinstance(comma, Comma):
comma._codegen(state)
def _name(self, node: CSTNode) -> str:
# Unrolled version of get_full_name_for_node to avoid circular imports.
if isinstance(node, Name):
return node.value
elif isinstance(node, Attribute):
return f"{self._name(node.value)}.{node.attr.value}"
else:
raise Exception("Logic error!")
@property
def evaluated_name(self) -> str:
"""
Returns the string name this :class:`ImportAlias` represents.
"""
return self._name(self.name)
@property
def evaluated_alias(self) -> Optional[str]:
"""
Returns the string name for any alias that this :class:`ImportAlias`
has. If there is no ``asname`` attribute, this returns ``None``.
"""
asname = self.asname
if asname is not None:
return self._name(asname.name)
return None
[docs]@add_slots
@dataclass(frozen=True)
class Import(BaseSmallStatement):
"""
An ``import`` statement.
"""
#: One or more names that are being imported, with optional local aliases.
names: Sequence[ImportAlias]
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
#: The whitespace that appears after the ``import`` keyword but before
#: the first import alias.
whitespace_after_import: SimpleWhitespace = SimpleWhitespace.field(" ")
def _validate(self) -> None:
if len(self.names) == 0:
raise CSTValidationError(
"An ImportStatement must have at least one ImportAlias"
)
if isinstance(self.names[-1].comma, Comma):
raise CSTValidationError(
"An ImportStatement does not allow a trailing comma"
)
if self.whitespace_after_import.empty:
raise CSTValidationError("Must have at least one space after import.")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Import":
return Import(
whitespace_after_import=visit_required(
self, "whitespace_after_import", self.whitespace_after_import, visitor
),
names=visit_sequence(self, "names", self.names, visitor),
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
state.add_token("import")
self.whitespace_after_import._codegen(state)
lastname = len(self.names) - 1
for i, name in enumerate(self.names):
name._codegen(state, default_comma=(i != lastname))
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class ImportFrom(BaseSmallStatement):
"""
A ``from x import y`` statement.
"""
#: Name or Attribute node representing the module we're importing from.
#: This is optional as :class:`ImportFrom` allows purely relative imports.
module: Optional[Union[Attribute, Name]]
#: One or more names that are being imported from the specified module,
#: with optional local aliases.
names: Union[Sequence[ImportAlias], ImportStar]
#: Sequence of :class:`Dot` nodes indicating relative import level.
relative: Sequence[Dot] = ()
#: Optional open parenthesis for multi-line import continuation.
lpar: Optional[LeftParen] = None
#: Optional close parenthesis for multi-line import continuation.
rpar: Optional[RightParen] = None
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
#: The whitespace that appears after the ``from`` keyword but before
#: the module and any relative import dots.
whitespace_after_from: SimpleWhitespace = SimpleWhitespace.field(" ")
#: The whitespace that appears after the module but before the
#: ``import`` keyword.
whitespace_before_import: SimpleWhitespace = SimpleWhitespace.field(" ")
#: The whitespace that appears after the ``import`` keyword but
#: before the first import name or optional left paren.
whitespace_after_import: SimpleWhitespace = SimpleWhitespace.field(" ")
def _validate_module(self) -> None:
if self.module is None and len(self.relative) == 0:
raise CSTValidationError(
"Must have a module specified if there is no relative import."
)
def _validate_names(self) -> None:
names = self.names
if isinstance(names, Sequence):
if len(names) == 0:
raise CSTValidationError(
"An ImportFrom must have at least one ImportAlias"
)
for name in names[:-1]:
if name.comma is None:
raise CSTValidationError("Non-final ImportAliases require a comma")
if self.lpar is not None and self.rpar is None:
raise CSTValidationError("Cannot have left paren without right paren.")
if self.lpar is None and self.rpar is not None:
raise CSTValidationError("Cannot have right paren without left paren.")
if isinstance(names, ImportStar):
if self.lpar is not None or self.rpar is not None:
raise CSTValidationError(
"An ImportFrom using ImportStar cannot have parens"
)
def _validate_whitespace(self) -> None:
if self.whitespace_after_from.empty and not self.relative:
raise CSTValidationError("Must have at least one space after from.")
if self.whitespace_before_import.empty and not (
self.relative and self.module is None
):
raise CSTValidationError("Must have at least one space before import.")
if (
self.whitespace_after_import.empty
and self.lpar is None
and not isinstance(self.names, ImportStar)
):
raise CSTValidationError("Must have at least one space after import.")
def _validate(self) -> None:
self._validate_module()
self._validate_names()
self._validate_whitespace()
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "ImportFrom":
names = self.names
return ImportFrom(
whitespace_after_from=visit_required(
self, "whitespace_after_from", self.whitespace_after_from, visitor
),
relative=visit_sequence(self, "relative", self.relative, visitor),
module=visit_optional(self, "module", self.module, visitor),
whitespace_before_import=visit_required(
self, "whitespace_before_import", self.whitespace_before_import, visitor
),
whitespace_after_import=visit_required(
self, "whitespace_after_import", self.whitespace_after_import, visitor
),
lpar=visit_optional(self, "lpar", self.lpar, visitor),
names=(
visit_required(self, "names", names, visitor)
if isinstance(names, ImportStar)
else visit_sequence(self, "names", names, visitor)
),
rpar=visit_optional(self, "rpar", self.rpar, visitor),
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
names = self.names
end_node = names[-1] if isinstance(names, Sequence) else names
end_node = end_node if self.rpar is None else self.rpar
with state.record_syntactic_position(self, end_node=end_node):
state.add_token("from")
self.whitespace_after_from._codegen(state)
for dot in self.relative:
dot._codegen(state)
module = self.module
if module is not None:
module._codegen(state)
self.whitespace_before_import._codegen(state)
state.add_token("import")
self.whitespace_after_import._codegen(state)
lpar = self.lpar
if lpar is not None:
lpar._codegen(state)
if isinstance(names, Sequence):
lastname = len(names) - 1
for i, name in enumerate(names):
name._codegen(state, default_comma=(i != lastname))
if isinstance(names, ImportStar):
names._codegen(state)
rpar = self.rpar
if rpar is not None:
rpar._codegen(state)
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class AssignTarget(CSTNode):
"""
A target for an :class:`Assign`. Owns the equals sign and the whitespace around it.
"""
#: The target expression being assigned to.
target: BaseAssignTargetExpression
#: The whitespace appearing before the equals sign.
whitespace_before_equal: SimpleWhitespace = SimpleWhitespace.field(" ")
#: The whitespace appearing after the equals sign.
whitespace_after_equal: SimpleWhitespace = SimpleWhitespace.field(" ")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "AssignTarget":
return AssignTarget(
target=visit_required(self, "target", self.target, visitor),
whitespace_before_equal=visit_required(
self, "whitespace_before_equal", self.whitespace_before_equal, visitor
),
whitespace_after_equal=visit_required(
self, "whitespace_after_equal", self.whitespace_after_equal, visitor
),
)
def _codegen_impl(self, state: CodegenState) -> None:
with state.record_syntactic_position(self):
self.target._codegen(state)
self.whitespace_before_equal._codegen(state)
state.add_token("=")
self.whitespace_after_equal._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class Assign(BaseSmallStatement):
"""
An assignment statement such as ``x = y`` or ``x = y = z``. Unlike
:class:`AnnAssign`, this does not allow type annotations but does
allow for multiple targets.
"""
#: One or more targets that are being assigned to.
targets: Sequence[AssignTarget]
#: The expression being assigned to the targets.
value: BaseExpression
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _validate(self) -> None:
if len(self.targets) == 0:
raise CSTValidationError(
"An Assign statement must have at least one AssignTarget"
)
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Assign":
return Assign(
targets=visit_sequence(self, "targets", self.targets, visitor),
value=visit_required(self, "value", self.value, visitor),
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
for target in self.targets:
target._codegen(state)
self.value._codegen(state)
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class AnnAssign(BaseSmallStatement):
"""
An assignment statement such as ``x: int = 5`` or ``x: int``. This only
allows for one assignment target unlike :class:`Assign` but it includes
a variable annotation. Also unlike :class:`Assign`, the assignment target
is optional, as it is possible to annotate an expression without assigning
to it.
"""
#: The target that is being annotated and possibly assigned to.
target: BaseAssignTargetExpression
#: The annotation for the target.
annotation: Annotation
#: The optional expression being assigned to the target.
value: Optional[BaseExpression] = None
#: The equals sign used to denote assignment if there is a value.
equal: Union[AssignEqual, MaybeSentinel] = MaybeSentinel.DEFAULT
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _validate(self) -> None:
if self.value is None and isinstance(self.equal, AssignEqual):
raise CSTValidationError(
"Must have a value when specifying an AssignEqual."
)
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "AnnAssign":
return AnnAssign(
target=visit_required(self, "target", self.target, visitor),
annotation=visit_required(self, "annotation", self.annotation, visitor),
equal=visit_sentinel(self, "equal", self.equal, visitor),
value=visit_optional(self, "value", self.value, visitor),
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
self.target._codegen(state)
self.annotation._codegen(state, default_indicator=":")
equal = self.equal
if equal is MaybeSentinel.DEFAULT and self.value is not None:
state.add_token(" = ")
elif isinstance(equal, AssignEqual):
equal._codegen(state)
value = self.value
if value is not None:
value._codegen(state)
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class AugAssign(BaseSmallStatement):
"""
An augmented assignment statement, such as ``x += 5``.
"""
#: Target that is being operated on and assigned to.
target: BaseAssignTargetExpression
#: The augmented assignment operation being performed.
operator: BaseAugOp
#: The value used with the above operator to calculate the new assignment.
value: BaseExpression
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "AugAssign":
return AugAssign(
target=visit_required(self, "target", self.target, visitor),
operator=visit_required(self, "operator", self.operator, visitor),
value=visit_required(self, "value", self.value, visitor),
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
self.target._codegen(state)
self.operator._codegen(state)
self.value._codegen(state)
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class Decorator(CSTNode):
"""
A single decorator that decorates a :class:`FunctionDef` or a :class:`ClassDef`.
"""
#: The decorator that will return a new function wrapping the parent
#: of this decorator.
decorator: Union[Name, Attribute, Call]
#: Line comments and empty lines before this decorator. The parent
#: :class:`FunctionDef` or :class:`ClassDef` node owns leading lines before
#: the first decorator so that if the first decorator is removed, spacing is preserved.
leading_lines: Sequence[EmptyLine] = ()
#: Whitespace after the ``@`` and before the decorator expression itself.
whitespace_after_at: SimpleWhitespace = SimpleWhitespace.field("")
#: Optional trailing comment and newline following the decorator before the next line.
trailing_whitespace: TrailingWhitespace = TrailingWhitespace.field()
def _validate(self) -> None:
decorator = self.decorator
if len(decorator.lpar) > 0 or len(decorator.rpar) > 0:
raise CSTValidationError(
"Cannot have parens around decorator in a Decorator."
)
if isinstance(decorator, Call) and not isinstance(
decorator.func, (Name, Attribute)
):
raise CSTValidationError(
"Decorator call function must be Name or Attribute node."
)
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Decorator":
return Decorator(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
whitespace_after_at=visit_required(
self, "whitespace_after_at", self.whitespace_after_at, visitor
),
decorator=visit_required(self, "decorator", self.decorator, visitor),
trailing_whitespace=visit_required(
self, "trailing_whitespace", self.trailing_whitespace, visitor
),
)
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
with state.record_syntactic_position(self):
state.add_token("@")
self.whitespace_after_at._codegen(state)
self.decorator._codegen(state)
self.trailing_whitespace._codegen(state)
def get_docstring_impl(
body: Union[BaseSuite, Sequence[Union[SimpleStatementLine, BaseCompoundStatement]]],
clean: bool,
) -> Optional[str]:
"""
Implementation Reference:
- :func:`ast.get_docstring` https://docs.python.org/3/library/ast.html#ast.get_docstring
and https://github.com/python/cpython/blob/89aa4694fc8c6d190325ef8ed6ce6a6b8efb3e50/Lib/ast.py#L254
- PEP 257 https://www.python.org/dev/peps/pep-0257/
"""
if isinstance(body, Sequence):
if body:
expr = body[0]
else:
return None
else:
expr = body
while isinstance(expr, (BaseSuite, SimpleStatementLine)):
if len(expr.body) == 0:
return None
expr = expr.body[0]
if not isinstance(expr, Expr):
return None
val = expr.value
if isinstance(val, (SimpleString, ConcatenatedString)):
evaluated_value = val.evaluated_value
else:
return None
if evaluated_value is not None and clean:
return inspect.cleandoc(evaluated_value)
return evaluated_value
[docs]@add_slots
@dataclass(frozen=True)
class FunctionDef(BaseCompoundStatement):
"""
A function definition.
"""
#: The function name.
name: Name
#: The function parameters. Present even if there are no params.
params: Parameters
#: The function body.
body: BaseSuite
#: Sequence of decorators applied to this function. Decorators are listed in
#: order that they appear in source (top to bottom) as apposed to the order
#: that they are applied to the function at runtime.
decorators: Sequence[Decorator] = ()
#: An optional return annotation, if the function is annotated.
returns: Optional[Annotation] = None
#: Optional async modifier, if this is an async function.
asynchronous: Optional[Asynchronous] = None
#: Leading empty lines and comments before the first decorator. We
#: assume any comments before the first decorator are owned by the
#: function definition itself. If there are no decorators, this will
#: still contain all of the empty lines and comments before the
#: function definition.
leading_lines: Sequence[EmptyLine] = ()
#: Empty lines and comments between the final decorator and the
#: :class:`FunctionDef` node. In the case of no decorators, this will be empty.
lines_after_decorators: Sequence[EmptyLine] = ()
#: Whitespace after the ``def`` keyword and before the function name.
whitespace_after_def: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Whitespace after the function name and before the opening parenthesis for
#: the parameters.
whitespace_after_name: SimpleWhitespace = SimpleWhitespace.field("")
#: Whitespace after the opening parenthesis for the parameters but before
#: the first param itself.
whitespace_before_params: BaseParenthesizableWhitespace = SimpleWhitespace.field("")
#: Whitespace after the closing parenthesis or return annotation and before
#: the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")
def _validate(self) -> None:
if len(self.name.lpar) > 0 or len(self.name.rpar) > 0:
raise CSTValidationError("Cannot have parens around Name in a FunctionDef.")
if self.whitespace_after_def.empty:
raise CSTValidationError(
"There must be at least one space between 'def' and name."
)
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "FunctionDef":
return FunctionDef(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
decorators=visit_sequence(self, "decorators", self.decorators, visitor),
lines_after_decorators=visit_sequence(
self, "lines_after_decorators", self.lines_after_decorators, visitor
),
asynchronous=visit_optional(
self, "asynchronous", self.asynchronous, visitor
),
whitespace_after_def=visit_required(
self, "whitespace_after_def", self.whitespace_after_def, visitor
),
name=visit_required(self, "name", self.name, visitor),
whitespace_after_name=visit_required(
self, "whitespace_after_name", self.whitespace_after_name, visitor
),
whitespace_before_params=visit_required(
self, "whitespace_before_params", self.whitespace_before_params, visitor
),
params=visit_required(self, "params", self.params, visitor),
returns=visit_optional(self, "returns", self.returns, visitor),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
body=visit_required(self, "body", self.body, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
for decorator in self.decorators:
decorator._codegen(state)
for lad in self.lines_after_decorators:
lad._codegen(state)
state.add_indent_tokens()
with state.record_syntactic_position(self, end_node=self.body):
asynchronous = self.asynchronous
if asynchronous is not None:
asynchronous._codegen(state)
state.add_token("def")
self.whitespace_after_def._codegen(state)
self.name._codegen(state)
self.whitespace_after_name._codegen(state)
state.add_token("(")
self.whitespace_before_params._codegen(state)
self.params._codegen(state)
state.add_token(")")
returns = self.returns
if returns is not None:
returns._codegen(state, default_indicator="->")
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)
[docs] def get_docstring(self, clean: bool = True) -> Optional[str]:
"""
When docstring is available, returns a :func:`inspect.cleandoc` cleaned docstring.
Otherwise, returns ``None``.
"""
return get_docstring_impl(self.body, clean)
[docs]@add_slots
@dataclass(frozen=True)
class ClassDef(BaseCompoundStatement):
"""
A class definition.
"""
#: The class name.
name: Name
#: The class body.
body: BaseSuite
#: Sequence of base classes this class inherits from.
bases: Sequence[Arg] = ()
#: Sequence of keywords, such as "metaclass".
keywords: Sequence[Arg] = ()
#: Sequence of decorators applied to this class.
decorators: Sequence[Decorator] = ()
#: Optional open parenthesis used when there are bases or keywords.
lpar: Union[LeftParen, MaybeSentinel] = MaybeSentinel.DEFAULT
#: Optional close parenthesis used when there are bases or keywords.
rpar: Union[RightParen, MaybeSentinel] = MaybeSentinel.DEFAULT
#: Leading empty lines and comments before the first decorator. We
#: assume any comments before the first decorator are owned by the
#: class definition itself. If there are no decorators, this will
#: still contain all of the empty lines and comments before the
#: class definition.
leading_lines: Sequence[EmptyLine] = ()
#: Empty lines and comments between the final decorator and the
#: :class:`ClassDef` node. In the case of no decorators, this will be empty.
lines_after_decorators: Sequence[EmptyLine] = ()
#: Whitespace after the ``class`` keyword and before the class name.
whitespace_after_class: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Whitespace after the class name and before the opening parenthesis for
#: the bases and keywords.
whitespace_after_name: SimpleWhitespace = SimpleWhitespace.field("")
#: Whitespace after the closing parenthesis or class name and before
#: the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")
def _validate_whitespace(self) -> None:
if self.whitespace_after_class.empty:
raise CSTValidationError(
"There must be at least one space between 'class' and name."
)
def _validate_parens(self) -> None:
if len(self.name.lpar) > 0 or len(self.name.rpar) > 0:
raise CSTValidationError("Cannot have parens around Name in a ClassDef.")
if isinstance(self.lpar, MaybeSentinel) and isinstance(self.rpar, RightParen):
raise CSTValidationError(
"Do not mix concrete LeftParen/RightParen with MaybeSentinel."
)
if isinstance(self.lpar, LeftParen) and isinstance(self.rpar, MaybeSentinel):
raise CSTValidationError(
"Do not mix concrete LeftParen/RightParen with MaybeSentinel."
)
def _validate_args(self) -> None:
if any((arg.keyword is not None) for arg in self.bases):
raise CSTValidationError("Bases must be arguments without keywords.")
if any((arg.keyword is None and arg.star != "**") for arg in self.keywords):
raise CSTValidationError(
"Keywords must be arguments with keywords or dictionary expansions."
)
def _validate(self) -> None:
self._validate_whitespace()
self._validate_parens()
self._validate_args()
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "ClassDef":
return ClassDef(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
decorators=visit_sequence(self, "decorators", self.decorators, visitor),
lines_after_decorators=visit_sequence(
self, "lines_after_decorators", self.lines_after_decorators, visitor
),
whitespace_after_class=visit_required(
self, "whitespace_after_class", self.whitespace_after_class, visitor
),
name=visit_required(self, "name", self.name, visitor),
whitespace_after_name=visit_required(
self, "whitespace_after_name", self.whitespace_after_name, visitor
),
lpar=visit_sentinel(self, "lpar", self.lpar, visitor),
bases=visit_sequence(self, "bases", self.bases, visitor),
keywords=visit_sequence(self, "keywords", self.keywords, visitor),
rpar=visit_sentinel(self, "rpar", self.rpar, visitor),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
body=visit_required(self, "body", self.body, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None: # noqa: C901
for ll in self.leading_lines:
ll._codegen(state)
for decorator in self.decorators:
decorator._codegen(state)
for lad in self.lines_after_decorators:
lad._codegen(state)
state.add_indent_tokens()
with state.record_syntactic_position(self, end_node=self.body):
state.add_token("class")
self.whitespace_after_class._codegen(state)
self.name._codegen(state)
self.whitespace_after_name._codegen(state)
lpar = self.lpar
if isinstance(lpar, MaybeSentinel):
if self.bases or self.keywords:
state.add_token("(")
elif isinstance(lpar, LeftParen):
lpar._codegen(state)
args = [*self.bases, *self.keywords]
last_arg = len(args) - 1
for i, arg in enumerate(args):
arg._codegen(state, default_comma=(i != last_arg))
rpar = self.rpar
if isinstance(rpar, MaybeSentinel):
if self.bases or self.keywords:
state.add_token(")")
elif isinstance(rpar, RightParen):
rpar._codegen(state)
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)
[docs] def get_docstring(self, clean: bool = True) -> Optional[str]:
"""
Returns a :func:`inspect.cleandoc` cleaned docstring if the docstring is available, ``None`` otherwise.
"""
return get_docstring_impl(self.body, clean)
[docs]@add_slots
@dataclass(frozen=True)
class WithItem(CSTNode):
"""
A single context manager in a :class:`With` block, with an optional variable name.
"""
#: Expression that evaluates to a context manager.
item: BaseExpression
#: Variable to assign the context manager to, if it is needed in the
#: :class:`With` body.
asname: Optional[AsName] = None
#: This is forbidden for the last :class:`WithItem` in a :class:`With`, but all
#: other items inside a with block must contain a comma to separate them.
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
def _validate(self) -> None:
asname = self.asname
if (
asname is not None
and asname.whitespace_before_as.empty
and not self.item._safe_to_use_with_word_operator(ExpressionPosition.LEFT)
):
raise CSTValidationError("Must have at least one space before as keyword.")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "WithItem":
return WithItem(
item=visit_required(self, "item", self.item, visitor),
asname=visit_optional(self, "asname", self.asname, visitor),
comma=visit_sentinel(self, "comma", self.comma, visitor),
)
def _codegen_impl(self, state: CodegenState, default_comma: bool = False) -> None:
with state.record_syntactic_position(self):
self.item._codegen(state)
asname = self.asname
if asname is not None:
asname._codegen(state)
comma = self.comma
if comma is MaybeSentinel.DEFAULT and default_comma:
state.add_token(", ")
elif isinstance(comma, Comma):
comma._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class With(BaseCompoundStatement):
"""
A ``with`` statement.
"""
#: A sequence of one or more items that evaluate to context managers.
items: Sequence[WithItem]
#: The suite that is wrapped with this statement.
body: BaseSuite
#: Optional async modifier if this is an ``async with`` statement.
asynchronous: Optional[Asynchronous] = None
#: Sequence of empty lines appearing before this with statement.
leading_lines: Sequence[EmptyLine] = ()
#: Optional open parenthesis for multi-line with bindings
lpar: Union[LeftParen, MaybeSentinel] = MaybeSentinel.DEFAULT
#: Optional close parenthesis for multi-line with bindings
rpar: Union[RightParen, MaybeSentinel] = MaybeSentinel.DEFAULT
#: Whitespace after the ``with`` keyword and before the first item.
whitespace_after_with: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Whitespace after the last item and before the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")
def _validate_parens(self) -> None:
if isinstance(self.lpar, MaybeSentinel) and isinstance(self.rpar, RightParen):
raise CSTValidationError(
"Do not mix concrete LeftParen/RightParen with MaybeSentinel."
)
if isinstance(self.lpar, LeftParen) and isinstance(self.rpar, MaybeSentinel):
raise CSTValidationError(
"Do not mix concrete LeftParen/RightParen with MaybeSentinel."
)
def _validate(self) -> None:
self._validate_parens()
if len(self.items) == 0:
raise CSTValidationError(
"A With statement must have at least one WithItem."
)
if (
isinstance(self.rpar, MaybeSentinel)
and self.items[-1].comma != MaybeSentinel.DEFAULT
):
raise CSTValidationError(
"The last WithItem in an unparenthesized With cannot have a trailing comma."
)
if self.whitespace_after_with.empty and not (
isinstance(self.lpar, LeftParen)
or self.items[0].item._safe_to_use_with_word_operator(
ExpressionPosition.RIGHT
)
):
raise CSTValidationError("Must have at least one space after with keyword.")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "With":
return With(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
asynchronous=visit_optional(
self, "asynchronous", self.asynchronous, visitor
),
whitespace_after_with=visit_required(
self, "whitespace_after_with", self.whitespace_after_with, visitor
),
lpar=visit_sentinel(self, "lpar", self.lpar, visitor),
items=visit_sequence(self, "items", self.items, visitor),
rpar=visit_sentinel(self, "rpar", self.rpar, visitor),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
body=visit_required(self, "body", self.body, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
needs_paren = False
for item in self.items:
comma = item.comma
if isinstance(comma, Comma):
if isinstance(
comma.whitespace_after,
(EmptyLine, TrailingWhitespace, ParenthesizedWhitespace),
):
needs_paren = True
break
with state.record_syntactic_position(self, end_node=self.body):
asynchronous = self.asynchronous
if asynchronous is not None:
asynchronous._codegen(state)
state.add_token("with")
self.whitespace_after_with._codegen(state)
lpar = self.lpar
if isinstance(lpar, LeftParen):
lpar._codegen(state)
elif needs_paren:
state.add_token("(")
last_item = len(self.items) - 1
for i, item in enumerate(self.items):
item._codegen(state, default_comma=(i != last_item))
rpar = self.rpar
if isinstance(rpar, RightParen):
rpar._codegen(state)
elif needs_paren:
state.add_token(")")
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class For(BaseCompoundStatement):
"""
A ``for target in iter`` statement.
"""
#: The target of the iterator in the for statement.
target: BaseAssignTargetExpression
#: The iterable expression we will loop over.
iter: BaseExpression
#: The suite that is wrapped with this statement.
body: BaseSuite
#: An optional else case which will be executed if there is no
#: :class:`Break` statement encountered while looping.
orelse: Optional[Else] = None
#: Optional async modifier, if this is an `async for` statement.
asynchronous: Optional[Asynchronous] = None
#: Sequence of empty lines appearing before this for statement.
leading_lines: Sequence[EmptyLine] = ()
#: Whitespace after the ``for`` keyword and before the target.
whitespace_after_for: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Whitespace after the target and before the ``in`` keyword.
whitespace_before_in: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Whitespace after the ``in`` keyword and before the iter.
whitespace_after_in: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Whitespace after the iter and before the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")
def _validate(self) -> None:
if (
self.whitespace_after_for.empty
and not self.target._safe_to_use_with_word_operator(
ExpressionPosition.RIGHT
)
):
raise CSTValidationError(
"Must have at least one space after 'for' keyword."
)
if (
self.whitespace_before_in.empty
and not self.target._safe_to_use_with_word_operator(ExpressionPosition.LEFT)
):
raise CSTValidationError(
"Must have at least one space before 'in' keyword."
)
if (
self.whitespace_after_in.empty
and not self.iter._safe_to_use_with_word_operator(ExpressionPosition.RIGHT)
):
raise CSTValidationError("Must have at least one space after 'in' keyword.")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "For":
return For(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
asynchronous=visit_optional(
self, "asynchronous", self.asynchronous, visitor
),
whitespace_after_for=visit_required(
self, "whitespace_after_for", self.whitespace_after_for, visitor
),
target=visit_required(self, "target", self.target, visitor),
whitespace_before_in=visit_required(
self, "whitespace_before_in", self.whitespace_before_in, visitor
),
whitespace_after_in=visit_required(
self, "whitespace_after_in", self.whitespace_after_in, visitor
),
iter=visit_required(self, "iter", self.iter, visitor),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
body=visit_required(self, "body", self.body, visitor),
orelse=visit_optional(self, "orelse", self.orelse, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
end_node = self.body if self.orelse is None else self.orelse
with state.record_syntactic_position(self, end_node=end_node):
asynchronous = self.asynchronous
if asynchronous is not None:
asynchronous._codegen(state)
state.add_token("for")
self.whitespace_after_for._codegen(state)
self.target._codegen(state)
self.whitespace_before_in._codegen(state)
state.add_token("in")
self.whitespace_after_in._codegen(state)
self.iter._codegen(state)
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)
orelse = self.orelse
if orelse is not None:
orelse._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class While(BaseCompoundStatement):
"""
A ``while`` statement.
"""
#: The test we will loop against.
test: BaseExpression
#: The suite that is wrapped with this statement.
body: BaseSuite
#: An optional else case which will be executed if there is no
#: :class:`Break` statement encountered while looping.
orelse: Optional[Else] = None
#: Sequence of empty lines appearing before this while statement.
leading_lines: Sequence[EmptyLine] = ()
#: Whitespace after the ``while`` keyword and before the test.
whitespace_after_while: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Whitespace after the test and before the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")
def _validate(self) -> None:
if (
self.whitespace_after_while.empty
and not self.test._safe_to_use_with_word_operator(ExpressionPosition.RIGHT)
):
raise CSTValidationError(
"Must have at least one space after 'while' keyword."
)
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "While":
return While(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
whitespace_after_while=visit_required(
self, "whitespace_after_while", self.whitespace_after_while, visitor
),
test=visit_required(self, "test", self.test, visitor),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
body=visit_required(self, "body", self.body, visitor),
orelse=visit_optional(self, "orelse", self.orelse, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
end_node = self.body if self.orelse is None else self.orelse
with state.record_syntactic_position(self, end_node=end_node):
state.add_token("while")
self.whitespace_after_while._codegen(state)
self.test._codegen(state)
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)
orelse = self.orelse
if orelse is not None:
orelse._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class Raise(BaseSmallStatement):
"""
A ``raise exc`` or ``raise exc from cause`` statement.
"""
#: The exception that we should raise.
exc: Optional[BaseExpression] = None
#: Optionally, a ``from cause`` clause to allow us to raise an exception
#: out of another exception's context.
cause: Optional[From] = None
#: Any whitespace appearing between the ``raise`` keyword and the exception.
whitespace_after_raise: Union[
SimpleWhitespace, MaybeSentinel
] = MaybeSentinel.DEFAULT
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _validate(self) -> None:
# Validate correct construction
if self.exc is None and self.cause is not None:
raise CSTValidationError(
"Must have an 'exc' when specifying 'clause'. on Raise."
)
# Validate spacing between "raise" and "exc"
exc = self.exc
if exc is not None:
whitespace_after_raise = self.whitespace_after_raise
has_no_gap = (
not isinstance(whitespace_after_raise, MaybeSentinel)
and whitespace_after_raise.empty
)
if has_no_gap and not exc._safe_to_use_with_word_operator(
ExpressionPosition.RIGHT
):
raise CSTValidationError("Must have at least one space after 'raise'.")
# Validate spacing between "exc" and "from"
cause = self.cause
if exc is not None and cause is not None:
whitespace_before_from = cause.whitespace_before_from
has_no_gap = (
not isinstance(whitespace_before_from, MaybeSentinel)
and whitespace_before_from.empty
)
if has_no_gap and not exc._safe_to_use_with_word_operator(
ExpressionPosition.LEFT
):
raise CSTValidationError("Must have at least one space before 'from'.")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Raise":
return Raise(
whitespace_after_raise=visit_sentinel(
self, "whitespace_after_raise", self.whitespace_after_raise, visitor
),
exc=visit_optional(self, "exc", self.exc, visitor),
cause=visit_optional(self, "cause", self.cause, visitor),
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
exc = self.exc
cause = self.cause
state.add_token("raise")
whitespace_after_raise = self.whitespace_after_raise
if isinstance(whitespace_after_raise, MaybeSentinel):
if exc is not None:
state.add_token(" ")
else:
whitespace_after_raise._codegen(state)
if exc is not None:
exc._codegen(state)
if cause is not None:
cause._codegen(state, default_space=" ")
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class Assert(BaseSmallStatement):
"""
An assert statement such as ``assert x > 5`` or ``assert x > 5, 'Uh oh!'``
"""
#: The test we are going to assert on.
test: BaseExpression
#: The optional message to display if the test evaluates to a falsey value.
msg: Optional[BaseExpression] = None
#: A comma separating test and message, if there is a message.
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
#: Whitespace appearing after the ``assert`` keyword and before the test.
whitespace_after_assert: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _validate(self) -> None:
# Validate whitespace
if (
self.whitespace_after_assert.empty
and not self.test._safe_to_use_with_word_operator(ExpressionPosition.RIGHT)
):
raise CSTValidationError("Must have at least one space after 'assert'.")
# Validate comma rules
if self.msg is None and isinstance(self.comma, Comma):
raise CSTValidationError("Cannot have trailing comma after 'test'.")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Assert":
return Assert(
whitespace_after_assert=visit_required(
self, "whitespace_after_assert", self.whitespace_after_assert, visitor
),
test=visit_required(self, "test", self.test, visitor),
comma=visit_sentinel(self, "comma", self.comma, visitor),
msg=visit_optional(self, "msg", self.msg, visitor),
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
state.add_token("assert")
self.whitespace_after_assert._codegen(state)
self.test._codegen(state)
comma = self.comma
msg = self.msg
if isinstance(comma, MaybeSentinel):
if msg is not None:
state.add_token(", ")
else:
comma._codegen(state)
if msg is not None:
msg._codegen(state)
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class NameItem(CSTNode):
"""
A single identifier name inside a :class:`Global` or :class:`Nonlocal` statement.
This exists because a list of names in a ``global`` or ``nonlocal`` statement need
to be separated by a comma, which ends up owned by the :class:`NameItem` node.
"""
#: Identifier name.
name: Name
#: This is forbidden for the last :class:`NameItem` in a
#: :class:`Global`/:class:`Nonlocal`, but all other tems inside a ``global`` or
#: ``nonlocal`` statement must contain a comma to separate them.
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
def _validate(self) -> None:
# No parens around names here
if len(self.name.lpar) > 0 or len(self.name.rpar) > 0:
raise CSTValidationError("Cannot have parens around names in NameItem.")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "NameItem":
return NameItem(
name=visit_required(self, "name", self.name, visitor),
comma=visit_sentinel(self, "comma", self.comma, visitor),
)
def _codegen_impl(self, state: CodegenState, default_comma: bool = False) -> None:
with state.record_syntactic_position(self):
self.name._codegen(state)
comma = self.comma
if comma is MaybeSentinel.DEFAULT and default_comma:
state.add_token(", ")
elif isinstance(comma, Comma):
comma._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class Global(BaseSmallStatement):
"""
A ``global`` statement.
"""
#: A list of one or more names.
names: Sequence[NameItem]
#: Whitespace appearing after the ``global`` keyword and before the first name.
whitespace_after_global: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _validate(self) -> None:
if len(self.names) == 0:
raise CSTValidationError(
"A Global statement must have at least one NameItem."
)
if self.names[-1].comma != MaybeSentinel.DEFAULT:
raise CSTValidationError(
"The last NameItem in a Global cannot have a trailing comma."
)
if self.whitespace_after_global.empty:
raise CSTValidationError(
"Must have at least one space after 'global' keyword."
)
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Global":
return Global(
whitespace_after_global=visit_required(
self, "whitespace_after_global", self.whitespace_after_global, visitor
),
names=visit_sequence(self, "names", self.names, visitor),
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
state.add_token("global")
self.whitespace_after_global._codegen(state)
last_name = len(self.names) - 1
for i, name in enumerate(self.names):
name._codegen(state, default_comma=(i != last_name))
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
[docs]@add_slots
@dataclass(frozen=True)
class Nonlocal(BaseSmallStatement):
"""
A ``nonlocal`` statement.
"""
#: A list of one or more names.
names: Sequence[NameItem]
#: Whitespace appearing after the ``global`` keyword and before the first name.
whitespace_after_nonlocal: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Optional semicolon when this is used in a statement line. This semicolon
#: owns the whitespace on both sides of it when it is used.
semicolon: Union[Semicolon, MaybeSentinel] = MaybeSentinel.DEFAULT
def _validate(self) -> None:
if len(self.names) == 0:
raise CSTValidationError(
"A Nonlocal statement must have at least one NameItem."
)
if self.names[-1].comma != MaybeSentinel.DEFAULT:
raise CSTValidationError(
"The last NameItem in a Nonlocal cannot have a trailing comma."
)
if self.whitespace_after_nonlocal.empty:
raise CSTValidationError(
"Must have at least one space after 'nonlocal' keyword."
)
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Nonlocal":
return Nonlocal(
whitespace_after_nonlocal=visit_required(
self,
"whitespace_after_nonlocal",
self.whitespace_after_nonlocal,
visitor,
),
names=visit_sequence(self, "names", self.names, visitor),
semicolon=visit_sentinel(self, "semicolon", self.semicolon, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_semicolon: bool = False
) -> None:
with state.record_syntactic_position(self):
state.add_token("nonlocal")
self.whitespace_after_nonlocal._codegen(state)
last_name = len(self.names) - 1
for i, name in enumerate(self.names):
name._codegen(state, default_comma=(i != last_name))
semicolon = self.semicolon
if isinstance(semicolon, MaybeSentinel):
if default_semicolon:
state.add_token("; ")
elif isinstance(semicolon, Semicolon):
semicolon._codegen(state)
class MatchPattern(_BaseParenthesizedNode, ABC):
"""
A base class for anything that can appear as a pattern in a :class:`Match`
statement.
"""
__slots__ = ()
@add_slots
@dataclass(frozen=True)
# pyre-fixme[13]: Attribute `body` is never initialized.
class Match(BaseCompoundStatement):
"""
A ``match`` statement.
"""
#: The subject of the match.
subject: BaseExpression
#: A non-empty list of match cases.
cases: Sequence["MatchCase"]
#: Sequence of empty lines appearing before this compound statement line.
leading_lines: Sequence[EmptyLine] = ()
#: Whitespace between the ``match`` keyword and the subject.
whitespace_after_match: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Whitespace after the subject but before the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")
#: Any optional trailing comment and the final ``NEWLINE`` at the end of the line.
whitespace_after_colon: TrailingWhitespace = TrailingWhitespace.field()
#: A string represents a specific indentation. A ``None`` value uses the modules's
#: default indentation. This is included because indentation is allowed to be
#: inconsistent across a file, just not ambiguously.
indent: Optional[str] = None
#: Any trailing comments or lines after the dedent that are owned by this match
#: block. Statements own preceeding and same-line trailing comments, but not
#: trailing lines, so it falls on :class:`Match` to own it. In the case
#: that a statement follows a :class:`Match` block, that statement will own the
#: comments and lines that are at the same indent as the statement, and this
#: :class:`Match` will own the comments and lines that are indented further.
footer: Sequence[EmptyLine] = ()
def _validate(self) -> None:
if len(self.cases) == 0:
raise CSTValidationError("A match statement must have at least one case.")
indent = self.indent
if indent is not None:
if len(indent) == 0:
raise CSTValidationError(
"A match statement must have a non-zero width indent."
)
if _INDENT_WHITESPACE_RE.fullmatch(indent) is None:
raise CSTValidationError(
"An indent must be composed of only whitespace characters."
)
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Match":
return Match(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
whitespace_after_match=visit_required(
self, "whitespace_after_match", self.whitespace_after_match, visitor
),
subject=visit_required(self, "subject", self.subject, visitor),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
whitespace_after_colon=visit_required(
self, "whitespace_after_colon", self.whitespace_after_colon, visitor
),
indent=self.indent,
cases=visit_sequence(self, "cases", self.cases, visitor),
footer=visit_sequence(self, "footer", self.footer, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
with state.record_syntactic_position(self, end_node=self.cases[-1]):
state.add_token("match")
self.whitespace_after_match._codegen(state)
self.subject._codegen(state)
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.whitespace_after_colon._codegen(state)
indent = self.indent
state.increase_indent(state.default_indent if indent is None else indent)
for c in self.cases:
c._codegen(state)
for f in self.footer:
f._codegen(state)
state.decrease_indent()
@add_slots
@dataclass(frozen=True)
class MatchCase(CSTNode):
"""
A single ``case`` block of a :class:`Match` statement.
"""
#: The pattern that ``subject`` will be matched against.
pattern: MatchPattern
#: The body of this case block, to be evaluated if ``pattern`` matches ``subject``
#: and ``guard`` evaluates to a truthy value.
body: BaseSuite
#: Optional expression that will be evaluated if ``pattern`` matches ``subject``.
guard: Optional[BaseExpression] = None
#: Sequence of empty lines appearing before this case block.
leading_lines: Sequence[EmptyLine] = ()
#: Whitespace directly after the ``case`` keyword.
whitespace_after_case: SimpleWhitespace = SimpleWhitespace.field(" ")
#: Whitespace before the ``if`` keyword in case there's a guard expression.
whitespace_before_if: SimpleWhitespace = SimpleWhitespace.field("")
#: Whitespace after the ``if`` keyword in case there's a guard expression.
whitespace_after_if: SimpleWhitespace = SimpleWhitespace.field("")
#: Whitespace before the colon.
whitespace_before_colon: SimpleWhitespace = SimpleWhitespace.field("")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "CSTNode":
return MatchCase(
leading_lines=visit_sequence(
self, "leading_lines", self.leading_lines, visitor
),
whitespace_after_case=visit_required(
self, "whitespace_after_case", self.whitespace_after_case, visitor
),
pattern=visit_required(self, "pattern", self.pattern, visitor),
# pyre-fixme[6]: Expected `SimpleWhitespace` for 4th param but got
# `Optional[SimpleWhitespace]`.
whitespace_before_if=visit_optional(
self, "whitespace_before_if", self.whitespace_before_if, visitor
),
# pyre-fixme[6]: Expected `SimpleWhitespace` for 5th param but got
# `Optional[SimpleWhitespace]`.
whitespace_after_if=visit_optional(
self, "whitespace_after_if", self.whitespace_after_if, visitor
),
guard=visit_optional(self, "guard", self.guard, visitor),
body=visit_required(self, "body", self.body, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
for ll in self.leading_lines:
ll._codegen(state)
state.add_indent_tokens()
with state.record_syntactic_position(self, end_node=self.body):
state.add_token("case")
self.whitespace_after_case._codegen(state)
self.pattern._codegen(state)
guard = self.guard
if guard is not None:
self.whitespace_before_if._codegen(state)
state.add_token("if")
self.whitespace_after_if._codegen(state)
guard._codegen(state)
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.body._codegen(state)
@add_slots
@dataclass(frozen=True)
class MatchValue(MatchPattern):
"""
A match literal or value pattern that compares by equality.
"""
#: an expression to compare to
value: BaseExpression
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "CSTNode":
return MatchValue(value=visit_required(self, "value", self.value, visitor))
def _codegen_impl(self, state: CodegenState) -> None:
with state.record_syntactic_position(self):
self.value._codegen(state)
@property
def lpar(self) -> Sequence[LeftParen]:
return self.value.lpar
@lpar.setter
def lpar(self, value: Sequence[LeftParen]) -> None:
self.value.lpar = value
@property
def rpar(self) -> Sequence[RightParen]:
return self.value.rpar
@rpar.setter
def rpar(self, value: Sequence[RightParen]) -> None:
self.value.rpar = value
@add_slots
@dataclass(frozen=True)
class MatchSingleton(MatchPattern):
"""
A match literal pattern that compares by identity.
"""
#: a literal to compare to
value: Name
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "CSTNode":
return MatchSingleton(value=visit_required(self, "value", self.value, visitor))
def _validate(self) -> None:
if self.value.value not in {"True", "False", "None"}:
raise CSTValidationError(
"A match singleton can only be True, False, or None"
)
def _codegen_impl(self, state: CodegenState) -> None:
with state.record_syntactic_position(self):
self.value._codegen(state)
@property
def lpar(self) -> Sequence[LeftParen]:
return self.value.lpar
@lpar.setter
def lpar(self, value: Sequence[LeftParen]) -> None:
# pyre-fixme[41]: Cannot reassign final attribute `lpar`.
self.value.lpar = value
@property
def rpar(self) -> Sequence[RightParen]:
return self.value.rpar
@rpar.setter
def rpar(self, value: Sequence[RightParen]) -> None:
# pyre-fixme[41]: Cannot reassign final attribute `rpar`.
self.value.rpar = value
@add_slots
@dataclass(frozen=True)
class MatchSequenceElement(CSTNode):
"""
An element in a sequence match pattern.
"""
value: MatchPattern
#: An optional trailing comma.
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
def _visit_and_replace_children(
self, visitor: CSTVisitorT
) -> "MatchSequenceElement":
return MatchSequenceElement(
value=visit_required(self, "value", self.value, visitor),
comma=visit_sentinel(self, "comma", self.comma, visitor),
)
def _codegen_impl(
self,
state: CodegenState,
default_comma: bool = False,
default_comma_whitespace: bool = True,
) -> None:
with state.record_syntactic_position(self):
self.value._codegen(state)
comma = self.comma
if comma is MaybeSentinel.DEFAULT and default_comma:
state.add_token(", " if default_comma_whitespace else ",")
elif isinstance(comma, Comma):
comma._codegen(state)
@add_slots
@dataclass(frozen=True)
class MatchStar(CSTNode):
"""
A starred element in a sequence match pattern. Matches the rest of the sequence.
"""
#: The name of the pattern binding. A ``None`` value represents ``*_``.
name: Optional[Name] = None
#: An optional trailing comma.
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
#: Optional whitespace between the star and the name.
whitespace_before_name: BaseParenthesizableWhitespace = SimpleWhitespace.field("")
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "MatchStar":
return MatchStar(
whitespace_before_name=visit_required(
self, "whitespace_before_name", self.whitespace_before_name, visitor
),
name=visit_optional(self, "name", self.name, visitor),
comma=visit_sentinel(self, "comma", self.comma, visitor),
)
def _codegen_impl(
self,
state: CodegenState,
default_comma: bool = False,
default_comma_whitespace: bool = True,
) -> None:
with state.record_syntactic_position(self):
state.add_token("*")
self.whitespace_before_name._codegen(state)
name = self.name
if name is None:
state.add_token("_")
else:
name._codegen(state)
comma = self.comma
if comma is MaybeSentinel.DEFAULT and default_comma:
state.add_token(", " if default_comma_whitespace else ",")
elif isinstance(comma, Comma):
comma._codegen(state)
class MatchSequence(MatchPattern, ABC):
"""
A match sequence pattern. It's either a :class:`MatchList` or a :class:`MatchTuple`.
Matches a variable length sequence if one of the patterns is a :class:`MatchStar`,
otherwise matches a fixed length sequence.
"""
__slots__ = ()
#: Patterns to be matched against the subject elements if it is a sequence.
patterns: Sequence[Union[MatchSequenceElement, MatchStar]]
@add_slots
@dataclass(frozen=True)
class MatchList(MatchSequence):
"""
A list match pattern. It's either an "open sequence pattern" (without brackets) or a
regular list literal (with brackets).
"""
#: Patterns to be matched against the subject elements if it is a sequence.
patterns: Sequence[Union[MatchSequenceElement, MatchStar]]
#: An optional left bracket. If missing, this is an open sequence pattern.
lbracket: Optional[LeftSquareBracket] = LeftSquareBracket.field()
#: An optional left bracket. If missing, this is an open sequence pattern.
rbracket: Optional[RightSquareBracket] = RightSquareBracket.field()
#: Parenthesis at the beginning of the node
lpar: Sequence[LeftParen] = ()
#: Parentheses after the pattern, but before a comma (if there is one).
rpar: Sequence[RightParen] = ()
def _validate(self) -> None:
if self.lbracket and not self.rbracket:
raise CSTValidationError("Cannot have left bracket without right bracket")
if self.rbracket and not self.lbracket:
raise CSTValidationError("Cannot have right bracket without left bracket")
if not self.patterns and not self.lbracket:
raise CSTValidationError(
"Must have brackets if matching against empty list"
)
super(MatchList, self)._validate()
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "MatchList":
return MatchList(
lpar=visit_sequence(self, "lpar", self.lpar, visitor),
lbracket=visit_optional(self, "lbracket", self.lbracket, visitor),
patterns=visit_sequence(self, "patterns", self.patterns, visitor),
rbracket=visit_optional(self, "rbracket", self.rbracket, visitor),
rpar=visit_sequence(self, "rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
lbracket = self.lbracket
if lbracket is not None:
lbracket._codegen(state)
pats = self.patterns
for idx, pat in enumerate(pats):
pat._codegen(state, default_comma=(idx < len(pats) - 1))
rbracket = self.rbracket
if rbracket is not None:
rbracket._codegen(state)
@add_slots
@dataclass(frozen=True)
class MatchTuple(MatchSequence):
"""
A tuple match pattern.
"""
#: Patterns to be matched against the subject elements if it is a sequence.
patterns: Sequence[Union[MatchSequenceElement, MatchStar]]
#: Parenthesis at the beginning of the node
lpar: Sequence[LeftParen] = field(default_factory=lambda: (LeftParen(),))
#: Parentheses after the pattern, but before a comma (if there is one).
rpar: Sequence[RightParen] = field(default_factory=lambda: (RightParen(),))
def _validate(self) -> None:
if len(self.lpar) < 1:
raise CSTValidationError(
"Tuple patterns must have at least pair of parenthesis"
)
super(MatchTuple, self)._validate()
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "MatchTuple":
return MatchTuple(
lpar=visit_sequence(self, "lpar", self.lpar, visitor),
patterns=visit_sequence(self, "patterns", self.patterns, visitor),
rpar=visit_sequence(self, "rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
pats = self.patterns
patlen = len(pats)
for idx, pat in enumerate(pats):
pat._codegen(
state,
default_comma=patlen == 1 or (idx < patlen - 1),
default_comma_whitespace=patlen != 1,
)
@add_slots
@dataclass(frozen=True)
class MatchMappingElement(CSTNode):
"""
A ``key: value`` pair in a match mapping pattern.
"""
key: BaseExpression
#: The pattern to be matched corresponding to ``key``.
pattern: MatchPattern
#: An optional trailing comma.
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
#: Whitespace between ``key`` and the colon.
whitespace_before_colon: BaseParenthesizableWhitespace = SimpleWhitespace.field("")
#: Whitespace between the colon and ``pattern``.
whitespace_after_colon: BaseParenthesizableWhitespace = SimpleWhitespace.field(" ")
def _visit_and_replace_children(
self, visitor: CSTVisitorT
) -> "MatchMappingElement":
return MatchMappingElement(
key=visit_required(self, "key", self.key, visitor),
whitespace_before_colon=visit_required(
self, "whitespace_before_colon", self.whitespace_before_colon, visitor
),
whitespace_after_colon=visit_required(
self, "whitespace_after_colon", self.whitespace_after_colon, visitor
),
pattern=visit_required(self, "pattern", self.pattern, visitor),
comma=visit_sentinel(self, "comma", self.comma, visitor),
)
def _codegen_impl(self, state: CodegenState, default_comma: bool = False) -> None:
with state.record_syntactic_position(self):
self.key._codegen(state)
self.whitespace_before_colon._codegen(state)
state.add_token(":")
self.whitespace_after_colon._codegen(state)
self.pattern._codegen(state)
comma = self.comma
if comma is MaybeSentinel.DEFAULT and default_comma:
state.add_token(", ")
elif isinstance(comma, Comma):
comma._codegen(state)
@add_slots
@dataclass(frozen=True)
class MatchMapping(MatchPattern):
"""
A match mapping pattern.
"""
#: A sequence of mapping elements.
elements: Sequence[MatchMappingElement] = ()
#: Left curly brace at the beginning of the pattern.
lbrace: LeftCurlyBrace = LeftCurlyBrace.field()
#: Right curly brace at the end of the pattern.
rbrace: RightCurlyBrace = RightCurlyBrace.field()
#: An optional name to capture the remaining elements of the mapping.
rest: Optional[Name] = None
#: Optional whitespace between stars and ``rest``.
whitespace_before_rest: SimpleWhitespace = SimpleWhitespace.field("")
#: An optional trailing comma attached to ``rest``.
trailing_comma: Optional[Comma] = None
#: Parenthesis at the beginning of the node
lpar: Sequence[LeftParen] = ()
#: Parentheses after the pattern
rpar: Sequence[RightParen] = ()
def _validate(self) -> None:
if isinstance(self.trailing_comma, Comma) and self.rest is not None:
raise CSTValidationError("Cannot have a trailing comma without **rest")
super(MatchMapping, self)._validate()
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "MatchMapping":
return MatchMapping(
lpar=visit_sequence(self, "lpar", self.lpar, visitor),
lbrace=visit_required(self, "lbrace", self.lbrace, visitor),
elements=visit_sequence(self, "elements", self.elements, visitor),
whitespace_before_rest=visit_required(
self, "whitespace_before_rest", self.whitespace_before_rest, visitor
),
rest=visit_optional(self, "rest", self.rest, visitor),
trailing_comma=visit_optional(
self, "trailing_comma", self.trailing_comma, visitor
),
rbrace=visit_required(self, "rbrace", self.rbrace, visitor),
rpar=visit_sequence(self, "rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
self.lbrace._codegen(state)
elems = self.elements
rest = self.rest
for idx, el in enumerate(elems):
el._codegen(
state, default_comma=rest is not None or idx < len(elems) - 1
)
if rest is not None:
state.add_token("**")
self.whitespace_before_rest._codegen(state)
rest._codegen(state)
comma = self.trailing_comma
if comma is not None:
comma._codegen(state)
self.rbrace._codegen(state)
@add_slots
@dataclass(frozen=True)
class MatchKeywordElement(CSTNode):
"""
A key=value pair in a :class:`MatchClass`.
"""
key: Name
#: The pattern to be matched against the attribute named ``key``.
pattern: MatchPattern
#: An optional trailing comma.
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
#: Whitespace between ``key`` and the equals sign.
whitespace_before_equal: BaseParenthesizableWhitespace = SimpleWhitespace.field("")
#: Whitespace between the equals sign and ``pattern``.
whitespace_after_equal: BaseParenthesizableWhitespace = SimpleWhitespace.field("")
def _visit_and_replace_children(
self, visitor: CSTVisitorT
) -> "MatchKeywordElement":
return MatchKeywordElement(
key=visit_required(self, "key", self.key, visitor),
whitespace_before_equal=visit_required(
self, "whitespace_before_equal", self.whitespace_before_equal, visitor
),
whitespace_after_equal=visit_required(
self, "whitespace_after_equal", self.whitespace_after_equal, visitor
),
pattern=visit_required(self, "pattern", self.pattern, visitor),
comma=visit_sentinel(self, "comma", self.comma, visitor),
)
def _codegen_impl(self, state: CodegenState, default_comma: bool = False) -> None:
with state.record_syntactic_position(self):
self.key._codegen(state)
self.whitespace_before_equal._codegen(state)
state.add_token("=")
self.whitespace_after_equal._codegen(state)
self.pattern._codegen(state)
comma = self.comma
if comma is MaybeSentinel.DEFAULT and default_comma:
state.add_token(", ")
elif isinstance(comma, Comma):
comma._codegen(state)
@add_slots
@dataclass(frozen=True)
class MatchClass(MatchPattern):
"""
A match class pattern.
"""
#: An expression giving the nominal class to be matched.
cls: BaseExpression
#: A sequence of patterns to be matched against the class defined sequence of
#: pattern matching attributes.
patterns: Sequence[MatchSequenceElement] = ()
#: A sequence of additional attribute names and corresponding patterns to be
#: matched.
kwds: Sequence[MatchKeywordElement] = ()
#: Whitespace between the class name and the left parenthesis.
whitespace_after_cls: BaseParenthesizableWhitespace = SimpleWhitespace.field("")
#: Whitespace between the left parenthesis and the first pattern.
whitespace_before_patterns: BaseParenthesizableWhitespace = SimpleWhitespace.field(
""
)
#: Whitespace between the last pattern and the right parenthesis.
whitespace_after_kwds: BaseParenthesizableWhitespace = SimpleWhitespace.field("")
#: Parenthesis at the beginning of the node
lpar: Sequence[LeftParen] = ()
#: Parentheses after the pattern
rpar: Sequence[RightParen] = ()
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "MatchClass":
return MatchClass(
lpar=visit_sequence(self, "lpar", self.lpar, visitor),
cls=visit_required(self, "cls", self.cls, visitor),
whitespace_after_cls=visit_required(
self, "whitespace_after_cls", self.whitespace_after_cls, visitor
),
whitespace_before_patterns=visit_required(
self,
"whitespace_before_patterns",
self.whitespace_before_patterns,
visitor,
),
patterns=visit_sequence(self, "patterns", self.patterns, visitor),
kwds=visit_sequence(self, "kwds", self.kwds, visitor),
whitespace_after_kwds=visit_required(
self, "whitespace_after_kwds", self.whitespace_after_kwds, visitor
),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
self.cls._codegen(state)
self.whitespace_after_cls._codegen(state)
state.add_token("(")
self.whitespace_before_patterns._codegen(state)
pats = self.patterns
kwds = self.kwds
for idx, pat in enumerate(pats):
pat._codegen(state, default_comma=idx + 1 < len(pats) + len(kwds))
for idx, kwd in enumerate(kwds):
kwd._codegen(state, default_comma=idx + 1 < len(kwds))
self.whitespace_after_kwds._codegen(state)
state.add_token(")")
@add_slots
@dataclass(frozen=True)
class MatchAs(MatchPattern):
"""
A match "as-pattern", capture pattern, or wildcard pattern.
"""
#: The match pattern that the subject will be matched against. If this is ``None``,
#: the node represents a capture pattern (i.e. a bare name) and will always succeed.
pattern: Optional[MatchPattern] = None
#: The name that will be bound if the pattern is successful. If this is ``None``,
#: ``pattern`` must also be ``None`` and the node represents the wildcard pattern
#: (i.e. ``_``).
name: Optional[Name] = None
#: Whitespace between ``pattern`` and the ``as`` keyword (if ``pattern`` is not
#: ``None``)
whitespace_before_as: Union[
BaseParenthesizableWhitespace, MaybeSentinel
] = MaybeSentinel.DEFAULT
#: Whitespace between the ``as`` keyword and ``name`` (if ``pattern`` is not
#: ``None``)
whitespace_after_as: Union[
BaseParenthesizableWhitespace, MaybeSentinel
] = MaybeSentinel.DEFAULT
#: Parenthesis at the beginning of the node
lpar: Sequence[LeftParen] = ()
#: Parentheses after the pattern
rpar: Sequence[RightParen] = ()
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "MatchAs":
return MatchAs(
lpar=visit_sequence(self, "lpar", self.lpar, visitor),
pattern=visit_optional(self, "pattern", self.pattern, visitor),
whitespace_before_as=visit_sentinel(
self, "whitespace_before_as", self.whitespace_before_as, visitor
),
whitespace_after_as=visit_sentinel(
self, "whitespace_after_as", self.whitespace_after_as, visitor
),
name=visit_optional(self, "name", self.name, visitor),
rpar=visit_sequence(self, "rpar", self.rpar, visitor),
)
def _validate(self) -> None:
if self.name is None and self.pattern is not None:
raise CSTValidationError("Pattern must be None if name is None")
super(MatchAs, self)._validate()
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
pat = self.pattern
name = self.name
if pat is not None:
pat._codegen(state)
ws_before = self.whitespace_before_as
if ws_before is MaybeSentinel.DEFAULT:
state.add_token(" ")
elif isinstance(ws_before, BaseParenthesizableWhitespace):
ws_before._codegen(state)
state.add_token("as")
ws_after = self.whitespace_after_as
if ws_after is MaybeSentinel.DEFAULT:
state.add_token(" ")
elif isinstance(ws_after, BaseParenthesizableWhitespace):
ws_after._codegen(state)
if name is None:
state.add_token("_")
else:
name._codegen(state)
@add_slots
@dataclass(frozen=True)
class MatchOrElement(CSTNode):
"""
An element in a :class:`MatchOr` node.
"""
pattern: MatchPattern
#: An optional ``|`` separator.
separator: Union[BitOr, MaybeSentinel] = MaybeSentinel.DEFAULT
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "MatchOrElement":
return MatchOrElement(
pattern=visit_required(self, "pattern", self.pattern, visitor),
separator=visit_sentinel(self, "separator", self.separator, visitor),
)
def _codegen_impl(
self, state: CodegenState, default_separator: bool = False
) -> None:
with state.record_syntactic_position(self):
self.pattern._codegen(state)
sep = self.separator
if sep is MaybeSentinel.DEFAULT and default_separator:
state.add_token(" | ")
elif isinstance(sep, BitOr):
sep._codegen(state)
@add_slots
@dataclass(frozen=True)
class MatchOr(MatchPattern):
"""
A match "or-pattern". It matches each of its subpatterns in turn to the subject,
until one succeeds. The or-pattern is then deemed to succeed. If none of the
subpatterns succeed the or-pattern fails.
"""
#: The subpatterns to be tried in turn.
patterns: Sequence[MatchOrElement]
#: Parenthesis at the beginning of the node
lpar: Sequence[LeftParen] = ()
#: Parentheses after the pattern
rpar: Sequence[RightParen] = ()
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "MatchOr":
return MatchOr(
lpar=visit_sequence(self, "lpar", self.lpar, visitor),
patterns=visit_sequence(self, "patterns", self.patterns, visitor),
rpar=visit_sequence(self, "rpar", self.rpar, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
with self._parenthesize(state):
pats = self.patterns
for idx, pat in enumerate(pats):
pat._codegen(state, default_separator=idx + 1 < len(pats))