Skip to content

Commit

Permalink
Merge pull request #473 from ccordoba12/catch-signature-error
Browse files Browse the repository at this point in the history
PR: Catch error when trying to get the signature of some objects and improve getting signatures from docstrings
  • Loading branch information
ccordoba12 authored Oct 13, 2023
2 parents 072c6cf + 59a1a08 commit 70cb79e
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 42 deletions.
81 changes: 41 additions & 40 deletions spyder_kernels/utils/dochelpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 == '<lambda>':
doc['name'] = name + ' lambda '
Expand Down Expand Up @@ -171,51 +179,61 @@ 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()
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 @@ -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
83 changes: 81 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)
from spyder_kernels.py3compat import PY2


Expand Down Expand Up @@ -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()

0 comments on commit 70cb79e

Please sign in to comment.