From 2848e2e1d6527d6031ea020cd991fd73e52c4a0b Mon Sep 17 00:00:00 2001 From: Zsolt Dollenstein Date: Sun, 28 Jul 2019 16:17:33 +0100 Subject: [PATCH] Support PEP-570 (positional only arguments) (#946) Code using positional only arguments is considered >= 3.8 --- black.py | 18 ++++++++++----- blib2to3/Grammar.txt | 52 ++++++++++++++++++++++++++++++++++++++----- tests/data/pep_570.py | 44 ++++++++++++++++++++++++++++++++++++ tests/test_black.py | 17 ++++++++++++++ 4 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 tests/data/pep_570.py diff --git a/black.py b/black.py index 9938b37..910a0ed 100644 --- a/black.py +++ b/black.py @@ -143,6 +143,7 @@ class Feature(Enum): ASYNC_IDENTIFIERS = 6 ASYNC_KEYWORDS = 7 ASSIGNMENT_EXPRESSIONS = 8 + POS_ONLY_ARGUMENTS = 9 VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { @@ -178,6 +179,7 @@ VERSION_TO_FEATURES: Dict[TargetVersion, Set[Feature]] = { Feature.TRAILING_COMMA_IN_DEF, Feature.ASYNC_KEYWORDS, Feature.ASSIGNMENT_EXPRESSIONS, + Feature.POS_ONLY_ARGUMENTS, }, } @@ -935,6 +937,7 @@ MATH_OPERATORS = { token.DOUBLESTAR, } STARS = {token.STAR, token.DOUBLESTAR} +VARARGS_SPECIALS = STARS | {token.SLASH} VARARGS_PARENTS = { syms.arglist, syms.argument, # double star in arglist @@ -1847,7 +1850,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 # that, too. return prevp.prefix - elif prevp.type in STARS: + elif prevp.type in VARARGS_SPECIALS: if is_vararg(prevp, within=VARARGS_PARENTS | UNPACKING_PARENTS): return NO @@ -1937,7 +1940,7 @@ def whitespace(leaf: Leaf, *, complex_subscript: bool) -> str: # noqa: C901 if not prevp or prevp.type == token.LPAR: return NO - elif prev.type in {token.EQUAL} | STARS: + elif prev.type in {token.EQUAL} | VARARGS_SPECIALS: return NO 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 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 p = leaf.parent @@ -3201,8 +3204,9 @@ def get_features_used(node: Node) -> Set[Feature]: Currently looking for: - f-strings; - - underscores in numeric literals; and - - trailing commas after * or ** in function signatures and calls. + - underscores in numeric literals; + - trailing commas after * or ** in function signatures and calls; + - positional only arguments in function signatures and lambdas; """ features: Set[Feature] = set() for n in node.pre_order(): @@ -3215,6 +3219,10 @@ def get_features_used(node: Node) -> Set[Feature]: if "_" in n.value: # type: ignore 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: features.add(Feature.ASSIGNMENT_EXPRESSIONS) diff --git a/blib2to3/Grammar.txt b/blib2to3/Grammar.txt index 1061ac8..daecb15 100644 --- a/blib2to3/Grammar.txt +++ b/blib2to3/Grammar.txt @@ -18,15 +18,55 @@ decorated: decorators (classdef | funcdef | async_funcdef) async_funcdef: ASYNC funcdef funcdef: 'def' NAME parameters ['->' test] ':' suite parameters: '(' [typedargslist] ')' -typedargslist: ((tfpdef ['=' test] ',')* - ('*' [tname] (',' tname ['=' test])* [',' ['**' tname [',']]] | '**' tname [',']) - | tfpdef ['=' test] (',' tfpdef ['=' test])* [',']) + +# The following definition for typedarglist is equivalent to this set of rules: +# +# 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] tfpdef: tname | '(' tfplist ')' tfplist: tfpdef (',' tfpdef)* [','] -varargslist: ((vfpdef ['=' test] ',')* - ('*' [vname] (',' vname ['=' test])* [',' ['**' vname [',']]] | '**' vname [',']) - | vfpdef ['=' test] (',' vfpdef ['=' test])* [',']) + +# The following definition for varargslist is equivalent to this set of rules: +# +# 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 vfpdef: vname | '(' vfplist ')' vfplist: vfpdef (',' vfpdef)* [','] diff --git a/tests/data/pep_570.py b/tests/data/pep_570.py new file mode 100644 index 0000000..ca8f7ab --- /dev/null +++ b/tests/data/pep_570.py @@ -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 diff --git a/tests/test_black.py b/tests/test_black.py index be6d98a..4e0a03a 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -344,6 +344,23 @@ class BlackTestCase(unittest.TestCase): black.assert_equivalent(source, actual) 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) def test_string_quotes(self) -> None: source, expected = read_data("string_quotes") -- 2.39.2