Subject: bug#45348: 27.1; python-eldoc-setup-code uses obsolete python function

From: Tomas Nordin <[email-masked]>
Date: Thu, 24 Dec 2020 00:53:38 +0100

Steven De Herdt <[email-masked]> writes:

> Hello folks
>
> This report concerns eldoc for Python.  I'm using Debian testing, with
> Python 3.9, in my daily config I use ipython3 as
> python-shell-interpreter.  Every time I place point in the argument list
> of a Python function without documentation, the minibuffer expands to
> show a warning ("DeprecationWarning: `formatargspec` is deprecated since
> Python 3.5. Use `signature` and the `Signature` object directly"), an
> unrelated snippet of my code, as well as the intended function signature.
>
> This behaviour can be partly reproduced in a minimal setting with the
> attached func.py:
> * emacs -Q func.py
> * C-u C-c C-p , choose "python3 -i"
> * in buffer for func.py: C-c C-c
> * place point at argument (13) of the call to func
> The DeprecationWarning only shows the first time with python3.  My
> ipython3 shows it every time however, that's a bit annoying.  I'm not
> sure the repeated warning is an issue with ipython, but the eldoc setup
> code would need to upgrade from deprecated functionality anyway.

I can reproduce this with Python 3.7.3 on Debian stable. And yes,
strange behaviour with Ipython, which is maybe not about the eldoc setup
code.

> Maybe I'll try to make python-eldoc-setup-code use inspect.signature
if
> it annoys me too much. :)

Yes, did you figure out something safe and sound? Otherwise can you
please try the attached patch. It fixes the problem here.

The patch touch the string of python code `python-eldoc-setup-code' and
do a little more than simply not call formatargspec with python 3, but
does not change the intended effect.

I think the patch improves readability somewhat. But also I identified a
few bugs with the python code in the string. Those bugs doesn't surface
due to the (needed) wrapping try block. Here are the observed bugs,

- if obj is not a string, if isinstance(obj, str_type) fails and doc
  does not get defined. After this, doc is referred to in the next
  test, (if not doc and callable(obj):) which will then error.

- if obj is a class instance and has an empty docstring and is not
  callable (often not), then (if not doc and callable(obj)) fails and
  doc.splitlines()[0] will Error.

- again, if obj is a class instance, not callable but has no
  docstring, doc will be None and doc.splitlines() will error.

- if obj is provided as the object itself, the function always fail
  because doc does not get defined.

Those bugs are fixed with the patch, as well as using inspect.signature
with python 3 as recommended by the deprecation warning.

Besides the patch, I wonder what other users thinks about the resulting
behaviour of this python function. Here is a a suitable docstring for
`python-eldoc-setup-code', would it be provided:

"""Provide a docstring or signature on obj.

Prefer a docstring, and fall back on the signature if no docstring
is found."""

Meaning, if there is a docstring, we will not get the call signature,
we get that only when there is no docstring. Personally I would prefer
the call signature always, like in elisp. Then perhaps when doing C-c
C-f (python-eldoc-at-point) show the (full) docstring. But that would be
another bug-report I guess.

Best regards
--
Tomas

diff --git a/lisp/progmodes/python.el b/lisp/progmodes/python.el
index d58b32f3c3..6acfafdb68 100644
--- a/lisp/progmodes/python.el
+++ b/lisp/progmodes/python.el
@@ -4533,33 +4533,35 @@ python-eldoc-setup-code
   "def __PYDOC_get_help(obj):
     try:
         import inspect
+        from sys import version_info as version_info
+
         try:
-            str_type = basestring
-            argspec_function = inspect.getargspec
-        except NameError:
-            str_type = str
-            argspec_function = inspect.getfullargspec
-        if isinstance(obj, str_type):
             obj = eval(obj, globals())
-        doc = inspect.getdoc(obj)
+            doc = inspect.getdoc(obj)
+        except TypeError:
+            doc = inspect.getdoc(obj)
+
         if not doc and callable(obj):
-            target = None
             if inspect.isclass(obj) and hasattr(obj, '__init__'):
                 target = obj.__init__
                 objtype = 'class'
             else:
                 target = obj
                 objtype = 'def'
-            if target:
-                args = inspect.formatargspec(*argspec_function(target))
-                name = obj.__name__
-                doc = '{objtype} {name}{args}'.format(
-                    objtype=objtype, name=name, args=args
-                )
-        else:
+            if version_info.major == 3:
+                args = str(inspect.signature(target))
+            else:
+                args = inspect.formatargspec(*inspect.getargspec(target))
+
+            name = obj.__name__
+            doc = '{objtype} {name}{args}'.format(
+                objtype=objtype, name=name, args=args)
+        elif doc:
             doc = doc.splitlines()[0]
+        else:
+            doc = ''
     except:
-        doc = ''
+        return ''
     return doc"
   "Python code to setup documentation retrieval."
   :type 'string