Added functionality to insert spaces around dicts, lists, and tuples. (#657)
* Added functionality to insert spaces around dicts, lists, and tuples.
* Removed hard-coded values
* Added missing vertical whitespace
* Modified whitespacing around examples
* Use "OpensScope()" and "ClosesScope()"
Co-authored-by: Bill Wendling <[email protected]>
diff --git a/CHANGELOG b/CHANGELOG
index 236ca89..dd0265e 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,7 +2,17 @@
# All notable changes to this project will be documented in this file.
# This project adheres to [Semantic Versioning](http://semver.org/).
-## [0.29.1] UNRELEASED
+## [0.30.0] UNRELEASED
+### Added
+- Added `SPACES_AROUND_LIST_DELIMITERS`, `SPACES_AROUND_DICT_DELIMITERS`,
+ and `SPACES_AROUND_TUPLE_DELIMITERS` to add spaces after the opening-
+ and before the closing-delimiters for lists, dicts, and tuples.
+- Adds `FORCE_MULTILINE_DICT` knob to ensure dictionaries always split,
+ even when shorter than the max line length.
+- New knob `SPACE_INSIDE_BRACKETS` to add spaces inside brackets, braces, and
+ parentheses.
+- New knob `SPACES_AROUND_SUBSCRIPT_COLON` to add spaces around the subscript /
+ slice operator.
### Fixed
- Honor a disable directive at the end of a multiline comment.
- Don't require splitting before comments in a list when
@@ -11,12 +21,6 @@
- Don't over-indent a parameter list when not needed. But make sure it is
properly indented so that it doesn't collide with the lines afterwards.
- Don't split between two-word comparison operators: "is not", "not in", etc.
-- Adds `FORCE_MULTILINE_DICT` knob to ensure dictionaries always split,
- even when shorter than the max line length.
-- New knob `SPACE_INSIDE_BRACKETS` to add spaces inside brackets, braces, and
- parentheses.
-- New knob `SPACES_AROUND_SUBSCRIPT_COLON` to add spaces around the subscript /
- slice operator.
## [0.29.0] 2019-11-28
### Added
diff --git a/README.rst b/README.rst
index 274e1f8..cc53c89 100644
--- a/README.rst
+++ b/README.rst
@@ -549,6 +549,32 @@
Set to ``True`` to prefer spaces around the assignment operator for default
or keyword arguments.
+``SPACES_AROUND_DICT_DELIMITERS``
+ Adds a space after the opening '{' and before the ending '}' dict delimiters.
+
+ .. code-block:: python
+
+ {1: 2}
+
+ will be formatted as:
+
+ .. code-block:: python
+
+ { 1: 2 }
+
+``SPACES_AROUND_LIST_DELIMITERS``
+ Adds a space after the opening '[' and before the ending ']' list delimiters.
+
+ .. code-block:: python
+
+ [1, 2]
+
+ will be formatted as:
+
+ .. code-block:: python
+
+ [ 1, 2 ]
+
``SPACES_AROUND_SUBSCRIPT_COLON``
Use spaces around the subscript / slice operator. For example:
@@ -556,6 +582,19 @@
my_list[1 : 10 : 2]
+``SPACES_AROUND_TUPLE_DELIMITERS``
+ Adds a space after the opening '(' and before the ending ')' tuple delimiters.
+
+ .. code-block:: python
+
+ (1, 2, 3)
+
+ will be formatted as:
+
+ .. code-block:: python
+
+ ( 1, 2, 3 )
+
``SPACES_BEFORE_COMMENT``
The number of spaces required before a trailing comment.
This can be a single value (representing the number of spaces
diff --git a/yapf/yapflib/style.py b/yapf/yapflib/style.py
index 3747941..b8213cd 100644
--- a/yapf/yapflib/style.py
+++ b/yapf/yapflib/style.py
@@ -31,6 +31,11 @@
return _style[setting_name]
+def GetOrDefault(setting_name, default_value):
+ """Get a style setting or default value if the setting does not exist."""
+ return _style.get(setting_name, default_value)
+
+
def Help():
"""Return dict mapping style names to help strings."""
return _STYLE_HELP
@@ -149,24 +154,8 @@
transform=Transformation.AVERAGE(window=timedelta(seconds=60)),
start_ts=now()-timedelta(days=3),
end_ts=now(),
- ) # <--- this bracket is dedented and on a separate line"""),
- INDENT_CLOSING_BRACKETS=textwrap.dedent("""\
- Put closing brackets on a separate line, indented, if the bracketed
- expression can't fit in a single line. Applies to all kinds of brackets,
- including function definitions and calls. For example:
-
- config = {
- 'key1': 'value1',
- 'key2': 'value2',
- } # <--- this bracket is indented and on a separate line
-
- time_series = self.remote_client.query_entity_counters(
- entity='dev3246.region1',
- key='dns.query_latency_tcp',
- transform=Transformation.AVERAGE(window=timedelta(seconds=60)),
- start_ts=now()-timedelta(days=3),
- end_ts=now(),
- ) # <--- this bracket is indented and on a separate line"""),
+ ) # <--- this bracket is dedented and on a separate line
+ """),
DISABLE_ENDING_COMMA_HEURISTIC=textwrap.dedent("""\
Disable the heuristic which places each list element on a separate line
if the list is comma-terminated."""),
@@ -187,6 +176,24 @@
The i18n function call names. The presence of this function stops
reformattting on that line, because the string it has cannot be moved
away from the i18n comment."""),
+ INDENT_CLOSING_BRACKETS=textwrap.dedent("""\
+ Put closing brackets on a separate line, indented, if the bracketed
+ expression can't fit in a single line. Applies to all kinds of brackets,
+ including function definitions and calls. For example:
+
+ config = {
+ 'key1': 'value1',
+ 'key2': 'value2',
+ } # <--- this bracket is indented and on a separate line
+
+ time_series = self.remote_client.query_entity_counters(
+ entity='dev3246.region1',
+ key='dns.query_latency_tcp',
+ transform=Transformation.AVERAGE(window=timedelta(seconds=60)),
+ start_ts=now()-timedelta(days=3),
+ end_ts=now(),
+ ) # <--- this bracket is indented and on a separate line
+ """),
INDENT_DICTIONARY_VALUE=textwrap.dedent("""\
Indent the dictionary value if it cannot fit on the same line as the
dictionary key. For example:
@@ -196,7 +203,8 @@
'value1',
'key2': value1 +
value2,
- }"""),
+ }
+ """),
INDENT_WIDTH=textwrap.dedent("""\
The number of columns to use for indentation."""),
INDENT_BLANK_LINES=textwrap.dedent("""\
@@ -226,10 +234,37 @@
Use spaces around the power operator."""),
SPACES_AROUND_DEFAULT_OR_NAMED_ASSIGN=textwrap.dedent("""\
Use spaces around default or named assigns."""),
+ SPACES_AROUND_DICT_DELIMITERS=textwrap.dedent("""\
+ Adds a space after the opening '{' and before the ending '}' dict delimiters.
+
+ {1: 2}
+
+ will be formatted as:
+
+ { 1: 2 }
+ """),
+ SPACES_AROUND_LIST_DELIMITERS=textwrap.dedent("""\
+ Adds a space after the opening '[' and before the ending ']' list delimiters.
+
+ [1, 2]
+
+ will be formatted as:
+
+ [ 1, 2 ]
+ """),
SPACES_AROUND_SUBSCRIPT_COLON=textwrap.dedent("""\
Use spaces around the subscript / slice operator. For example:
my_list[1 : 10 : 2]
+ """)
+ SPACES_AROUND_TUPLE_DELIMITERS=textwrap.dedent("""\
+ Adds a space after the opening '(' and before the ending ')' tuple delimiters.
+
+ (1, 2, 3)
+
+ will be formatted as:
+
+ ( 1, 2, 3 )
"""),
SPACES_BEFORE_COMMENT=textwrap.dedent("""\
The number of spaces required before a trailing comment.
@@ -407,7 +442,10 @@
SPACE_INSIDE_BRACKETS=False,
SPACES_AROUND_POWER_OPERATOR=False,
SPACES_AROUND_DEFAULT_OR_NAMED_ASSIGN=False,
+ SPACES_AROUND_DICT_DELIMITERS=False,
+ SPACES_AROUND_LIST_DELIMITERS=False,
SPACES_AROUND_SUBSCRIPT_COLON=False,
+ SPACES_AROUND_TUPLE_DELIMITERS=False,
SPACES_BEFORE_COMMENT=2,
SPLIT_ARGUMENTS_WHEN_COMMA_TERMINATED=False,
SPLIT_ALL_COMMA_SEPARATED_VALUES=False,
@@ -592,7 +630,10 @@
SPACE_INSIDE_BRACKETS=_BoolConverter,
SPACES_AROUND_POWER_OPERATOR=_BoolConverter,
SPACES_AROUND_DEFAULT_OR_NAMED_ASSIGN=_BoolConverter,
+ SPACES_AROUND_DICT_DELIMITERS=_BoolConverter,
+ SPACES_AROUND_LIST_DELIMITERS=_BoolConverter,
SPACES_AROUND_SUBSCRIPT_COLON=_BoolConverter,
+ SPACES_AROUND_TUPLE_DELIMITERS=_BoolConverter,
SPACES_BEFORE_COMMENT=_IntOrIntListConverter,
SPLIT_ARGUMENTS_WHEN_COMMA_TERMINATED=_BoolConverter,
SPLIT_ALL_COMMA_SEPARATED_VALUES=_BoolConverter,
diff --git a/yapf/yapflib/unwrapped_line.py b/yapf/yapflib/unwrapped_line.py
index ec28f9b..38501c0 100644
--- a/yapf/yapflib/unwrapped_line.py
+++ b/yapf/yapflib/unwrapped_line.py
@@ -25,6 +25,8 @@
from yapf.yapflib import split_penalty
from yapf.yapflib import style
+from lib2to3.fixer_util import syms as python_symbols
+
class UnwrappedLine(object):
"""Represents a single unwrapped line in the output.
@@ -69,7 +71,7 @@
prev_length = self.first.total_length
for token in self._tokens[1:]:
if (token.spaces_required_before == 0 and
- _SpaceRequiredBetween(prev_token, token)):
+ _SpaceRequiredBetween(prev_token, token, self.disable)):
token.spaces_required_before = 1
tok_len = len(token.value) if not token.is_pseudo_paren else 0
@@ -265,7 +267,7 @@
return (token1.is_number or token1.is_name) and token2.is_subscript_colon
-def _SpaceRequiredBetween(left, right):
+def _SpaceRequiredBetween(left, right, is_line_disabled):
"""Return True if a space is required between the left and right token."""
lval = left.value
rval = right.value
@@ -411,6 +413,22 @@
(lval == '{' and rval == '}')):
# Empty objects shouldn't be separated by spaces.
return False
+ if not is_line_disabled and (left.OpensScope() or right.ClosesScope()):
+ if (style.GetOrDefault('SPACES_AROUND_DICT_DELIMITERS', False) and (
+ (lval == '{' and _IsDictListTupleDelimiterTok(left, is_opening=True)) or
+ (rval == '}' and
+ _IsDictListTupleDelimiterTok(right, is_opening=False)))):
+ return True
+ if (style.GetOrDefault('SPACES_AROUND_LIST_DELIMITERS', False) and (
+ (lval == '[' and _IsDictListTupleDelimiterTok(left, is_opening=True)) or
+ (rval == ']' and
+ _IsDictListTupleDelimiterTok(right, is_opening=False)))):
+ return True
+ if (style.GetOrDefault('SPACES_AROUND_TUPLE_DELIMITERS', False) and (
+ (lval == '(' and _IsDictListTupleDelimiterTok(left, is_opening=True)) or
+ (rval == ')' and
+ _IsDictListTupleDelimiterTok(right, is_opening=False)))):
+ return True
if (lval in pytree_utils.OPENING_BRACKETS and
rval in pytree_utils.OPENING_BRACKETS):
# Nested objects' opening brackets shouldn't be separated, unless enabled
@@ -549,6 +567,33 @@
return None
+def _IsDictListTupleDelimiterTok(tok, is_opening):
+ assert tok
+
+ if tok.matching_bracket is None:
+ return False
+
+ if is_opening:
+ open_tok = tok
+ close_tok = tok.matching_bracket
+ else:
+ open_tok = tok.matching_bracket
+ close_tok = tok
+
+ # There must be something in between the tokens
+ if open_tok.next_token == close_tok:
+ return False
+
+ assert open_tok.next_token.node
+ assert open_tok.next_token.node.parent
+
+ return open_tok.next_token.node.parent.type in [
+ python_symbols.dictsetmaker,
+ python_symbols.listmaker,
+ python_symbols.testlist_gexp,
+ ]
+
+
_LOGICAL_OPERATORS = frozenset({'and', 'or'})
_BITWISE_OPERATORS = frozenset({'&', '|', '^'})
_ARITHMETIC_OPERATORS = frozenset({'+', '-', '*', '/', '%', '//', '@'})
diff --git a/yapftests/yapf_test.py b/yapftests/yapf_test.py
index 25e4f9b..46fde99 100644
--- a/yapftests/yapf_test.py
+++ b/yapftests/yapf_test.py
@@ -1778,5 +1778,188 @@
self._Check(unformatted_code, expected_formatted_code)
+class _SpacesAroundDictListTupleTestImpl(unittest.TestCase):
+
+ @staticmethod
+ def _OwnStyle():
+ my_style = style.CreatePEP8Style()
+ my_style['DISABLE_ENDING_COMMA_HEURISTIC'] = True
+ my_style['SPLIT_ALL_COMMA_SEPARATED_VALUES'] = False
+ my_style['SPLIT_ARGUMENTS_WHEN_COMMA_TERMINATED'] = False
+ return my_style
+
+ def _Check(self, unformatted_code, expected_formatted_code):
+ formatted_code, _ = yapf_api.FormatCode(
+ unformatted_code, style_config=style.SetGlobalStyle(self._OwnStyle()))
+ self.assertEqual(expected_formatted_code, formatted_code)
+
+ def setUp(self):
+ self.maxDiff = None
+
+
+class SpacesAroundDictTest(_SpacesAroundDictListTupleTestImpl):
+
+ @classmethod
+ def _OwnStyle(cls):
+ style = super(SpacesAroundDictTest, cls)._OwnStyle()
+ style['SPACES_AROUND_DICT_DELIMITERS'] = True
+
+ return style
+
+ def testStandard(self):
+ unformatted_code = textwrap.dedent("""\
+ {1 : 2}
+ {k:v for k, v in other.items()}
+ {k for k in [1, 2, 3]}
+
+ # The following statements should not change
+ {}
+ {1 : 2} # yapf: disable
+
+ # yapf: disable
+ {1 : 2}
+ # yapf: enable
+
+ # Dict settings should not impact lists or tuples
+ [1, 2]
+ (3, 4)
+ """)
+ expected_formatted_code = textwrap.dedent("""\
+ { 1: 2 }
+ { k: v for k, v in other.items() }
+ { k for k in [1, 2, 3] }
+
+ # The following statements should not change
+ {}
+ {1 : 2} # yapf: disable
+
+ # yapf: disable
+ {1 : 2}
+ # yapf: enable
+
+ # Dict settings should not impact lists or tuples
+ [1, 2]
+ (3, 4)
+ """)
+
+ self._Check(unformatted_code, expected_formatted_code)
+
+
+class SpacesAroundListTest(_SpacesAroundDictListTupleTestImpl):
+
+ @classmethod
+ def _OwnStyle(cls):
+ style = super(SpacesAroundListTest, cls)._OwnStyle()
+ style['SPACES_AROUND_LIST_DELIMITERS'] = True
+
+ return style
+
+ def testStandard(self):
+ unformatted_code = textwrap.dedent("""\
+ [a,b,c]
+ [4,5,]
+ [6, [7, 8], 9]
+ [v for v in [1,2,3] if v & 1]
+
+ # The following statements should not change
+ index[0]
+ index[a, b]
+ []
+ [v for v in [1,2,3] if v & 1] # yapf: disable
+
+ # yapf: disable
+ [a,b,c]
+ [4,5,]
+ # yapf: enable
+
+ # List settings should not impact dicts or tuples
+ {a: b}
+ (1, 2)
+ """)
+ expected_formatted_code = textwrap.dedent("""\
+ [ a, b, c ]
+ [ 4, 5, ]
+ [ 6, [ 7, 8 ], 9 ]
+ [ v for v in [ 1, 2, 3 ] if v & 1 ]
+
+ # The following statements should not change
+ index[0]
+ index[a, b]
+ []
+ [v for v in [1,2,3] if v & 1] # yapf: disable
+
+ # yapf: disable
+ [a,b,c]
+ [4,5,]
+ # yapf: enable
+
+ # List settings should not impact dicts or tuples
+ {a: b}
+ (1, 2)
+ """)
+
+ self._Check(unformatted_code, expected_formatted_code)
+
+
+class SpacesAroundTupleTest(_SpacesAroundDictListTupleTestImpl):
+
+ @classmethod
+ def _OwnStyle(cls):
+ style = super(SpacesAroundTupleTest, cls)._OwnStyle()
+ style['SPACES_AROUND_TUPLE_DELIMITERS'] = True
+
+ return style
+
+ def testStandard(self):
+ unformatted_code = textwrap.dedent("""\
+ (0, 1)
+ (2, 3)
+ (4, 5, 6,)
+ func((7, 8), 9)
+
+ # The following statements should not change
+ func(1, 2)
+ (this_func or that_func)(3, 4)
+ if (True and False): pass
+ ()
+
+ (0, 1) # yapf: disable
+
+ # yapf: disable
+ (0, 1)
+ (2, 3)
+ # yapf: enable
+
+ # Tuple settings should not impact dicts or lists
+ {a: b}
+ [3, 4]
+ """)
+ expected_formatted_code = textwrap.dedent("""\
+ ( 0, 1 )
+ ( 2, 3 )
+ ( 4, 5, 6, )
+ func(( 7, 8 ), 9)
+
+ # The following statements should not change
+ func(1, 2)
+ (this_func or that_func)(3, 4)
+ if (True and False): pass
+ ()
+
+ (0, 1) # yapf: disable
+
+ # yapf: disable
+ (0, 1)
+ (2, 3)
+ # yapf: enable
+
+ # Tuple settings should not impact dicts or lists
+ {a: b}
+ [3, 4]
+ """)
+
+ self._Check(unformatted_code, expected_formatted_code)
+
+
if __name__ == '__main__':
unittest.main()