diff --git a/external-deps/spyder-kernels/.gitrepo b/external-deps/spyder-kernels/.gitrepo index 03c67691624..f284d151b41 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 = master - commit = 37a97e5ea326c5e80d4a292944571ffbc87dcad3 - parent = 52c88bf82c25e059913ae85e8a51947c170b5c4a + commit = 5ea26d6f2bd6c1d79be49b49103f2c629253f276 + parent = 0328ff5e22a6541096d2c75f4b2c822328a1f38a 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 924779926a0..2aace3afa97 100644 --- a/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py +++ b/external-deps/spyder-kernels/spyder_kernels/utils/dochelpers.py @@ -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 == '': doc['name'] = name + ' lambda ' @@ -158,51 +166,78 @@ 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: + # 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: + 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: + real_sigs = [ + s for s in valid_sigs if s[1] not in default_ipy_sigs + ] -# Fix for Issue 1953 -# TODO: Add more signatures and remove this hack in 2.4 -getsignaturesfromtext = getsignaturefromtext + if real_sigs: + sig = real_sigs[0][1] + else: + sig = valid_sigs[0][1] + + 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() + first_block = blocks[0].strip().replace('\n', '') 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(',') @@ -317,20 +352,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 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 90744879480..05197a0ab01 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) class Test(object): @@ -58,5 +59,87 @@ 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)" + + +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()