Repository: spyder-ide/spyder

Issue:
Black maximum allowed line length is not working.
## Problem Description
It seems that black isn't following maximum allowed line length option.
Is there a way to change the black option?


### What steps reproduce the problem?

1. Tools->Preferences->Completion and linting->Code Formatting->Choose the code formatting provider->black
2. Tools->Preferences->Completion and linting->Code style and formatting->Line length->Change "Maximum allowed length" to 100->OK
3. Source->Format file or seclection with Black


## Versions
<!--- You can get this information from Help > About Spyder...
or (if Spyder won't launch) the "conda list" command
from the Anaconda Prompt/Terminal/command line. --->

* Spyder version: 4.2.0 None
* Python version: 3.7.9 64-bit
* Qt version: 5.9.6
* PyQt5 version: 5.9.2
* Operating System: Windows 10

### Dependencies

# Mandatory:
atomicwrites >=1.2.0            :  1.4.0 (OK)
chardet >=2.0.0                 :  4.0.0 (OK)
cloudpickle >=0.5.0             :  1.6.0 (OK)
diff_match_patch >=20181111     :  20200713 (OK)
intervaltree >=3.0.2            :  3.1.0 (OK)
IPython >=4.0                   :  7.19.0 (OK)
jedi =0.17.2                    :  0.17.2 (OK)
jsonschema >=3.2.0              :  3.2.0 (OK)
nbconvert >=4.0                 :  6.0.7 (OK)
numpydoc >=0.6.0                :  1.1.0 (OK)
paramiko >=2.4.0                :  2.7.2 (OK)
parso =0.7.0                    :  0.7.0 (OK)
pexpect >=4.4.0                 :  4.8.0 (OK)
pickleshare >=0.4               :  0.7.5 (OK)
psutil >=5.3                    :  5.7.2 (OK)
pygments >=2.0                  :  2.7.3 (OK)
pylint >=1.0                    :  2.6.0 (OK)
pyls >=0.36.1;<1.0.0            :  0.36.2 (OK)
pyls_black >=0.4.6              :  0.4.6 (OK)
pyls_spyder >=0.1.1             :  0.3.0 (OK)
qdarkstyle >=2.8                :  2.8.1 (OK)
qtawesome >=0.5.7               :  1.0.1 (OK)
qtconsole >=4.7.7               :  4.7.7 (OK)
qtpy >=1.5.0                    :  1.9.0 (OK)
rtree >=0.8.3                   :  0.9.4 (OK)
setuptools >=39.0.0             :  51.0.0.post20201207 (OK)
sphinx >=0.6.6                  :  3.4.3 (OK)
spyder_kernels >=1.10.0;<1.11.0 :  1.10.1 (OK)
three_merge >=0.1.1             :  0.1.1 (OK)
watchdog >=0.10.3               :  0.10.4 (OK)
zmq >=17                        :  20.0.0 (OK)

# Optional:
cython >=0.21                   :  None (NOK)
matplotlib >=2.0.0              :  3.3.2 (OK)
numpy >=1.7                     :  1.19.2 (OK)
pandas >=1.1.1                  :  1.1.5 (OK)
scipy >=0.17.0                  :  1.5.2 (OK)
sympy >=0.7.3                   :  None (NOK)




Discussion:
Hi @mindnect thanks for opening this. I can successfully reproduce this error. We will fix it for 4.2.2!
Please @steff456 take care of this one. 
Hello, 

I was wondering when this issue might be resolved?

It seems that black is simply using its default value of 88.
To add to this, after updating to 5.2, it seems that autopep8 now also ignores the line length setting and goes for 88 characters :D

Resolving patch:
diff --git a/spyder/config/lsp.py b/spyder/config/lsp.py
--- a/spyder/config/lsp.py
+++ b/spyder/config/lsp.py
@@ -50,6 +50,11 @@
                 'pylsp_black': {
                     'enabled': False
                 },
+                'black': {
+                    'line_length': 79,
+                    'preview': False,
+                    'cache_config': False,
+                },
                 'yapf': {
                     'enabled': False
                 },
diff --git a/spyder/plugins/completion/providers/languageserver/provider.py b/spyder/plugins/completion/providers/languageserver/provider.py
--- a/spyder/plugins/completion/providers/languageserver/provider.py
+++ b/spyder/plugins/completion/providers/languageserver/provider.py
@@ -763,7 +763,12 @@ def generate_python_config(self):
 
         # Autoformatting configuration
         formatter = self.get_conf('formatting')
+
+        # This is necessary because PyLSP third-party plugins can only be
+        # disabled with their module name.
         formatter = 'pylsp_black' if formatter == 'black' else formatter
+
+        # Enabling/disabling formatters
         formatters = ['autopep8', 'yapf', 'pylsp_black']
         formatter_options = {
             fmt: {
@@ -772,8 +777,12 @@ def generate_python_config(self):
             for fmt in formatters
         }
 
-        if formatter == 'pylsp_black':
-            formatter_options['pylsp_black']['line_length'] = cs_max_line_length
+        # Setting max line length for formatters
+        # The autopep8 plugin shares the same maxLineLength value with the
+        # pycodestyle one. That's why it's not necessary to set it here.
+        # NOTE: We need to use `black` and not `pylsp_black` because that's
+        # the options' namespace of that plugin.
+        formatter_options['black'] = {'line_length': cs_max_line_length}
 
         # PyLS-Spyder configuration
         group_cells = self.get_conf(
@@ -837,6 +846,7 @@ def generate_python_config(self):
         python_config['host'] = host
         python_config['port'] = port
 
+        # Updating options
         plugins = python_config['configurations']['pylsp']['plugins']
         plugins['pycodestyle'].update(pycodestyle)
         plugins['pyflakes'].update(pyflakes)
@@ -847,8 +857,6 @@ def generate_python_config(self):
         plugins['jedi_signature_help'].update(jedi_signature_help)
         plugins['jedi_definition'].update(jedi_definition)
         plugins['preload']['modules'] = self.get_conf('preload_modules')
-
-        for formatter in formatters:
-            plugins[formatter] = formatter_options[formatter]
+        plugins.update(formatter_options)
 
         return python_config


Test patch:
diff --git a/spyder/plugins/editor/widgets/tests/assets/autopep8_max_line.py b/spyder/plugins/editor/widgets/tests/assets/autopep8_max_line.py
new file mode 100644
--- /dev/null
+++ b/spyder/plugins/editor/widgets/tests/assets/autopep8_max_line.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+"""
+Spyder Editor
+
+This is a temporary script file.
+"""
+
+import os
+import sys
+
+
+# %% functions
+def d():
+    def inner(): return 2
+    return inner
+# ---- func 1 and 2
+
+
+def func1():
+    for i in range(3):
+        print(i)
+
+
+def func2():
+    if True:
+        pass
+# ---- other functions
+
+
+def a():
+    pass
+
+
+def b():
+    pass
+
+
+def c():
+    pass
+
+
+# %% classes
+class Class1:
+    def __init__(self):
+        super(
+            Class1, self).__init__()
+        self.x = 2
+
+    def method3(self):
+        pass
+
+    def method2(self):
+        pass
+
+    def method1(self):
+        pass
diff --git a/spyder/plugins/editor/widgets/tests/assets/black_max_line.py b/spyder/plugins/editor/widgets/tests/assets/black_max_line.py
new file mode 100644
--- /dev/null
+++ b/spyder/plugins/editor/widgets/tests/assets/black_max_line.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+"""
+Spyder Editor
+
+This is a temporary script file.
+"""
+
+import os
+import sys
+
+
+# %% functions
+def d():
+    def inner():
+        return 2
+
+    return inner
+
+
+# ---- func 1 and 2
+def func1():
+    for i in range(
+        3
+    ):
+        print(i)
+
+
+def func2():
+    if True:
+        pass
+
+
+# ---- other functions
+def a():
+    pass
+
+
+def b():
+    pass
+
+
+def c():
+    pass
+
+
+# %% classes
+class Class1:
+    def __init__(
+        self,
+    ):
+        super(
+            Class1,
+            self,
+        ).__init__()
+        self.x = 2
+
+    def method3(
+        self,
+    ):
+        pass
+
+    def method2(
+        self,
+    ):
+        pass
+
+    def method1(
+        self,
+    ):
+        pass
diff --git a/spyder/plugins/editor/widgets/tests/assets/yapf_max_line.py b/spyder/plugins/editor/widgets/tests/assets/yapf_max_line.py
new file mode 100644
--- /dev/null
+++ b/spyder/plugins/editor/widgets/tests/assets/yapf_max_line.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+"""
+Spyder Editor
+
+This is a temporary script file.
+"""
+
+import os
+import sys
+
+
+# %% functions
+def d():
+
+    def inner():
+        return 2
+
+    return inner
+
+
+# ---- func 1 and 2
+def func1():
+    for i in range(
+            3):
+        print(i)
+
+
+def func2():
+    if True:
+        pass
+
+
+# ---- other functions
+def a():
+    pass
+
+
+def b():
+    pass
+
+
+def c():
+    pass
+
+
+# %% classes
+class Class1:
+
+    def __init__(
+            self):
+        super(
+            Class1,
+            self
+        ).__init__(
+        )
+        self.x = 2
+
+    def method3(
+            self):
+        pass
+
+    def method2(
+            self):
+        pass
+
+    def method1(
+            self):
+        pass
diff --git a/spyder/plugins/editor/widgets/tests/test_formatting.py b/spyder/plugins/editor/widgets/tests/test_formatting.py
--- a/spyder/plugins/editor/widgets/tests/test_formatting.py
+++ b/spyder/plugins/editor/widgets/tests/test_formatting.py
@@ -12,19 +12,19 @@
 
 # Third party imports
 import pytest
-import yapf
-
-# Qt imports
 from qtpy.QtGui import QTextCursor
+import yapf
 
 # Local imports
 from spyder.config.manager import CONF
 from spyder.utils.programs import is_module_installed
 
 
+# ---- Auxiliary constants and functions
 HERE = osp.dirname(osp.abspath(__file__))
 ASSETS = osp.join(HERE, 'assets')
 
+
 autopep8 = pytest.param(
     'autopep8',
     marks=pytest.mark.skipif(
@@ -44,14 +44,20 @@
 black = pytest.param(
     'black',
     marks=pytest.mark.skipif(
-        is_module_installed('pylsp_black', '<1.2.0'),
+        is_module_installed('python-lsp-black', '<1.2.0'),
         reason="Versions older than 1.2 don't handle eol's correctly"
     )
 )
 
 
-def get_formatter_values(formatter, newline, range_fmt=False):
-    suffix = 'range' if range_fmt else 'result'
+def get_formatter_values(formatter, newline, range_fmt=False, max_line=False):
+    if range_fmt:
+        suffix = 'range'
+    elif max_line:
+        suffix = 'max_line'
+    else:
+        suffix = 'result'
+
     original_file = osp.join(ASSETS, 'original_file.py')
     formatted_file = osp.join(ASSETS, '{0}_{1}.py'.format(formatter, suffix))
 
@@ -66,6 +72,7 @@ def get_formatter_values(formatter, newline, range_fmt=False):
     return text, result
 
 
+# ---- Tests
 @pytest.mark.slow
 @pytest.mark.order(1)
 @pytest.mark.parametrize('formatter', [autopep8, yapf, black])
@@ -76,10 +83,13 @@ def test_document_formatting(formatter, newline, completions_codeeditor,
     code_editor, completion_plugin = completions_codeeditor
     text, expected = get_formatter_values(formatter, newline)
 
-    # After this call the manager needs to be reinitialized
-    CONF.set('completions',
-             ('provider_configuration', 'lsp', 'values','formatting'),
-             formatter)
+    # Set formatter
+    CONF.set(
+        'completions',
+        ('provider_configuration', 'lsp', 'values', 'formatting'),
+        formatter
+    )
+    completion_plugin.after_configuration_update([])
     qtbot.wait(2000)
 
     # Set text in editor
@@ -114,10 +124,12 @@ def test_document_range_formatting(formatter, newline, completions_codeeditor,
     code_editor, completion_plugin = completions_codeeditor
     text, expected = get_formatter_values(formatter, newline, range_fmt=True)
 
-    # After this call the manager needs to be reinitialized
-    CONF.set('completions',
-             ('provider_configuration', 'lsp', 'values','formatting'),
-             formatter)
+    # Set formatter
+    CONF.set(
+        'completions',
+        ('provider_configuration', 'lsp', 'values', 'formatting'),
+        formatter
+    )
     completion_plugin.after_configuration_update([])
     qtbot.wait(2000)
 
@@ -151,3 +163,47 @@ def test_document_range_formatting(formatter, newline, completions_codeeditor,
     # Wait to text to be formatted
     qtbot.wait(2000)
     assert code_editor.get_text_with_eol() == expected
+
+
+@pytest.mark.slow
+@pytest.mark.order(1)
+@pytest.mark.parametrize('formatter', [autopep8, black])
+def test_max_line_length(formatter, completions_codeeditor, qtbot):
+    """Validate autoformatting with a different value of max_line_length."""
+    code_editor, completion_plugin = completions_codeeditor
+    text, expected = get_formatter_values(
+        formatter, newline='\n', max_line=True)
+    max_line_length = 20
+
+    # Set formatter and max line length options
+    CONF.set(
+        'completions',
+        ('provider_configuration', 'lsp', 'values', 'formatting'),
+        formatter
+    )
+    CONF.set(
+        'completions',
+        ('provider_configuration', 'lsp', 'values',
+         'pycodestyle/max_line_length'),
+        max_line_length
+    )
+    completion_plugin.after_configuration_update([])
+    qtbot.wait(2000)
+
+    # Set text in editor
+    code_editor.set_text(text)
+
+    # Notify changes
+    with qtbot.waitSignal(
+            code_editor.completions_response_signal, timeout=30000):
+        code_editor.document_did_change()
+
+    # Perform formatting
+    with qtbot.waitSignal(
+            code_editor.completions_response_signal, timeout=30000):
+        code_editor.format_document()
+
+    # Wait for text to be formatted
+    qtbot.wait(2000)
+
+    assert code_editor.get_text_with_eol() == expected