Skip to content

Commit

Permalink
Support PEP-570 (positional only arguments) (#946)
Browse files Browse the repository at this point in the history
Code using positional only arguments is considered >= 3.8
  • Loading branch information
zsol committed Jul 28, 2019
1 parent d8fa8df commit 2848e2e
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 11 deletions.
18 changes: 13 additions & 5 deletions black.py
Expand Up @@ -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]] = {
Expand Down Expand Up @@ -178,6 +179,7 @@ class Feature(Enum):
Feature.TRAILING_COMMA_IN_DEF,
Feature.ASYNC_KEYWORDS,
Feature.ASSIGNMENT_EXPRESSIONS,
Feature.POS_ONLY_ARGUMENTS,
},
}

Expand Down Expand Up @@ -935,6 +937,7 @@ def show(cls, code: Union[str, Leaf, Node]) -> None:
token.DOUBLESTAR,
}
STARS = {token.STAR, token.DOUBLESTAR}
VARARGS_SPECIALS = STARS | {token.SLASH}
VARARGS_PARENTS = {
syms.arglist,
syms.argument, # double star in arglist
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand All @@ -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)

Expand Down
52 changes: 46 additions & 6 deletions blib2to3/Grammar.txt
Expand Up @@ -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)* [',']
Expand Down
44 changes: 44 additions & 0 deletions 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
17 changes: 17 additions & 0 deletions tests/test_black.py
Expand Up @@ -344,6 +344,23 @@ def test_fstring(self) -> None:
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")
Expand Down

0 comments on commit 2848e2e

Please sign in to comment.