Skip to content

Commit

Permalink
Merge from 2.x: PR #473
Browse files Browse the repository at this point in the history
  • Loading branch information
ccordoba12 committed Oct 13, 2023
2 parents 37a97e5 + 70cb79e commit 105e396
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 42 deletions.
75 changes: 35 additions & 40 deletions spyder_kernels/utils/dochelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,15 @@ def getdoc(obj):
doc['note'] = 'Function'
doc['name'] = obj.__name__
if inspect.isfunction(obj):
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 == '<lambda>':
doc['name'] = name + ' lambda '
Expand Down Expand Up @@ -158,51 +166,55 @@ 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 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()
return getsignaturefromtext(first_block, '')


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(',')


Expand Down Expand Up @@ -317,20 +329,3 @@ def isdefined(obj, force_import=False, namespace=None):
return False
base += '.'+attr
return True


if __name__ == "__main__":
class Test:
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
70 changes: 68 additions & 2 deletions spyder_kernels/utils/tests/test_dochelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


class Test(object):
Expand Down Expand Up @@ -58,5 +59,70 @@ def test_dochelpers():
assert getobj('4.') == '4'


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',
[
# 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()

0 comments on commit 105e396

Please sign in to comment.