Repository: spyder-ide/spyder

Issue:
re.error when getting signature in the editor
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

I was just typing the statement new_shape = np.concatenate(()) and the issue reporter popped up.

### Traceback
```python-traceback
    documentation=documentation,
    max_width=max_width)
    max_width=max_width
    row = re.sub(pattern, handle_sub, row)
    return _compile(pattern, flags).sub(repl, string, count)
    p = sre_compile.compile(pattern, flags)
    p = sre_parse.parse(p, flags)
    p = _parse_sub(source, pattern, flags & SRE_FLAG_VERBOSE, 0)
    not nested and not items))
    source.tell() - start)
re.error: missing ), unterminated subpattern at position 9

Traceback (most recent call last):
    msg = self.format(record)
    return fmt.format(record)
    record.message = record.getMessage()
    msg = msg % self.args
ValueError: incomplete format
Call stack:
    sys.exit(main())
    mainwindow.main()
    mainwindow = run_spyder(app, options, args)
    app.exec_()
    handler(resp['result'], req_id)
    {'params': response})
    self.COMPLETION_CLIENT_NAME, req_id, response)
    completion_source, response_instance, req_type, req_id)
    response_instance.handle_response(req_type, responses)
    handler(params)
    self.log_lsp_handle_errors("Error when processing signature")
    logger.error('%', 1, stack_info=True)
Arguments: (1,)
```

## Versions

* Spyder version: 4.0.0b4 
* Python version: 3.7.4
* Qt version: 5.12.4
* PyQt5 version: 5.12.3
* Operating System: Linux 4.15.0-54-generic

### Dependencies

```
pygments >=2.0    :  2.4.2 (OK)
qdarkstyle >=2.6.4:  2.7 (OK)
sphinx >=0.6.6    :  2.1.2 (OK)
psutil >=0.3      :  5.6.3 (OK)
pyls >=0.27.0     :  0.28.0 (OK)
nbconvert >=4.0   :  5.5.0 (OK)
pandas >=0.13.1   :  None (NOK)
numpy >=1.7       :  1.16.4 (OK)
sympy >=0.7.3     :  None (NOK)
cython >=0.21     :  0.29.12 (OK)
qtconsole >=4.5.2 :  4.5.2 (OK)
IPython >=4.0     :  7.7.0 (OK)
matplotlib >=2.0.0:  3.1.0 (OK)
pylint >=0.25     :  2.3.1 (OK)
```



Discussion:
@dalthviz, please take a look at this one.

Resolving patch:
diff --git a/spyder/widgets/mixins.py b/spyder/widgets/mixins.py
--- a/spyder/widgets/mixins.py
+++ b/spyder/widgets/mixins.py
@@ -319,6 +319,7 @@ def _format_signature(self, signatures, parameter=None,
 
         Special chars depend on the language.
         """
+        language = getattr(self, 'language', language).lower()
         active_parameter_template = (
             '<span style=\'font-family:"{font_family}";'
             'font-size:{font_size}pt;'
@@ -354,11 +355,24 @@ def handle_sub(matchobj):
             signature = signature.replace('( ', '(')
 
             # Process signature template
-            if parameter:
-                # '*' has a meaning in regex so needs to be escaped
-                if '*' in parameter:
-                    parameter = parameter.replace('*', '\\*')
-                pattern = r'[\*|(|\s](' + parameter + r')[,|)|\s|=]'
+            if parameter and language == 'python':
+                # Escape all possible regex characters
+                # ( ) { } | [ ] . ^ $ * +
+                escape_regex_chars = ['|', '.', '^', '$', '*', '+']
+                remove_regex_chars = ['(', ')', '{', '}', '[', ']']
+                regex_parameter = parameter
+                for regex_char in escape_regex_chars + remove_regex_chars:
+                    if regex_char in escape_regex_chars:
+                        escape_char = r'\{char}'.format(char=regex_char)
+                        regex_parameter = regex_parameter.replace(regex_char,
+                                                                  escape_char)
+                    else:
+                        regex_parameter = regex_parameter.replace(regex_char,
+                                                                  '')
+                        parameter = parameter.replace(regex_char, '')
+
+                pattern = (r'[\*|\(|\[|\s](' + regex_parameter +
+                           r')[,|\)|\]|\s|=]')
 
             formatted_lines = []
             name = signature.split('(')[0]
@@ -366,15 +380,15 @@ def handle_sub(matchobj):
             rows = textwrap.wrap(signature, width=max_width,
                                  subsequent_indent=indent)
             for row in rows:
-                if parameter:
+                if parameter and language == 'python':
                     # Add template to highlight the active parameter
                     row = re.sub(pattern, handle_sub, row)
 
                 row = row.replace(' ', '&nbsp;')
                 row = row.replace('span&nbsp;', 'span ')
+                row = row.replace('{}', '{{}}')
 
-                language = getattr(self, 'language', language)
-                if language and 'python' == language.lower():
+                if language and language == 'python':
                     for char in ['(', ')', ',', '*', '**']:
                         new_char = chars_template.format(char=char)
                         row = row.replace(char, new_char)
@@ -388,7 +402,7 @@ def handle_sub(matchobj):
             font_family = font.family()
 
             # Format title to display active parameter
-            if parameter:
+            if parameter and language == 'python':
                 title = title_template.format(
                     font_size=font_size,
                     font_family=font_family,


Test patch:
diff --git a/spyder/plugins/editor/widgets/tests/test_codeeditor_1.py b/spyder/plugins/editor/widgets/tests/test_codeeditor_1.py
--- a/spyder/plugins/editor/widgets/tests/test_codeeditor_1.py
+++ b/spyder/plugins/editor/widgets/tests/test_codeeditor_1.py
@@ -58,6 +58,46 @@ def copy_files_clipboard(create_folders_files):
 
 # --- Tests
 # -----------------------------------------------------------------------------
+def test_format_signature(code_editor_bot):
+    """Test signature format method."""
+    signature = """
+    concatenate((a1, a2, a...), [b1, b2, b...], axis={}, index=[],
+                *args, **kargs)"""
+    editor, qtbot = code_editor_bot
+
+    format_signature = editor._format_signature(signature, parameter="(a1")
+
+    assert "color:#DAA520'><b>a1</b></span>" in format_signature
+
+    format_signature = editor._format_signature(signature, parameter="a2")
+    assert "color:#DAA520'><b>a2</b></span>" in format_signature
+
+    format_signature = editor._format_signature(signature, parameter="a...")
+    print(format_signature)
+    assert "color:#DAA520'><b>a...</b></span>" in format_signature
+
+    format_signature = editor._format_signature(signature, parameter="[b1")
+    assert "color:#DAA520'><b>b1</b></span>" in format_signature
+
+    format_signature = editor._format_signature(signature, parameter="b2")
+    assert "color:#DAA520'><b>b2</b></span>" in format_signature
+
+    format_signature = editor._format_signature(signature, parameter="b...")
+    assert "color:#DAA520'><b>b...</b></span>" in format_signature
+
+    format_signature = editor._format_signature(signature, parameter="axis")
+    assert "color:#DAA520'><b>axis</b></span>" in format_signature
+
+    format_signature = editor._format_signature(signature, parameter="index")
+    assert "color:#DAA520'><b>index</b></span>" in format_signature
+
+    format_signature = editor._format_signature(signature, parameter="*args")
+    assert "color:#DAA520'><b>*args</b></span>" in format_signature
+
+    format_signature = editor._format_signature(signature, parameter="**kargs")
+    assert "color:#DAA520'><b>**kargs</b></span>" in format_signature
+
+
 def test_delete(code_editor_bot, mocker):
     """Test CodeEditor.delete()."""
     editor, qtbot = code_editor_bot