Add posonly parameter support to Parameters node.

This is part one of a two-part change to support the posonly param indicator "/".
This commit is contained in:
Jennifer Taylor 2020-01-13 11:00:04 -08:00 committed by Jennifer Taylor
parent 0d01792e6d
commit 6eb6ec7b1d
10 changed files with 411 additions and 9 deletions

View file

@ -242,6 +242,7 @@ they belong with.
sense to group these closer to FunctionDef than with Lambda.
.. autoclass:: libcst.Parameters
.. autoclass:: libcst.Param
.. autoclass:: libcst.ParamSlash
.. autoclass:: libcst.ParamStar
.. autoclass:: libcst.WithItem

View file

@ -61,6 +61,7 @@ from libcst._nodes.expression import (
NamedExpr,
Param,
Parameters,
ParamSlash,
ParamStar,
RightCurlyBrace,
RightParen,
@ -268,6 +269,7 @@ __all__ = [
"NamedExpr",
"Param",
"Parameters",
"ParamSlash",
"ParamStar",
"RightCurlyBrace",
"RightParen",

View file

@ -1531,7 +1531,7 @@ class Annotation(CSTNode):
@dataclass(frozen=True)
class ParamStar(CSTNode):
"""
A sentinel indicator on a :class:`Parameter` list to denote that the subsequent
A sentinel indicator on a :class:`Parameters` list to denote that the subsequent
params are keyword-only args.
This syntax is described in `PEP 3102`_.
@ -1550,11 +1550,39 @@ class ParamStar(CSTNode):
self.comma._codegen(state)
@add_slots
@dataclass(frozen=True)
class ParamSlash(CSTNode):
"""
A sentinel indicator on a :class:`Parameters` list to denote that the previous
params are positional-only args.
This syntax is described in `PEP 570`_.
.. _PEP 570: https://www.python.org/dev/peps/pep-0570/#specification
"""
# Optional comma that comes after the slash.
comma: Union[Comma, MaybeSentinel] = MaybeSentinel.DEFAULT
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "ParamSlash":
return ParamSlash(comma=visit_sentinel(self, "comma", self.comma, visitor))
def _codegen_impl(self, state: CodegenState, default_comma: bool = False) -> None:
state.add_token("/")
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 Param(CSTNode):
"""
A positional or keyword argument in a :class:`Parameter` list. May contain an
A positional or keyword argument in a :class:`Parameters` list. May contain an
:class:`Annotation` and, in some cases, a ``default``.
"""
@ -1670,6 +1698,14 @@ class Parameters(CSTNode):
#: Optional parameter that captures unspecified kwargs.
star_kwarg: Optional[Param] = None
#: Positional-only parameters, with or without defaults. Positional-only
#: parameters with defaults must all be after those without defaults.
posonly_params: Sequence[Param] = ()
#: Optional sentinel that dictates parameters preceeding are positional-only
#: args.
posonly_ind: Union[ParamSlash, MaybeSentinel] = MaybeSentinel.DEFAULT
def _validate_stars_sequence(self, vals: Sequence[Param], *, section: str) -> None:
if len(vals) == 0:
return
@ -1680,7 +1716,13 @@ class Parameters(CSTNode):
f"Expecting a star prefix of '' for {section} Param."
)
def _validate_kwonlystar(self) -> None:
def _validate_posonly_ind(self) -> None:
if isinstance(self.posonly_ind, ParamSlash) and len(self.posonly_params) == 0:
raise CSTValidationError(
"Must have at least one posonly param if ParamSlash is used."
)
def _validate_kwonly_star(self) -> None:
if isinstance(self.star_arg, ParamStar) and len(self.kwonly_params) == 0:
raise CSTValidationError(
"Must have at least one kwonly param if ParamStar is used."
@ -1688,7 +1730,7 @@ class Parameters(CSTNode):
def _validate_defaults(self) -> None:
seen_default = False
for param in self.params:
for param in (*self.posonly_params, *self.params):
if param.default:
# Mark that we've moved onto defaults
if not seen_default:
@ -1709,6 +1751,8 @@ class Parameters(CSTNode):
def _validate_stars(self) -> None:
if len(self.params) > 0:
self._validate_stars_sequence(self.params, section="params")
if len(self.posonly_params) > 0:
self._validate_stars_sequence(self.posonly_params, section="posonly_params")
star_arg = self.star_arg
if (
isinstance(star_arg, Param)
@ -1733,8 +1777,10 @@ class Parameters(CSTNode):
)
def _validate(self) -> None:
# Validate posonly_params slash placement semantics.
self._validate_posonly_ind()
# Validate kwonly_param star placement semantics.
self._validate_kwonlystar()
self._validate_kwonly_star()
# Validate defaults semantics for params and star_arg/star_kwarg.
self._validate_defaults()
# Validate that we don't have random stars on non star_kwarg.
@ -1742,6 +1788,10 @@ class Parameters(CSTNode):
def _visit_and_replace_children(self, visitor: CSTVisitorT) -> "Parameters":
return Parameters(
posonly_params=visit_sequence(
self, "posonly_params", self.posonly_params, visitor
),
posonly_ind=visit_sentinel(self, "posonly_ind", self.posonly_ind, visitor),
params=visit_sequence(self, "params", self.params, visitor),
star_arg=visit_sentinel(self, "star_arg", self.star_arg, visitor),
kwonly_params=visit_sequence(
@ -1750,7 +1800,7 @@ class Parameters(CSTNode):
star_kwarg=visit_optional(self, "star_kwarg", self.star_kwarg, visitor),
)
def _codegen_impl(self, state: CodegenState) -> None:
def _codegen_impl(self, state: CodegenState) -> None: # noqa: C901
# Compute the star existence first so we can ask about whether
# each element is the last in the list or not.
star_arg = self.star_arg
@ -1760,7 +1810,29 @@ class Parameters(CSTNode):
starincluded = True
else:
starincluded = False
# Render out the params first, computing necessary trailing commas.
# Render out the positional-only params first. They will always have trailing
# commas because in order to have positional-only params, there must be a
# slash afterwards.
for i, param in enumerate(self.posonly_params):
param._codegen(state, default_star="", default_comma=True)
# Render out the positional-only indicator if necessary.
more_values = (
starincluded
or len(self.params) > 0
or len(self.kwonly_params) > 0
or self.star_kwarg is not None
)
posonly_ind = self.posonly_ind
if isinstance(posonly_ind, ParamSlash):
# Its explicitly included, so render the version we have here which
# might have spacing applied to its comma.
posonly_ind._codegen(state, default_comma=more_values)
elif len(self.posonly_params) > 0:
if more_values:
state.add_token("/, ")
else:
state.add_token("/")
# Render out the params next, computing necessary trailing commas.
lastparam = len(self.params) - 1
more_values = (
starincluded or len(self.kwonly_params) > 0 or self.star_kwarg is not None
@ -1838,6 +1910,7 @@ class Lambda(BaseExpression):
super(Lambda, self)._validate()
# Sum up all parameters
all_params = [
*self.params.posonly_params,
*self.params.params,
*self.params.kwonly_params,
]
@ -1882,7 +1955,8 @@ class Lambda(BaseExpression):
whitespace_after_lambda = self.whitespace_after_lambda
if isinstance(whitespace_after_lambda, MaybeSentinel):
if not (
len(self.params.params) == 0
len(self.params.posonly_params) == 0
and len(self.params.params) == 0
and not isinstance(self.params.star_arg, Param)
and len(self.params.kwonly_params) == 0
and self.params.star_kwarg is None

View file

@ -121,6 +121,72 @@ class FunctionDefCreationTest(CSTNodeTest):
),
"code": 'def foo(bar: str = "one", baz: int = 5): pass\n',
},
# Test basic positional only params
{
"node": cst.FunctionDef(
cst.Name("foo"),
cst.Parameters(
posonly_params=(
cst.Param(cst.Name("bar")),
cst.Param(cst.Name("baz")),
)
),
cst.SimpleStatementSuite((cst.Pass(),)),
),
"code": "def foo(bar, baz, /): pass\n",
},
# Typed positional only params
{
"node": cst.FunctionDef(
cst.Name("foo"),
cst.Parameters(
posonly_params=(
cst.Param(cst.Name("bar"), cst.Annotation(cst.Name("str"))),
cst.Param(cst.Name("baz"), cst.Annotation(cst.Name("int"))),
)
),
cst.SimpleStatementSuite((cst.Pass(),)),
),
"code": "def foo(bar: str, baz: int, /): pass\n",
},
# Test basic positional only default params
{
"node": cst.FunctionDef(
cst.Name("foo"),
cst.Parameters(
posonly_params=(
cst.Param(
cst.Name("bar"), default=cst.SimpleString('"one"')
),
cst.Param(cst.Name("baz"), default=cst.Integer("5")),
)
),
cst.SimpleStatementSuite((cst.Pass(),)),
),
"code": 'def foo(bar = "one", baz = 5, /): pass\n',
},
# Typed positional only default params
{
"node": cst.FunctionDef(
cst.Name("foo"),
cst.Parameters(
posonly_params=(
cst.Param(
cst.Name("bar"),
cst.Annotation(cst.Name("str")),
default=cst.SimpleString('"one"'),
),
cst.Param(
cst.Name("baz"),
cst.Annotation(cst.Name("int")),
default=cst.Integer("5"),
),
)
),
cst.SimpleStatementSuite((cst.Pass(),)),
),
"code": 'def foo(bar: str = "one", baz: int = 5, /): pass\n',
},
# Mixed positional and default params.
{
"node": cst.FunctionDef(
@ -365,6 +431,54 @@ class FunctionDefCreationTest(CSTNodeTest):
),
"code": "def foo(*params: str, **kwparams: int): pass\n",
},
# Test positional only params and positional params
{
"node": cst.FunctionDef(
cst.Name("foo"),
cst.Parameters(
posonly_params=(cst.Param(cst.Name("bar")),),
params=(cst.Param(cst.Name("baz")),),
),
cst.SimpleStatementSuite((cst.Pass(),)),
),
"code": "def foo(bar, /, baz): pass\n",
},
# Test positional only params and star args
{
"node": cst.FunctionDef(
cst.Name("foo"),
cst.Parameters(
posonly_params=(cst.Param(cst.Name("bar")),),
star_arg=cst.Param(cst.Name("baz")),
),
cst.SimpleStatementSuite((cst.Pass(),)),
),
"code": "def foo(bar, /, *baz): pass\n",
},
# Test positional only params and kwonly params
{
"node": cst.FunctionDef(
cst.Name("foo"),
cst.Parameters(
posonly_params=(cst.Param(cst.Name("bar")),),
kwonly_params=(cst.Param(cst.Name("baz")),),
),
cst.SimpleStatementSuite((cst.Pass(),)),
),
"code": "def foo(bar, /, *, baz): pass\n",
},
# Test positional only params and star kwargs
{
"node": cst.FunctionDef(
cst.Name("foo"),
cst.Parameters(
posonly_params=(cst.Param(cst.Name("bar")),),
star_kwarg=cst.Param(cst.Name("baz")),
),
cst.SimpleStatementSuite((cst.Pass(),)),
),
"code": "def foo(bar, /, **baz): pass\n",
},
# Test decorators
{
"node": cst.FunctionDef(
@ -620,6 +734,21 @@ class FunctionDefCreationTest(CSTNodeTest):
),
"Cannot have param without defaults following a param with defaults.",
),
(
lambda: cst.FunctionDef(
cst.Name("foo"),
cst.Parameters(
posonly_params=(
cst.Param(
cst.Name("bar"), default=cst.SimpleString('"one"')
),
),
params=(cst.Param(cst.Name("bar")),),
),
cst.SimpleStatementSuite((cst.Pass(),)),
),
"Cannot have param without defaults following a param with defaults.",
),
(
lambda: cst.FunctionDef(
cst.Name("foo"),
@ -628,6 +757,14 @@ class FunctionDefCreationTest(CSTNodeTest):
),
"Must have at least one kwonly param if ParamStar is used.",
),
(
lambda: cst.FunctionDef(
cst.Name("foo"),
cst.Parameters(posonly_ind=cst.ParamSlash()),
cst.SimpleStatementSuite((cst.Pass(),)),
),
"Must have at least one posonly param if ParamSlash is used.",
),
(
lambda: cst.FunctionDef(
cst.Name("foo"),

View file

@ -18,6 +18,19 @@ class LambdaCreationTest(CSTNodeTest):
(
# Simple lambda
(cst.Lambda(cst.Parameters(), cst.Integer("5")), "lambda: 5"),
# Test basic positional only params
{
"node": cst.Lambda(
cst.Parameters(
posonly_params=(
cst.Param(cst.Name("bar")),
cst.Param(cst.Name("baz")),
)
),
cst.Integer("5"),
),
"code": "lambda bar, baz, /: 5",
},
# Test basic positional params
(
cst.Lambda(
@ -249,6 +262,14 @@ class LambdaCreationTest(CSTNodeTest):
),
"right paren without left paren",
),
(
lambda: cst.Lambda(
cst.Parameters(posonly_params=(cst.Param(cst.Name("arg")),)),
cst.Integer("5"),
whitespace_after_lambda=cst.SimpleWhitespace(""),
),
"at least one space after lambda",
),
(
lambda: cst.Lambda(
cst.Parameters(params=(cst.Param(cst.Name("arg")),)),
@ -372,6 +393,21 @@ class LambdaCreationTest(CSTNodeTest):
),
r"Expecting a star prefix of '\*\*'",
),
(
lambda: cst.Lambda(
cst.Parameters(
posonly_params=(
cst.Param(
cst.Name("arg"),
annotation=cst.Annotation(cst.Name("str")),
),
)
),
cst.Integer("5"),
whitespace_after_lambda=cst.SimpleWhitespace(""),
),
"Lambda params cannot have type annotations",
),
(
lambda: cst.Lambda(
cst.Parameters(

View file

@ -150,6 +150,7 @@ if TYPE_CHECKING:
Name,
NamedExpr,
Param,
ParamSlash,
ParamStar,
Parameters,
RightCurlyBrace,
@ -3544,6 +3545,18 @@ class CSTTypedBaseFunctions:
def leave_Param_whitespace_after_param(self, node: "Param") -> None:
pass
@mark_no_op
def visit_ParamSlash(self, node: "ParamSlash") -> Optional[bool]:
pass
@mark_no_op
def visit_ParamSlash_comma(self, node: "ParamSlash") -> None:
pass
@mark_no_op
def leave_ParamSlash_comma(self, node: "ParamSlash") -> None:
pass
@mark_no_op
def visit_ParamStar(self, node: "ParamStar") -> Optional[bool]:
pass
@ -3592,6 +3605,22 @@ class CSTTypedBaseFunctions:
def leave_Parameters_star_kwarg(self, node: "Parameters") -> None:
pass
@mark_no_op
def visit_Parameters_posonly_params(self, node: "Parameters") -> None:
pass
@mark_no_op
def leave_Parameters_posonly_params(self, node: "Parameters") -> None:
pass
@mark_no_op
def visit_Parameters_posonly_ind(self, node: "Parameters") -> None:
pass
@mark_no_op
def leave_Parameters_posonly_ind(self, node: "Parameters") -> None:
pass
@mark_no_op
def visit_ParenthesizedWhitespace(
self, node: "ParenthesizedWhitespace"
@ -5072,6 +5101,10 @@ class CSTTypedVisitorFunctions(CSTTypedBaseFunctions):
def leave_Param(self, original_node: "Param") -> None:
pass
@mark_no_op
def leave_ParamSlash(self, original_node: "ParamSlash") -> None:
pass
@mark_no_op
def leave_ParamStar(self, original_node: "ParamStar") -> None:
pass
@ -5838,6 +5871,12 @@ class CSTTypedTransformerFunctions(CSTTypedBaseFunctions):
) -> Union["Param", MaybeSentinel, RemovalSentinel]:
return updated_node
@mark_no_op
def leave_ParamSlash(
self, original_node: "ParamSlash", updated_node: "ParamSlash"
) -> Union["ParamSlash", MaybeSentinel]:
return updated_node
@mark_no_op
def leave_ParamStar(
self, original_node: "ParamStar", updated_node: "ParamStar"

View file

@ -9639,6 +9639,19 @@ class Param(BaseMatcherNode):
] = DoNotCare()
@dataclass(frozen=True, eq=False, unsafe_hash=False)
class ParamSlash(BaseMatcherNode):
comma: Union[
CommaMatchType, DoNotCareSentinel, OneOf[CommaMatchType], AllOf[CommaMatchType]
] = DoNotCare()
metadata: Union[
MetadataMatchType,
DoNotCareSentinel,
OneOf[MetadataMatchType],
AllOf[MetadataMatchType],
] = DoNotCare()
@dataclass(frozen=True, eq=False, unsafe_hash=False)
class ParamStar(BaseMatcherNode):
comma: Union[
@ -9667,6 +9680,9 @@ ParamOrNoneMatchType = Union[
MetadataMatchType,
MatchIfTrue[Callable[[Union[cst.Param, None]], bool]],
]
ParamSlashMatchType = Union[
"ParamSlash", MetadataMatchType, MatchIfTrue[Callable[[cst.ParamSlash], bool]]
]
@dataclass(frozen=True, eq=False, unsafe_hash=False)
@ -9843,6 +9859,92 @@ class Parameters(BaseMatcherNode):
OneOf[ParamOrNoneMatchType],
AllOf[ParamOrNoneMatchType],
] = DoNotCare()
posonly_params: Union[
Sequence[
Union[
ParamMatchType,
DoNotCareSentinel,
OneOf[ParamMatchType],
AllOf[ParamMatchType],
AtLeastN[
Union[
ParamMatchType,
DoNotCareSentinel,
OneOf[ParamMatchType],
AllOf[ParamMatchType],
]
],
AtMostN[
Union[
ParamMatchType,
DoNotCareSentinel,
OneOf[ParamMatchType],
AllOf[ParamMatchType],
]
],
]
],
DoNotCareSentinel,
MatchIfTrue[Callable[[Sequence[cst.Param]], bool]],
OneOf[
Union[
Sequence[
Union[
ParamMatchType,
OneOf[ParamMatchType],
AllOf[ParamMatchType],
AtLeastN[
Union[
ParamMatchType,
OneOf[ParamMatchType],
AllOf[ParamMatchType],
]
],
AtMostN[
Union[
ParamMatchType,
OneOf[ParamMatchType],
AllOf[ParamMatchType],
]
],
]
],
MatchIfTrue[Callable[[Sequence[cst.Param]], bool]],
]
],
AllOf[
Union[
Sequence[
Union[
ParamMatchType,
OneOf[ParamMatchType],
AllOf[ParamMatchType],
AtLeastN[
Union[
ParamMatchType,
OneOf[ParamMatchType],
AllOf[ParamMatchType],
]
],
AtMostN[
Union[
ParamMatchType,
OneOf[ParamMatchType],
AllOf[ParamMatchType],
]
],
]
],
MatchIfTrue[Callable[[Sequence[cst.Param]], bool]],
]
],
] = DoNotCare()
posonly_ind: Union[
ParamSlashMatchType,
DoNotCareSentinel,
OneOf[ParamSlashMatchType],
AllOf[ParamSlashMatchType],
] = DoNotCare()
metadata: Union[
MetadataMatchType,
DoNotCareSentinel,
@ -13094,6 +13196,7 @@ __all__ = [
"OneOf",
"Or",
"Param",
"ParamSlash",
"ParamStar",
"Parameters",
"ParenthesizedWhitespace",

View file

@ -53,6 +53,7 @@ from libcst._nodes.expression import (
NamedExpr,
Param,
Parameters,
ParamSlash,
ParamStar,
RightCurlyBrace,
RightParen,
@ -283,6 +284,7 @@ TYPED_FUNCTION_RETURN_MAPPING: TypingDict[Type[CSTNode], object] = {
NotIn: BaseCompOp,
Or: BaseBooleanOp,
Param: Union[Param, MaybeSentinel, RemovalSentinel],
ParamSlash: Union[ParamSlash, MaybeSentinel],
ParamStar: Union[ParamStar, MaybeSentinel],
Parameters: Parameters,
ParenthesizedWhitespace: Union[BaseParenthesizableWhitespace, MaybeSentinel],

View file

@ -58,6 +58,8 @@ class PrettyPrintNodesTest(UnitTest):
star_arg=MaybeSentinel.DEFAULT,
kwonly_params=[],
star_kwarg=None,
posonly_params=[],
posonly_ind=MaybeSentinel.DEFAULT,
),
body=IndentedBlock(
body=[
@ -208,6 +210,8 @@ class PrettyPrintNodesTest(UnitTest):
star_arg=MaybeSentinel.DEFAULT,
kwonly_params=[],
star_kwarg=None,
posonly_params=[],
posonly_ind=MaybeSentinel.DEFAULT,
),
body=IndentedBlock(
body=[
@ -446,6 +450,8 @@ class PrettyPrintNodesTest(UnitTest):
star_arg=MaybeSentinel.DEFAULT,
kwonly_params=[],
star_kwarg=None,
posonly_params=[],
posonly_ind=MaybeSentinel.DEFAULT,
),
body=IndentedBlock(
body=[
@ -578,6 +584,8 @@ class PrettyPrintNodesTest(UnitTest):
star_arg=MaybeSentinel.DEFAULT,
kwonly_params=[],
star_kwarg=None,
posonly_params=[],
posonly_ind=MaybeSentinel.DEFAULT,
),
body=IndentedBlock(
body=[

View file

@ -101,7 +101,7 @@ def _node_repr_recursive( # noqa: C901
type_str = repr(field.type)
if (
"Sentinel" in type_str
and field.name not in ["star_arg", "star"]
and field.name not in ["star_arg", "star", "posonly_ind"]
and "whitespace" not in field.name
):
# This is a value that can optionally be specified, so its