Add ALLOW_MULTILINE_DICTIONARY_KEYS knob
This allows us to split a dictionary key over multiple lines, which is
something pylint doesn't like.
diff --git a/CHANGELOG b/CHANGELOG
index 38e3272..ed619f1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -11,6 +11,8 @@
dictionary/set generator.
- The `BLANK_LINE_BEFORE_CLASS_DOCSTRING` knob adds a blank line before a
class's docstring.
+- The `ALLOW_MULTILINE_DICTIONARY_KEYS` knob allows dictionary keys to span
+ more than one line.
### Fixed
- Split before all entries in a dict/set or list maker when comma-terminated,
even if there's only one entry.
diff --git a/README.rst b/README.rst
index 6abb80d..c15464d 100644
--- a/README.rst
+++ b/README.rst
@@ -290,6 +290,17 @@
``ALLOW_MULTILINE_LAMBDAS``
Allow lambdas to be formatted on more than one line.
+``ALLOW_MULTILINE_DICTIONARY_KEYS``
+ Allow dictionary keys to exist on multiple lines. For example:
+
+ .. code-block:: python
+
+ x = {
+ ('this is the first element of a tuple',
+ 'this is the second element of a tuple'):
+ value,
+ }
+
``BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF``
Insert a blank line before a ``def`` or ``class`` immediately nested within
another ``def`` or ``class``. For example:
diff --git a/setup.py b/setup.py
index fc8781e..c73016b 100644
--- a/setup.py
+++ b/setup.py
@@ -64,5 +64,9 @@
'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Software Development :: Quality Assurance',
],
- entry_points={'console_scripts': ['yapf = yapf:run_main'],},
- cmdclass={'test': RunTests,},)
+ entry_points={
+ 'console_scripts': ['yapf = yapf:run_main'],
+ },
+ cmdclass={
+ 'test': RunTests,
+ },)
diff --git a/yapf/yapflib/format_decision_state.py b/yapf/yapflib/format_decision_state.py
index 93379e2..65e4c8d 100644
--- a/yapf/yapflib/format_decision_state.py
+++ b/yapf/yapflib/format_decision_state.py
@@ -111,13 +111,27 @@
(self.column, repr(self.next_token), self.paren_level,
'\n\t'.join(repr(s) for s in self.stack) + ']'))
- def CanSplit(self):
- """Returns True if the line can be split before the next token."""
+ def CanSplit(self, must_split):
+ """Determine if we can split before the next token.
+
+ Arguments:
+ must_split: (bool) A newline was required before this token.
+
+ Returns:
+ True if the line can be split before the next token.
+ """
current = self.next_token
if current.is_pseudo_paren:
return False
+ if (format_token.Subtype.DICTIONARY_KEY_PART in current.subtypes and
+ format_token.Subtype.DICTIONARY_KEY not in current.subtypes and
+ not style.Get('ALLOW_MULTILINE_DICTIONARY_KEYS') and not must_split):
+ # In some situations, a dictionary may be multiline, but pylint doesn't
+ # like it. So don't allow it unless forced to.
+ return False
+
return current.can_break_before
def MustSplit(self):
@@ -209,8 +223,7 @@
format_token.Subtype.DEFAULT_OR_NAMED_ASSIGN_ARG_LIST in
current.subtypes):
if (previous.value not in {'=', ':', '*', '**'} and
- current.value not in ':=,)' and
- not _IsFunctionDefinition(previous)):
+ current.value not in ':=,)' and not _IsFunctionDefinition(previous)):
# If we're going to split the lines because of named arguments, then we
# want to split after the opening bracket as well. But not when this is
# part of a function definition.
@@ -276,8 +289,8 @@
return True
pprevious = previous.previous_token
- if (current.is_name and pprevious and
- pprevious.is_name and previous.value == '('):
+ if (current.is_name and pprevious and pprevious.is_name and
+ previous.value == '('):
if (not self._FitsOnLine(previous, previous.matching_bracket) and
_IsFunctionCallWithArguments(current)):
# There is a function call, with more than 1 argument, where the first
diff --git a/yapf/yapflib/format_token.py b/yapf/yapflib/format_token.py
index 13d78ff..ec9f0e4 100644
--- a/yapf/yapflib/format_token.py
+++ b/yapf/yapflib/format_token.py
@@ -46,12 +46,13 @@
KWARGS_STAR_STAR = 9
ASSIGN_OPERATOR = 10
DICTIONARY_KEY = 11
- DICTIONARY_VALUE = 12
- DICT_SET_GENERATOR = 13
- COMP_FOR = 14
- COMP_IF = 15
- FUNC_DEF = 16
- DECORATOR = 17
+ DICTIONARY_KEY_PART = 12
+ DICTIONARY_VALUE = 13
+ DICT_SET_GENERATOR = 14
+ COMP_FOR = 15
+ COMP_IF = 16
+ FUNC_DEF = 17
+ DECORATOR = 18
class FormatToken(object):
diff --git a/yapf/yapflib/reformatter.py b/yapf/yapflib/reformatter.py
index 1d69783..5fd9ca1 100644
--- a/yapf/yapflib/reformatter.py
+++ b/yapf/yapflib/reformatter.py
@@ -357,10 +357,10 @@
Returns:
The updated number of elements in the queue.
"""
- if newline and not previous_node.state.CanSplit():
+ must_split = previous_node.state.MustSplit()
+ if newline and not previous_node.state.CanSplit(must_split):
# Don't add a newline if the token cannot be split.
return count
- must_split = previous_node.state.MustSplit()
if not newline and must_split:
# Don't add a token we must split but where we aren't splitting.
return count
diff --git a/yapf/yapflib/style.py b/yapf/yapflib/style.py
index 0b4a1f4..63d110a 100644
--- a/yapf/yapflib/style.py
+++ b/yapf/yapflib/style.py
@@ -51,6 +51,14 @@
Align closing bracket with visual indentation."""),
ALLOW_MULTILINE_LAMBDAS=textwrap.dedent("""\
Allow lambdas to be formatted on more than one line."""),
+ ALLOW_MULTILINE_DICTIONARY_KEYS=textwrap.dedent("""\
+ Allow dictionary keys to exist on multiple lines. For example:
+
+ x = {
+ ('this is the first element of a tuple',
+ 'this is the second element of a tuple'):
+ value,
+ }"""),
BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF=textwrap.dedent("""\
Insert a blank line before a 'def' or 'class' immediately nested
within another 'def' or 'class'. For example:
@@ -193,6 +201,7 @@
return dict(
ALIGN_CLOSING_BRACKET_WITH_VISUAL_INDENT=True,
ALLOW_MULTILINE_LAMBDAS=False,
+ ALLOW_MULTILINE_DICTIONARY_KEYS=False,
BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF=False,
BLANK_LINE_BEFORE_CLASS_DOCSTRING=False,
COALESCE_BRACKETS=False,
@@ -301,6 +310,7 @@
_STYLE_OPTION_VALUE_CONVERTER = dict(
ALIGN_CLOSING_BRACKET_WITH_VISUAL_INDENT=_BoolConverter,
ALLOW_MULTILINE_LAMBDAS=_BoolConverter,
+ ALLOW_MULTILINE_DICTIONARY_KEYS=_BoolConverter,
BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF=_BoolConverter,
BLANK_LINE_BEFORE_CLASS_DOCSTRING=_BoolConverter,
COALESCE_BRACKETS=_BoolConverter,
diff --git a/yapf/yapflib/subtype_assigner.py b/yapf/yapflib/subtype_assigner.py
index 098eb54..3e27fbf 100644
--- a/yapf/yapflib/subtype_assigner.py
+++ b/yapf/yapflib/subtype_assigner.py
@@ -98,6 +98,7 @@
# on a single line.
_AppendFirstLeafTokenSubtype(child,
format_token.Subtype.DICTIONARY_KEY)
+ _AppendSubtypeRec(child, format_token.Subtype.DICTIONARY_KEY_PART)
last_was_colon = pytree_utils.NodeName(child) == 'COLON'
def Visit_expr_stmt(self, node): # pylint: disable=invalid-name
diff --git a/yapftests/format_decision_state_test.py b/yapftests/format_decision_state_test.py
index 5f09795..1afa325 100644
--- a/yapftests/format_decision_state_test.py
+++ b/yapftests/format_decision_state_test.py
@@ -42,42 +42,42 @@
# Add: 'f'
state = format_decision_state.FormatDecisionState(uwline, 0)
self.assertEqual('f', state.next_token.value)
- self.assertFalse(state.CanSplit())
+ self.assertFalse(state.CanSplit(False))
# Add: '('
state.AddTokenToState(False, True)
self.assertEqual('(', state.next_token.value)
- self.assertFalse(state.CanSplit())
+ self.assertFalse(state.CanSplit(False))
self.assertFalse(state.MustSplit())
# Add: 'a'
state.AddTokenToState(False, True)
self.assertEqual('a', state.next_token.value)
- self.assertTrue(state.CanSplit())
+ self.assertTrue(state.CanSplit(False))
self.assertFalse(state.MustSplit())
# Add: ','
state.AddTokenToState(False, True)
self.assertEqual(',', state.next_token.value)
- self.assertFalse(state.CanSplit())
+ self.assertFalse(state.CanSplit(False))
self.assertFalse(state.MustSplit())
# Add: 'b'
state.AddTokenToState(False, True)
self.assertEqual('b', state.next_token.value)
- self.assertTrue(state.CanSplit())
+ self.assertTrue(state.CanSplit(False))
self.assertFalse(state.MustSplit())
# Add: ')'
state.AddTokenToState(False, True)
self.assertEqual(')', state.next_token.value)
- self.assertTrue(state.CanSplit())
+ self.assertTrue(state.CanSplit(False))
self.assertFalse(state.MustSplit())
# Add: ':'
state.AddTokenToState(False, True)
self.assertEqual(':', state.next_token.value)
- self.assertFalse(state.CanSplit())
+ self.assertFalse(state.CanSplit(False))
self.assertFalse(state.MustSplit())
clone = state.Clone()
@@ -95,37 +95,37 @@
# Add: 'f'
state = format_decision_state.FormatDecisionState(uwline, 0)
self.assertEqual('f', state.next_token.value)
- self.assertFalse(state.CanSplit())
+ self.assertFalse(state.CanSplit(False))
# Add: '('
state.AddTokenToState(True, True)
self.assertEqual('(', state.next_token.value)
- self.assertFalse(state.CanSplit())
+ self.assertFalse(state.CanSplit(False))
# Add: 'a'
state.AddTokenToState(True, True)
self.assertEqual('a', state.next_token.value)
- self.assertTrue(state.CanSplit())
+ self.assertTrue(state.CanSplit(False))
# Add: ','
state.AddTokenToState(True, True)
self.assertEqual(',', state.next_token.value)
- self.assertFalse(state.CanSplit())
+ self.assertFalse(state.CanSplit(False))
# Add: 'b'
state.AddTokenToState(True, True)
self.assertEqual('b', state.next_token.value)
- self.assertTrue(state.CanSplit())
+ self.assertTrue(state.CanSplit(False))
# Add: ')'
state.AddTokenToState(True, True)
self.assertEqual(')', state.next_token.value)
- self.assertTrue(state.CanSplit())
+ self.assertTrue(state.CanSplit(False))
# Add: ':'
state.AddTokenToState(True, True)
self.assertEqual(':', state.next_token.value)
- self.assertFalse(state.CanSplit())
+ self.assertFalse(state.CanSplit(False))
clone = state.Clone()
self.assertEqual(repr(state), repr(clone))
diff --git a/yapftests/reformatter_basic_test.py b/yapftests/reformatter_basic_test.py
index b612099..5724122 100644
--- a/yapftests/reformatter_basic_test.py
+++ b/yapftests/reformatter_basic_test.py
@@ -1510,6 +1510,38 @@
finally:
style.SetGlobalStyle(style.CreateChromiumStyle())
+ def testMultilineDictionaryKeys(self):
+ try:
+ style.SetGlobalStyle(
+ style.CreateStyleFromConfig('{based_on_style: chromium, '
+ 'allow_multiline_dictionary_keys: true}'))
+ unformatted_code = textwrap.dedent("""\
+ MAP_WITH_LONG_KEYS = {
+ ('lorem ipsum', 'dolor sit amet'):
+ 1,
+ ('consectetur adipiscing elit.', 'Vestibulum mauris justo, ornare eget dolor eget'):
+ 2,
+ ('vehicula convallis nulla. Vestibulum dictum nisl in malesuada finibus.',):
+ 3
+ }
+ """)
+ expected_formatted_code = textwrap.dedent("""\
+ MAP_WITH_LONG_KEYS = {
+ ('lorem ipsum', 'dolor sit amet'):
+ 1,
+ ('consectetur adipiscing elit.',
+ 'Vestibulum mauris justo, ornare eget dolor eget'):
+ 2,
+ ('vehicula convallis nulla. Vestibulum dictum nisl in malesuada finibus.',):
+ 3
+ }
+ """)
+ uwlines = yapf_test_helper.ParseAndUnwrap(unformatted_code)
+ self.assertCodeEqual(expected_formatted_code,
+ reformatter.Reformat(uwlines))
+ finally:
+ style.SetGlobalStyle(style.CreateChromiumStyle())
+
def testStableDictionaryFormatting(self):
try:
style.SetGlobalStyle(
@@ -1992,8 +2024,7 @@
try:
style.SetGlobalStyle(
- style.CreateStyleFromConfig(
- '{based_on_style: chromium, \
+ style.CreateStyleFromConfig('{based_on_style: chromium, \
blank_line_before_class_docstring: True}'))
unformatted_code = textwrap.dedent('''\
class A: