Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python introspection: get the argument list of a method_descriptor?

Tags:

A code illustration as an intro to my questions:

import re, inspect, datetime

inspect.getargspec (re.findall)
# =>
# ArgSpec(args = ['pattern', 'string', 'flags'], varargs=None,
# keywords=None, defaults = (0,))

type (datetime.datetime.replace)
# => <type 'method_descriptor'>

inspect.getargspec (datetime.datetime.replace)
# => Traceback (most recent call last):
#      File "<stdin>", line 1, in <module>
#      File "/usr/lib/python2.7/inspect.py", line 816, in getargspec
#        raise TypeError('{!r} is not a Python function'.format(func))
# TypeError: <method 'replace' of 'datetime.datetime' objects> is
# not a Python function

It seems that the only way for me to find the signature of datetime.datetime.replace while I code is to look it up in the doc: date.replace(year, month, day).

The only introspection part that seems to work:

datetime.datetime.replace.__doc__
# => 'Return datetime with new specified fields.'

I've examined how the Jupyter function arglist tool-tip works, they have the exact same problem, i.e. no arglist available for datetime.datetime.replace.

So here are the questions:

  1. Is it still possible to get the argument list somehow? Maybe I could install the C sources for datetime and connect them via the __file__ attribute?

  2. Is it possible to annotate a <type 'method_descriptor'> with the arglist information? In that case, I could parse the linked doc's markdown definition and automatically annotate the built-in module functions.

like image 896
abo-abo Avatar asked Feb 09 '17 11:02

abo-abo


1 Answers

No, you can't get more information; installing the C sources would not give you easy access to the same. That's because most methods defined in C code do not actually expose this information; you'd have to parse out a rather cryptic piece of C code:

if (! PyArg_ParseTupleAndKeywords(args, kw, "|iiiiiiiO$i:replace",
                                  datetime_kws,
                                  &y, &m, &d, &hh, &mm, &ss, &us,
                                  &tzinfo, &fold))

The re.findall() function is a pure Python function, so is introspectable.

I said most methods defined in C, because as of Python 3.4 and up, methods that use the new Argument Clinic preprocessor will include a new __text_signature__ attribute, which the internal inspect._signature_fromstr() function can parse. This means that even for such C-defined methods, you can introspect the arguments:

>>> import io
>>> import inspect
>>> type(io.BytesIO.read)
<class 'method_descriptor'>
>>> inspect.signature(io.BytesIO.read)
<Signature (self, size=None, /)>

Also see What are __signature__ and __text_signature__ used for in Python 3.4

The datetime module has not yet received much Argument Clinic love. We'll have to be patient, or if you really care a lot about this, supply patches that convert the module to using Argument Clinic.

If you want to see what modules do have support already, look at the Modules/clinic subdirectory which contains the generated clinic output; for the datetime module, only datetime.datetime.now() is currently included. That method defines a clinic block:

/*[clinic input]
@classmethod
datetime.datetime.now
    tz: object = None
        Timezone object.
Returns new datetime object representing current time local to tz.
If no tz is specified, uses local timezone.
[clinic start generated code]*/

static PyObject *
datetime_datetime_now_impl(PyTypeObject *type, PyObject *tz)
/*[clinic end generated code: output=b3386e5345e2b47a input=80d09869c5267d00]*/

making the method introspectable:

>>> import datetime
>>> inspect.signature(datetime.datetime.now)
<Signature (tz=None)>

There is no way to directly attach information to those C functions and methods that are not introspectable; they don't support attributes either.

Most autocomplete solutions that want to support such objects use separate data structures where the information is maintained independently (with all the inherent risks of the data getting out of sync). Some of these are available for your own purposes:

  • The Komodo IDE code intelligence library (open source, used other editors too) uses the CIX format to encode this data; you could download the Python 3 catalog. Unfortunately for your specific example, the datetime.replace() function signature has not been fleshed out either:

    <scope doc="Return datetime with new specified fields." ilk="function" name="replace" />
    
  • The new Python 3.5 type hinting syntax also needs to know what types of arguments objects expect, and to this end stub files need to be provided for objects that can't be introspected. The Python typeshed project provides these. This includes all argument names for the datetime module:

    class datetime:
        # ...
        def replace(self, year: int = ..., month: int = ..., day: int = ..., hour: int = ...,
            minute: int = ..., second: int = ..., microsecond: int = ..., tzinfo:
            Optional[_tzinfo] = None) -> datetime: ...
    

    You'd have to parse such a file yourself; they can't always be imported as the stubs reference types not yet defined, rather than use forward references:

    >>> import importlib.machinery
    >>> path = 'stdlib/3/datetime.pyi'
    >>> loader = importlib.machinery.SourceFileLoader('datetime', path)
    >>> loader.load_module()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<frozen importlib._bootstrap_external>", line 399, in _check_name_wrapper
      File "<frozen importlib._bootstrap_external>", line 823, in load_module
      File "<frozen importlib._bootstrap_external>", line 682, in load_module
      File "<frozen importlib._bootstrap>", line 251, in _load_module_shim
      File "<frozen importlib._bootstrap>", line 675, in _load
      File "<frozen importlib._bootstrap>", line 655, in _load_unlocked
      File "<frozen importlib._bootstrap_external>", line 678, in exec_module
      File "<frozen importlib._bootstrap>", line 205, in _call_with_frames_removed
      File "stdlib/3/datetime.pyi", line 12, in <module>
        class tzinfo:
      File "stdlib/3/datetime.pyi", line 13, in tzinfo
        def tzname(self, dt: Optional[datetime]) -> str: ...
    NameError: name 'datetime' is not defined
    

    You may be able to work around that by using a pre-defined module object and globals, then iterating on name errors until it imports though. I'll leave that as an exercise for the reader. Mypy and other type checkers don't try to execute the code, they merely build an AST.

like image 96
Martijn Pieters Avatar answered Sep 29 '22 11:09

Martijn Pieters