Support PEP-570 (positional only arguments) (#946)

Code using positional only arguments is considered >= 3.8
This commit is contained in:
Zsolt Dollenstein 2019-07-28 16:17:33 +01:00 committed by GitHub
parent d8fa8df052
commit 2848e2e1d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 120 additions and 11 deletions

View File

@ -143,6 +143,7 @@ class Feature(Enum):
ASYNC_IDENTIFIERS = 6 ASYNC_IDENTIFIERS = 6
ASYNC_KEYWORDS = 7 ASYNC_KEYWORDS = 7
ASSIGNMENT_EXPRESSIONS = 8 ASSIGNMENT_EXPRESSIONS = 8
POS_ONLY_ARGUMENTS = 9
VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = {
@ -178,6 +179,7 @@ class Feature(Enum):
Feature.TRAILING_COMMA_IN_DEF, Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_KEYWORDS, Feature.ASYNC_KEYWORDS,
Feature.ASSIGNMENT_EXPRESSIONS, Feature.ASSIGNMENT_EXPRESSIONS,
Feature.POS_ONLY_ARGUMENTS,
}, },
} }
@ -935,6 +937,7 @@ def show(cls, code: Union[str, Leaf, Node]) -> None:
token.DOUBLESTAR, token.DOUBLESTAR,
} }
STARS = {token.STAR, token.DOUBLESTAR} STARS = {token.STAR, token.DOUBLESTAR}
VARARGS_SPECIALS = STARS | {token.SLASH}
VARARGS_PARENTS = { VARARGS_PARENTS = {
syms.arglist, syms.arglist,
syms.argument, # double star in arglist syms.argument, # double star in arglist
@ -1847,7 +1850,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901
# that, too. # that, too.
return prevp.prefix return prevp.prefix
elif prevp.type in STARS: elif prevp.type in VARARGS_SPECIALS:
if is_vararg(prevp, within=VARARGS_PARENTS | UNPACKING_PARENTS): if is_vararg(prevp, within=VARARGS_PARENTS | UNPACKING_PARENTS):
return NO return NO
@ -1937,7 +1940,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901
if not prevp or prevp.type == token.LPAR: if not prevp or prevp.type == token.LPAR:
return NO return NO
elif prev.type in {token.EQUAL} | STARS: elif prev.type in {token.EQUAL} | VARARGS_SPECIALS:
return NO return NO
elif p.type == syms.decorator: elif p.type == syms.decorator:
@ -3086,7 +3089,7 @@ def is_vararg(leaf: Leaf, within: Set[NodeType]) -> bool:
extended iterable unpacking (PEP 3132) and additional unpacking extended iterable unpacking (PEP 3132) and additional unpacking
generalizations (PEP 448). generalizations (PEP 448).
""" """
if leaf.type not in STARS or not leaf.parent: if leaf.type not in VARARGS_SPECIALS or not leaf.parent:
return False return False
p = leaf.parent p = leaf.parent
@ -3201,8 +3204,9 @@ def get_features_used(node: Node) -> Set[Feature]:
Currently looking for: Currently looking for:
- f-strings; - f-strings;
- underscores in numeric literals; and - underscores in numeric literals;
- trailing commas after * or ** in function signatures and calls. - trailing commas after * or ** in function signatures and calls;
- positional only arguments in function signatures and lambdas;
""" """
features: Set[Feature] = set() features: Set[Feature] = set()
for n in node.pre_order(): for n in node.pre_order():
@ -3215,6 +3219,10 @@ def get_features_used(node: Node) -> Set[Feature]:
if "_" in n.value: # type: ignore if "_" in n.value: # type: ignore
features.add(Feature.NUMERIC_UNDERSCORES) features.add(Feature.NUMERIC_UNDERSCORES)
elif n.type == token.SLASH:
if n.parent and n.parent.type in {syms.typedargslist, syms.arglist}:
features.add(Feature.POS_ONLY_ARGUMENTS)
elif n.type == token.COLONEQUAL: elif n.type == token.COLONEQUAL:
features.add(Feature.ASSIGNMENT_EXPRESSIONS) features.add(Feature.ASSIGNMENT_EXPRESSIONS)

View File

@ -18,15 +18,55 @@ decorated: decorators (classdef | funcdef | async_funcdef)
async_funcdef: ASYNC funcdef async_funcdef: ASYNC funcdef
funcdef: 'def' NAME parameters ['->' test] ':' suite funcdef: 'def' NAME parameters ['->' test] ':' suite
parameters: '(' [typedargslist] ')' parameters: '(' [typedargslist] ')'
typedargslist: ((tfpdef ['=' test] ',')*
('*' [tname] (',' tname ['=' test])* [',' ['**' tname [',']]] | '**' tname [',']) # The following definition for typedarglist is equivalent to this set of rules:
| tfpdef ['=' test] (',' tfpdef ['=' test])* [',']) #
# arguments = argument (',' argument)*
# argument = tfpdef ['=' test]
# kwargs = '**' tname [',']
# args = '*' [tname]
# kwonly_kwargs = (',' argument)* [',' [kwargs]]
# args_kwonly_kwargs = args kwonly_kwargs | kwargs
# poskeyword_args_kwonly_kwargs = arguments [',' [args_kwonly_kwargs]]
# typedargslist_no_posonly = poskeyword_args_kwonly_kwargs | args_kwonly_kwargs
# typedarglist = arguments ',' '/' [',' [typedargslist_no_posonly]])|(typedargslist_no_posonly)"
#
# It needs to be fully expanded to allow our LL(1) parser to work on it.
typedargslist: tfpdef ['=' test] (',' tfpdef ['=' test])* ',' '/' [
',' [((tfpdef ['=' test] ',')* ('*' [tname] (',' tname ['=' test])*
[',' ['**' tname [',']]] | '**' tname [','])
| tfpdef ['=' test] (',' tfpdef ['=' test])* [','])]
] | ((tfpdef ['=' test] ',')* ('*' [tname] (',' tname ['=' test])*
[',' ['**' tname [',']]] | '**' tname [','])
| tfpdef ['=' test] (',' tfpdef ['=' test])* [','])
tname: NAME [':' test] tname: NAME [':' test]
tfpdef: tname | '(' tfplist ')' tfpdef: tname | '(' tfplist ')'
tfplist: tfpdef (',' tfpdef)* [','] tfplist: tfpdef (',' tfpdef)* [',']
varargslist: ((vfpdef ['=' test] ',')*
('*' [vname] (',' vname ['=' test])* [',' ['**' vname [',']]] | '**' vname [',']) # The following definition for varargslist is equivalent to this set of rules:
| vfpdef ['=' test] (',' vfpdef ['=' test])* [',']) #
# arguments = argument (',' argument )*
# argument = vfpdef ['=' test]
# kwargs = '**' vname [',']
# args = '*' [vname]
# kwonly_kwargs = (',' argument )* [',' [kwargs]]
# args_kwonly_kwargs = args kwonly_kwargs | kwargs
# poskeyword_args_kwonly_kwargs = arguments [',' [args_kwonly_kwargs]]
# vararglist_no_posonly = poskeyword_args_kwonly_kwargs | args_kwonly_kwargs
# varargslist = arguments ',' '/' [','[(vararglist_no_posonly)]] | (vararglist_no_posonly)
#
# It needs to be fully expanded to allow our LL(1) parser to work on it.
varargslist: vfpdef ['=' test ](',' vfpdef ['=' test])* ',' '/' [',' [
((vfpdef ['=' test] ',')* ('*' [vname] (',' vname ['=' test])*
[',' ['**' vname [',']]] | '**' vname [','])
| vfpdef ['=' test] (',' vfpdef ['=' test])* [','])
]] | ((vfpdef ['=' test] ',')*
('*' [vname] (',' vname ['=' test])* [',' ['**' vname [',']]]| '**' vname [','])
| vfpdef ['=' test] (',' vfpdef ['=' test])* [','])
vname: NAME vname: NAME
vfpdef: vname | '(' vfplist ')' vfpdef: vname | '(' vfplist ')'
vfplist: vfpdef (',' vfpdef)* [','] vfplist: vfpdef (',' vfpdef)* [',']

44
tests/data/pep_570.py Normal file
View File

@ -0,0 +1,44 @@
def positional_only_arg(a, /):
pass
def all_markers(a, b, /, c, d, *, e, f):
pass
def all_markers_with_args_and_kwargs(
a_long_one,
b_long_one,
/,
c_long_one,
d_long_one,
*args,
e_long_one,
f_long_one,
**kwargs,
):
pass
def all_markers_with_defaults(a, b=1, /, c=2, d=3, *, e=4, f=5):
pass
def long_one_with_long_parameter_names(
but_all_of_them,
are_positional_only,
arguments_mmmmkay,
so_this_is_only_valid_after,
three_point_eight,
/,
):
pass
lambda a, /: a
lambda a, b, /, c, d, *, e, f: a
lambda a, b, /, c, d, *args, e, f, **kwargs: args
lambda a, b=1, /, c=2, d=3, *, e=4, f=5: 1

View File

@ -344,6 +344,23 @@ def test_fstring(self) -> None:
black.assert_equivalent(source, actual) black.assert_equivalent(source, actual)
black.assert_stable(source, actual, black.FileMode()) black.assert_stable(source, actual, black.FileMode())
@patch("black.dump_to_file", dump_to_stderr)
def test_pep_570(self) -> None:
source, expected = read_data("pep_570")
actual = fs(source)
self.assertFormatEqual(expected, actual)
black.assert_stable(source, actual, black.FileMode())
if sys.version_info >= (3, 8):
black.assert_equivalent(source, actual)
def test_detect_pos_only_arguments(self) -> None:
source, _ = read_data("pep_570")
root = black.lib2to3_parse(source)
features = black.get_features_used(root)
self.assertIn(black.Feature.POS_ONLY_ARGUMENTS, features)
versions = black.detect_target_versions(root)
self.assertIn(black.TargetVersion.PY38, versions)
@patch("black.dump_to_file", dump_to_stderr) @patch("black.dump_to_file", dump_to_stderr)
def test_string_quotes(self) -> None: def test_string_quotes(self) -> None:
source, expected = read_data("string_quotes") source, expected = read_data("string_quotes")