From 79d2e4659fb1ec7a0a18db65a73c9769f7444eac Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 13 Oct 2023 11:03:03 -0500 Subject: [PATCH 1/9] Application: Improve update message for Anaconda installations Also, skip an update test in Mac because it's failing too much on CIs. --- spyder/plugins/application/container.py | 15 ++++++++++----- spyder/workers/tests/test_update.py | 3 +++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/spyder/plugins/application/container.py b/spyder/plugins/application/container.py index a8267741c6d..133b3999b65 100644 --- a/spyder/plugins/application/container.py +++ b/spyder/plugins/application/container.py @@ -345,14 +345,19 @@ def _check_updates_ready(self): if os.name == "nt": if is_anaconda(): - msg += _("Run the following commands in the Anaconda " - "prompt to update manually:

") + msg += _("Run the following command or commands in " + "the Anaconda prompt to update manually:" + "

") else: - msg += _("Run the following commands in a cmd prompt " + msg += _("Run the following command in a cmd prompt " "to update manually:

") else: - msg += _("Run the following commands in a terminal to " - "update manually:

") + if is_anaconda(): + msg += _("Run the following command or commands in a " + "terminal to update manually:

") + else: + msg += _("Run the following command in a terminal to " + "update manually:

") if is_anaconda(): channel, __ = get_spyder_conda_channel() diff --git a/spyder/workers/tests/test_update.py b/spyder/workers/tests/test_update.py index 3bcfd326e2a..4dadadfa9a3 100644 --- a/spyder/workers/tests/test_update.py +++ b/spyder/workers/tests/test_update.py @@ -4,6 +4,8 @@ # Licensed under the terms of the MIT License # (see spyder/__init__.py for details) +import sys + import pytest from spyder.workers.updates import WorkerUpdates @@ -48,6 +50,7 @@ def test_updates(qtbot, mocker, is_anaconda, is_pypi, version, assert len(worker.releases) == 1 +@pytest.mark.skipif(sys.platform == 'darwin', reason="Fails frequently on Mac") @pytest.mark.parametrize("version", ["1.0.0", "1000.0.0"]) def test_updates_for_installers(qtbot, mocker, version): """ From 1b7c8caed6fc03074f9daa672e7999abb6735c28 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Fri, 13 Oct 2023 11:03:26 -0500 Subject: [PATCH 2/9] git subrepo pull (merge) external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "70cb79e68a" upstream: origin: "https://github.com/spyder-ide/spyder-kernels.git" branch: "2.x" commit: "70cb79e68a" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo" commit: "2f68596" --- external-deps/spyder-kernels/.gitrepo | 4 +- .../spyder_kernels/utils/dochelpers.py | 81 +++++++++--------- .../utils/tests/test_dochelpers.py | 83 ++++++++++++++++++- 3 files changed, 124 insertions(+), 44 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 12bdc618184..32278fb18c0 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = https://github.com/spyder-ide/spyder-kernels.git branch = 2.x - commit = 072c6cf31a0f438ee4122bfdd14a9cfcec37b69c - parent = 7628e5add6ead8ef3dcbfa3e5794363962ebc968 + commit = 70cb79e68a7e585fd4f97de78b57ba06f892a08e + parent = 3c59ce50ab7487b9559e160cf45c000b5f7a8fa2 method = merge cmdver = 0.4.3 diff --git a/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py b/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py index 336a606c965..426cb9dc981 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py +++ b/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py @@ -120,7 +120,15 @@ def getdoc(obj): args, varargs, varkw, defaults, formatvalue=lambda o:'='+repr(o)) else: - sig = inspect.signature(obj) + # This is necessary to catch errors for objects without a + # signature, like numpy.where. + # Fixes spyder-ide/spyder#21148 + try: + sig = inspect.signature(obj) + except ValueError: + sig = getargspecfromtext(doc['docstring']) + if not sig: + sig = '(...)' doc['argspec'] = str(sig) if name == '': doc['name'] = name + ' lambda ' @@ -171,40 +179,50 @@ def getsource(obj): def getsignaturefromtext(text, objname): - """Get object signatures from text (object documentation) - Return a list containing a single string in most cases - Example of multiple signatures: PyQt5 objects""" + """Get object signature from text (i.e. object documentation).""" if isinstance(text, dict): text = text.get('docstring', '') + # Regexps - oneline_re = objname + r'\([^\)].+?(?<=[\w\]\}\'"])\)(?!,)' - multiline_re = objname + r'\([^\)]+(?<=[\w\]\}\'"])\)(?!,)' - multiline_end_parenleft_re = r'(%s\([^\)]+(\),\n.+)+(?<=[\w\]\}\'"])\))' + args_re = r'(\(.+?\))' + if objname: + signature_re = objname + args_re + else: + identifier_re = r'(\w+)' + signature_re = identifier_re + args_re + # Grabbing signatures if not text: text = '' - sigs_1 = re.findall(oneline_re + '|' + multiline_re, text) - sigs_2 = [g[0] for g in re.findall(multiline_end_parenleft_re % objname, text)] - all_sigs = sigs_1 + sigs_2 + + sigs = re.findall(signature_re, text) + # The most relevant signature is usually the first one. There could be - # others in doctests but those are not so important - if all_sigs: - return all_sigs[0] - else: - return '' + # others in doctests or other places, but those are not so important. + sig = '' + if sigs: + if PY2: + # We don't have an easy way to check if the identifier detected by + # signature_re is a valid one in Python 2. So, we simply select the + # first match. + sig = sigs[0] if objname else sigs[0][1] + else: + if objname: + sig = sigs[0] + else: + valid_sigs = [s for s in sigs if s[0].isidentifier()] + if valid_sigs: + sig = valid_sigs[0][1] -# Fix for Issue 1953 -# TODO: Add more signatures and remove this hack in 2.4 -getsignaturesfromtext = getsignaturefromtext + return sig def getargspecfromtext(text): """ Try to get the formatted argspec of a callable from the first block of its - docstring + docstring. - This will return something like - '(foo, bar, k=1)' + This will return something like `(x, y, k=1)`. """ blocks = text.split("\n\n") first_block = blocks[0].strip() @@ -212,10 +230,10 @@ def getargspecfromtext(text): def getargsfromtext(text, objname): - """Get arguments from text (object documentation)""" + """Get arguments from text (object documentation).""" signature = getsignaturefromtext(text, objname) if signature: - argtxt = signature[signature.find('(')+1:-1] + argtxt = signature[signature.find('(') + 1:-1] return argtxt.split(',') @@ -330,20 +348,3 @@ def isdefined(obj, force_import=False, namespace=None): return False base += '.'+attr return True - - -if __name__ == "__main__": - class Test(object): - def method(self, x, y=2): - pass - print(getargtxt(Test.__init__)) # spyder: test-skip - print(getargtxt(Test.method)) # spyder: test-skip - print(isdefined('numpy.take', force_import=True)) # spyder: test-skip - print(isdefined('__import__')) # spyder: test-skip - print(isdefined('.keys', force_import=True)) # spyder: test-skip - print(getobj('globals')) # spyder: test-skip - print(getobj('globals().keys')) # spyder: test-skip - print(getobj('+scipy.signal.')) # spyder: test-skip - print(getobj('4.')) # spyder: test-skip - print(getdoc(sorted)) # spyder: test-skip - print(getargtxt(sorted)) # spyder: test-skip diff --git a/external-deps/spyder-kernels/spyder_kernels/utils/tests/test_dochelpers.py b/external-deps/spyder-kernels/spyder_kernels/utils/tests/test_dochelpers.py index 220db9e2887..f3fa957f4cd 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/tests/test_dochelpers.py +++ b/external-deps/spyder-kernels/spyder_kernels/utils/tests/test_dochelpers.py @@ -18,8 +18,9 @@ import pytest # Local imports -from spyder_kernels.utils.dochelpers import (getargtxt, getdoc, getobj, - isdefined) +from spyder_kernels.utils.dochelpers import ( + getargtxt, getargspecfromtext, getdoc, getobj, getsignaturefromtext, + isdefined) from spyder_kernels.py3compat import PY2 @@ -60,5 +61,83 @@ def test_dochelpers(): assert getobj('4.') == '4' +@pytest.mark.skipif(PY2, reason="Fails in Python 2") +def test_no_signature(): + """ + Test that we can get documentation for objects for which Python can't get a + signature directly because it gives an error. + + This is a regression test for issue spyder-ide/spyder#21148 + """ + import numpy as np + doc = getdoc(np.where) + signature = doc['argspec'] + assert signature and signature != "(...)" and signature.startswith("(") + assert doc['docstring'] + + +@pytest.mark.parametrize( + 'text, name, expected', + [ + ('foo(x, y)', 'foo', '(x, y)'), + ('foo(x, y)', '', '(x, y)'), + ] +) +def test_getsignaturefromtext_py2(text, name, expected): + assert getsignaturefromtext(text, name) == expected + + +@pytest.mark.skipif(PY2, reason="Don't work in Python 2") +@pytest.mark.parametrize( + 'text, name, expected', + [ + # Simple text with and without name + ('foo(x, y)', 'foo', '(x, y)'), + ('foo(x, y)', '', '(x, y)'), + # Single arg + ('foo(x)', '', '(x)'), + ('foo(x = {})', '', '(x = {})'), + # Not a valid identifier + ('1a(x, y)', '', ''), + # Valid identifier + ('a1(x, y=2)', '', '(x, y=2)'), + # Unicode identifier with and without name + ('ΣΔ(x, y)', 'ΣΔ', '(x, y)'), + ('ΣΔ(x, y)', '', '(x, y)'), + # Multiple signatures in a single line + ('ΣΔ(x, y) foo(a, b)', '', '(x, y)'), + ('1a(x, y) foo(a, b)', '', '(a, b)'), + # Multiple signatures in multiple lines + ('foo(a, b = 1)\n\nΣΔ(x, y=2)', '', '(a, b = 1)'), + ('1a(a, b = 1)\n\nΣΔ(x, y=2)', '', '(x, y=2)'), + # Signature after math operations + ('2(3 + 5) 3*(99) ΣΔ(x, y)', '', '(x, y)'), + # No identifier + ('(x, y)', '', ''), + ('foo (a=1, b = 2)', '', ''), + # Empty signature + ('foo()', '', ''), + ('foo()', 'foo', ''), + ] +) +def test_getsignaturefromtext(text, name, expected): + assert getsignaturefromtext(text, name) == expected + + +def test_multisignature(): + """ + Test that we can get at least one signature from an object with multiple + ones declared in its docstring. + """ + def foo(): + """ + foo(x, y) foo(a, b) + foo(c, d) + """ + + signature = getargspecfromtext(foo.__doc__) + assert signature == "(x, y)" + + if __name__ == "__main__": pytest.main() From f5df00726fc14e3e3d8070eb283bc808fd045973 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 15 Oct 2023 12:39:47 -0500 Subject: [PATCH 3/9] IPython console: Make some code style improvements Also, remove unnecessary import. --- spyder/plugins/ipythonconsole/widgets/help.py | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/spyder/plugins/ipythonconsole/widgets/help.py b/spyder/plugins/ipythonconsole/widgets/help.py index 60e03bbad7f..17a550b9cc0 100644 --- a/spyder/plugins/ipythonconsole/widgets/help.py +++ b/spyder/plugins/ipythonconsole/widgets/help.py @@ -16,7 +16,6 @@ from pickle import UnpicklingError from qtconsole.ansi_code_processor import ANSI_OR_SPECIAL_PATTERN, ANSI_PATTERN from qtconsole.rich_jupyter_widget import RichJupyterWidget -from qtpy.QtCore import QEventLoop # Local imports from spyder_kernels.utils.dochelpers import (getargspecfromtext, @@ -42,23 +41,29 @@ def get_documentation(self, content): """Get documentation from inspect reply content.""" data = content.get('data', {}) text = data.get('text/plain', '') + if text: - if (self.language_name is not None - and self.language_name == 'python'): + if ( + self.language_name is not None + and self.language_name == 'python' + ): text = re.compile(ANSI_PATTERN).sub('', text) signature = self.get_signature(content).split('(')[-1] # Base value for the documentation - documentation = (text.split('Docstring:')[-1]. - split('Type:')[0].split('File:')[0]) + documentation = ( + text.split('Docstring:')[-1].split('Type:')[0]. + split('File:')[0] + ) if signature: # Check if the signature is in the Docstring doc_from_signature = documentation.split(signature) if len(doc_from_signature) > 1: - return (doc_from_signature[-1].split('Docstring:')[-1]. - split('Type:')[0]. - split('File:')[0]).strip('\r\n') + return ( + doc_from_signature[-1].split('Docstring:')[-1]. + split('Type:')[0].split('File:')[0] + ).strip('\r\n') return documentation.strip('\r\n') else: @@ -71,6 +76,7 @@ def _get_signature(self, name, text): """Get signature from text using a given function name.""" signature = '' argspec = getargspecfromtext(text) + if argspec: # This covers cases like np.abs, whose docstring is # the same as np.absolute and because of that a proper @@ -78,15 +84,19 @@ def _get_signature(self, name, text): signature = name + argspec else: signature = getsignaturefromtext(text, name) + return signature def get_signature(self, content): """Get signature from inspect reply content""" data = content.get('data', {}) text = data.get('text/plain', '') + if text: - if (self.language_name is not None - and self.language_name == 'python'): + if ( + self.language_name is not None + and self.language_name == 'python' + ): self._control.current_prompt_pos = self._prompt_pos line = self._control.get_current_line_to_cursor() name = line[:-1].split('(')[-1] # Take last token after a ( @@ -156,14 +166,19 @@ def _handle_inspect_reply(self, rep): """ cursor = self._get_cursor() info = self._request_info.get('call_tip') - if (info and info.id == rep['parent_header']['msg_id'] and - info.pos == cursor.position()): + + if ( + info + and info.id == rep['parent_header']['msg_id'] + and info.pos == cursor.position() + ): content = rep['content'] if content.get('status') == 'ok' and content.get('found', False): signature = self.get_signature(content) documentation = self.get_documentation(content) new_line = (self.language_name is not None and self.language_name == 'python') + self._control.show_calltip( signature, documentation=documentation, From d2ce691c57233e197f23c635cffe466ef5c01ca0 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 15 Oct 2023 13:11:30 -0500 Subject: [PATCH 4/9] IPython console: Improve how we get signatures from inspect replies --- spyder/plugins/ipythonconsole/widgets/help.py | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/spyder/plugins/ipythonconsole/widgets/help.py b/spyder/plugins/ipythonconsole/widgets/help.py index 17a550b9cc0..f5e6a1b7aa8 100644 --- a/spyder/plugins/ipythonconsole/widgets/help.py +++ b/spyder/plugins/ipythonconsole/widgets/help.py @@ -14,7 +14,7 @@ # Third party imports from pickle import UnpicklingError -from qtconsole.ansi_code_processor import ANSI_OR_SPECIAL_PATTERN, ANSI_PATTERN +from qtconsole.ansi_code_processor import ANSI_PATTERN from qtconsole.rich_jupyter_widget import RichJupyterWidget # Local imports @@ -57,7 +57,7 @@ def get_documentation(self, content): ) if signature: - # Check if the signature is in the Docstring + # Check if the signature is in the docstring doc_from_signature = documentation.split(signature) if len(doc_from_signature) > 1: return ( @@ -83,7 +83,7 @@ def _get_signature(self, name, text): # signature can't be obtained correctly signature = name + argspec else: - signature = getsignaturefromtext(text, name) + signature = name + getsignaturefromtext(text, name) return signature @@ -93,11 +93,17 @@ def get_signature(self, content): text = data.get('text/plain', '') if text: + # Remove ANSI characters from text + text = re.compile(ANSI_PATTERN).sub('', text) + if ( self.language_name is not None and self.language_name == 'python' ): + signature = '' self._control.current_prompt_pos = self._prompt_pos + + # Get object's name line = self._control.get_current_line_to_cursor() name = line[:-1].split('(')[-1] # Take last token after a ( name = name.split('.')[-1] # Then take last token after a . @@ -108,29 +114,37 @@ def get_signature(self, content): except Exception: pass - text = text.split('Docstring:') - - # Try signature from text before 'Docstring:' - before_text = text[0] - before_signature = self._get_signature(name, before_text) - - # Try signature from text after 'Docstring:' - after_text = text[-1] - after_signature = self._get_signature(name, after_text) - - # Stay with the longest signature - if len(before_signature) > len(after_signature): - signature = before_signature + # Split between docstring and text before it + if 'Docstring:' in text: + before_text, after_text = text.split('Docstring:') else: - signature = after_signature - - # Prevent special characters. Applied here to ensure - # recognizing the signature in the logic above. - signature = ANSI_OR_SPECIAL_PATTERN.sub('', signature) + before_text, after_text = '', text + + if before_text: + # This is the case for objects for which IPython was able + # to get a signature (e.g. np.vectorize) + before_text = before_text.strip().replace('\n', '') + signature = self._get_signature(name, before_text) + + # Default signatures returned by IPython + default_sigs = [ + name + '(*args, **kwargs)', + name + '(self, /, *args, **kwargs)' + ] + + # This is the case for objects without signature (e.g. + # np.where). For them, we try to find it from their docstrings. + if not signature or signature in default_sigs: + after_signature = self._get_signature( + name, after_text.strip() + ) + if after_signature: + signature = after_signature + + signature = signature.replace('\n', '') return signature.strip('\r\n') else: - text = re.compile(ANSI_PATTERN).sub('', text) return text.strip('\r\n') else: return '' From e701b7a5ce486640e8fc5a208ebab71157918452 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 15 Oct 2023 13:13:19 -0500 Subject: [PATCH 5/9] IPython console: Avoid getting signature twice in inspect replies Also, fix removing signature from first line of docstring if present. --- spyder/plugins/ipythonconsole/widgets/help.py | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/spyder/plugins/ipythonconsole/widgets/help.py b/spyder/plugins/ipythonconsole/widgets/help.py index f5e6a1b7aa8..d1894951521 100644 --- a/spyder/plugins/ipythonconsole/widgets/help.py +++ b/spyder/plugins/ipythonconsole/widgets/help.py @@ -37,37 +37,39 @@ def clean_invalid_var_chars(self, var): """ return re.sub(r'\W|^(?=\d)', '_', var) - def get_documentation(self, content): + def get_documentation(self, content, signature): """Get documentation from inspect reply content.""" data = content.get('data', {}) text = data.get('text/plain', '') if text: + # Remove ANSI characters from text + text = re.compile(ANSI_PATTERN).sub('', text) + if ( self.language_name is not None and self.language_name == 'python' ): - text = re.compile(ANSI_PATTERN).sub('', text) - signature = self.get_signature(content).split('(')[-1] - # Base value for the documentation documentation = ( - text.split('Docstring:')[-1].split('Type:')[0]. + text.split('Docstring:')[-1]. + split('Type:')[0]. split('File:')[0] - ) + ).strip() + # Check if the signature is at the beginning of the docstring + # to remove it if signature: - # Check if the signature is in the docstring - doc_from_signature = documentation.split(signature) - if len(doc_from_signature) > 1: - return ( - doc_from_signature[-1].split('Docstring:')[-1]. - split('Type:')[0].split('File:')[0] - ).strip('\r\n') + signature_and_doc = documentation.split("\n\n") + + if ( + len(signature_and_doc) > 1 + and signature_and_doc[0].replace('\n', '') == signature + ): + return "\n\n".join(signature_and_doc[1:]).strip('\r\n') return documentation.strip('\r\n') else: - text = re.compile(ANSI_PATTERN).sub('', text) return text.strip('\r\n') else: return '' @@ -189,7 +191,7 @@ def _handle_inspect_reply(self, rep): content = rep['content'] if content.get('status') == 'ok' and content.get('found', False): signature = self.get_signature(content) - documentation = self.get_documentation(content) + documentation = self.get_documentation(content, signature) new_line = (self.language_name is not None and self.language_name == 'python') From bb2fc9a025927245ce00f2f5f2296bfa76585505 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sun, 15 Oct 2023 13:27:11 -0500 Subject: [PATCH 6/9] Testing: Expand test_get_calltips to include more cases --- .../tests/test_ipythonconsole.py | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py index 811e3f7bfc9..40481fc50d0 100644 --- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py +++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py @@ -77,21 +77,34 @@ def test_banners(ipyconsole, qtbot): @flaky(max_runs=3) @pytest.mark.parametrize( - "function,signature,documentation", - [("arange", + "function, signature, documentation", + [("np.arange", # Check we get the signature from the object's docstring ["start", "stop"], ["Return evenly spaced values within a given interval.
", "open interval ..."]), - ("vectorize", + ("np.vectorize", # Numpy function with a proper signature ["pyfunc", "otype", "signature"], ["Returns an object that acts like pyfunc, but takes arrays as
input." "
", "Define a vectorized function which takes a nested sequence ..."]), - ("absolute", + ("np.abs", # np.abs has the same signature as np.absolute ["x", "/", "out"], - ["Parameters
", "x : array_like ..."])] - ) -@pytest.mark.skipif(not os.name == 'nt', + ["Calculate the absolute value"]), + ("np.where", # Python gives an error when getting its signature + ["condition", "/"], + ["Return elements chosen from `x`"]), + ("range", # Check we display the first signature among several + ["stop"], + ["range(stop) -> range object"]), + ("dict", # Check we skip an empty signature + ["mapping"], + ["dict() -> new empty dictionary"]), + ("foo", # Check we display the right tooltip for interactive objects + ["x", "y"], + ["My function"]) + ] +) +@pytest.mark.skipif(running_in_ci() and not os.name == 'nt', reason="Times out on macOS and fails on Linux") @pytest.mark.skipif(parse(np.__version__) < parse('1.25.0'), reason="Documentation for np.vectorize is different") @@ -104,10 +117,22 @@ def test_get_calltips(ipyconsole, qtbot, function, signature, documentation): with qtbot.waitSignal(shell.executed): shell.execute('import numpy as np') + if function == "foo": + with qtbot.waitSignal(shell.executed): + code = dedent(''' + def foo(x, y): + """ + My function + """ + return x + y + ''') + + shell.execute(code) + # Write an object in the console that should generate a calltip # and wait for the kernel to send its response. with qtbot.waitSignal(shell.kernel_client.shell_channel.message_received): - qtbot.keyClicks(control, 'np.' + function + '(') + qtbot.keyClicks(control, function + '(') # Wait a little bit for the calltip to appear qtbot.waitUntil(lambda: control.calltip_widget.isVisible()) From 7b04f60df5644cba06761da6fc25f334ef3ba24a Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 17 Oct 2023 11:30:32 -0500 Subject: [PATCH 7/9] git subrepo pull --remote=https://github.com/ccordoba12/spyder-kernels.git --branch=sig-improvements --update --force external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "cd347998c8" upstream: origin: "https://github.com/ccordoba12/spyder-kernels.git" branch: "sig-improvements" commit: "cd347998c8" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo" commit: "2f68596" --- external-deps/spyder-kernels/.gitrepo | 8 ++--- .../spyder_kernels/utils/dochelpers.py | 32 +++++++++++++++++-- .../utils/tests/test_dochelpers.py | 17 ++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 32278fb18c0..17cc17dc1de 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -4,9 +4,9 @@ ; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme ; [subrepo] - remote = https://github.com/spyder-ide/spyder-kernels.git - branch = 2.x - commit = 70cb79e68a7e585fd4f97de78b57ba06f892a08e - parent = 3c59ce50ab7487b9559e160cf45c000b5f7a8fa2 + remote = https://github.com/ccordoba12/spyder-kernels.git + branch = sig-improvements + commit = cd347998c82e98ed68b47b71c34849d7fb090dc7 + parent = bb2fc9a025927245ce00f2f5f2296bfa76585505 method = merge cmdver = 0.4.3 diff --git a/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py b/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py index 426cb9dc981..83db1a43e00 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py +++ b/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py @@ -207,12 +207,38 @@ def getsignaturefromtext(text, objname): # first match. sig = sigs[0] if objname else sigs[0][1] else: + # Default signatures returned by IPython. + # Notes: + # * These are not real signatures but only used to provide a + # placeholder. + # * We skip them if we can find other signatures in `text`. + # * This is necessary because we also use this function in Spyder + # to parse the content of inspect replies that come from the + # kernel, which can include these signatures. + default_ipy_sigs = [ + '(*args, **kwargs)', + '(self, /, *args, **kwargs)' + ] + if objname: - sig = sigs[0] + real_sigs = [s for s in sigs if s not in default_ipy_sigs] + + if real_sigs: + sig = real_sigs[0] + else: + sig = sigs[0] else: valid_sigs = [s for s in sigs if s[0].isidentifier()] + if valid_sigs: - sig = valid_sigs[0][1] + real_sigs = [ + s for s in valid_sigs if s[1] not in default_ipy_sigs + ] + + if real_sigs: + sig = real_sigs[0][1] + else: + sig = valid_sigs[0][1] return sig @@ -225,7 +251,7 @@ def getargspecfromtext(text): This will return something like `(x, y, k=1)`. """ blocks = text.split("\n\n") - first_block = blocks[0].strip() + first_block = blocks[0].strip().replace('\n', '') return getsignaturefromtext(first_block, '') diff --git a/external-deps/spyder-kernels/spyder_kernels/utils/tests/test_dochelpers.py b/external-deps/spyder-kernels/spyder_kernels/utils/tests/test_dochelpers.py index f3fa957f4cd..f67d730fbe6 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/tests/test_dochelpers.py +++ b/external-deps/spyder-kernels/spyder_kernels/utils/tests/test_dochelpers.py @@ -139,5 +139,22 @@ def foo(): assert signature == "(x, y)" +def test_multiline_signature(): + """ + Test that we can get signatures splitted into multiple lines in a + docstring. + """ + def foo(): + """ + foo(x, + y) + + This is a docstring. + """ + + signature = getargspecfromtext(foo.__doc__) + assert signature.startswith("(x, ") + + if __name__ == "__main__": pytest.main() From efdd32e8f4534384b27c271b927b3e083e25f3d3 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 17 Oct 2023 11:41:01 -0500 Subject: [PATCH 8/9] Testing: Expand test_get_calltips with even more cases --- spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py index 40481fc50d0..bc95dad37dc 100644 --- a/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py +++ b/spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py @@ -93,6 +93,12 @@ def test_banners(ipyconsole, qtbot): ("np.where", # Python gives an error when getting its signature ["condition", "/"], ["Return elements chosen from `x`"]), + ("np.array", # Signature is splitted into several lines + ["object", "dtype=None"], + ["Create an array.

", "Parameters"]), + ("np.linalg.norm", # Includes IPython default signature in inspect reply + ["x", "ord=None"], + ["Matrix or vector norm"]), ("range", # Check we display the first signature among several ["stop"], ["range(stop) -> range object"]), From f9cb3d90ef258d1152048a24c58f641146957118 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 17 Oct 2023 23:29:50 -0500 Subject: [PATCH 9/9] git subrepo pull (merge) --remote=https://github.com/spyder-ide/spyder-kernels.git --branch=2.x --update --force external-deps/spyder-kernels subrepo: subdir: "external-deps/spyder-kernels" merged: "82eea9f77a" upstream: origin: "https://github.com/spyder-ide/spyder-kernels.git" branch: "2.x" commit: "82eea9f77a" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo" commit: "2f68596" --- external-deps/spyder-kernels/.gitrepo | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 17cc17dc1de..325ddedfbfc 100644 --- a/external-deps/spyder-kernels/.gitrepo +++ b/external-deps/spyder-kernels/.gitrepo @@ -4,9 +4,9 @@ ; git-subrepo command. See https://github.com/git-commands/git-subrepo#readme ; [subrepo] - remote = https://github.com/ccordoba12/spyder-kernels.git - branch = sig-improvements - commit = cd347998c82e98ed68b47b71c34849d7fb090dc7 - parent = bb2fc9a025927245ce00f2f5f2296bfa76585505 + remote = https://github.com/spyder-ide/spyder-kernels.git + branch = 2.x + commit = 82eea9f77ac3cd3b7c6bdbd782de3dfb03a71d94 + parent = efdd32e8f4534384b27c271b927b3e083e25f3d3 method = merge cmdver = 0.4.3