Skip to content

Commit

Permalink
Merge from 5.x: PR #21426
Browse files Browse the repository at this point in the history
Fixes #21148
  • Loading branch information
ccordoba12 committed Oct 18, 2023
2 parents 14ff937 + ba6f83a commit 0328ff5
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 55 deletions.
15 changes: 10 additions & 5 deletions spyder/plugins/application/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,14 +342,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:<br><br>")
msg += _("Run the following command or commands in "
"the Anaconda prompt to update manually:"
"<br><br>")
else:
msg += _("Run the following commands in a cmd prompt "
msg += _("Run the following command in a cmd prompt "
"to update manually:<br><br>")
else:
msg += _("Run the following commands in a terminal to "
"update manually:<br><br>")
if is_anaconda():
msg += _("Run the following command or commands in a "
"terminal to update manually:<br><br>")
else:
msg += _("Run the following command in a terminal to "
"update manually:<br><br>")

if is_anaconda():
channel, __ = get_spyder_conda_channel()
Expand Down
47 changes: 39 additions & 8 deletions spyder/plugins/ipythonconsole/tests/test_ipythonconsole.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,21 +74,40 @@ 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.<br>",
"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<br>input."
"<br>",
"Define a vectorized function which takes a nested sequence ..."]),
("absolute",
("np.abs", # np.abs has the same signature as np.absolute
["x", "/", "out"],
["Parameters<br>", "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`"]),
("np.array", # Signature is splitted into several lines
["object", "dtype=None"],
["Create an array.<br><br>", "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"]),
("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")
Expand All @@ -101,10 +120,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())
Expand Down
115 changes: 73 additions & 42 deletions spyder/plugins/ipythonconsole/widgets/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@

# 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
from qtpy.QtCore import QEventLoop

# Local imports
from spyder_kernels.utils.dochelpers import (getargspecfromtext,
Expand All @@ -38,31 +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:
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]
# Remove ANSI characters from text
text = re.compile(ANSI_PATTERN).sub('', text)

if (
self.language_name is not None
and self.language_name == 'python'
):
# 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]
).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 ''
Expand All @@ -71,23 +78,34 @@ 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
# signature can't be obtained correctly
signature = name + argspec
else:
signature = getsignaturefromtext(text, name)
signature = name + 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'):
# 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 .
Expand All @@ -98,29 +116,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 ''
Expand Down Expand Up @@ -159,14 +185,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)
documentation = self.get_documentation(content, signature)
new_line = (self.language_name is not None
and self.language_name == 'python')

self._control.show_calltip(
signature,
documentation=documentation,
Expand Down
3 changes: 3 additions & 0 deletions spyder/workers/tests/test_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down

0 comments on commit 0328ff5

Please sign in to comment.