Add __signature__ to plugins

Auto-generate inspect.Signature from plugin arguments and options. The
signature is used by (amongst others) pydoc / help.

```
$ ipa console
>>> help(api.Command.group_add)
Help on group_add in module ipaserver.plugins.group object:

class group_add(ipaserver.plugins.baseldap.LDAPCreate)
 |  group_add(cn: str, *, description: str = None, gidnumber: int = None, setattr: List[str] = None, addattr: List[str] = None, nonposix: bool, external: bool, all: bool, raw: bool, version: str = None, no_members: bool) -> Dict[str, Any]
```

Fixes: https://pagure.io/freeipa/issue/8388
Signed-off-by: Christian Heimes <cheimes@redhat.com>
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
This commit is contained in:
Christian Heimes 2020-06-26 17:07:50 +02:00
parent 51d5ec1757
commit 069f41a01e
3 changed files with 101 additions and 1 deletions

View File

@ -29,6 +29,7 @@ from ipapython.ipautil import APIVersion
from ipalib.base import NameSpace
from ipalib.plugable import Plugin, APINameSpace
from ipalib.parameters import create_param, Param, Str, Flag
from ipalib.parameters import create_signature
from ipalib.parameters import Password # pylint: disable=unused-import
from ipalib.output import Output, Entry, ListOfEntries
from ipalib.text import _
@ -37,7 +38,7 @@ from ipalib.errors import (ZeroArgumentError, MaxArgumentError, OverlapError,
ValidationError, ConversionError)
from ipalib import errors, messages
from ipalib.request import context, context_frame
from ipalib.util import classproperty, json_serialize
from ipalib.util import classproperty, classobjectproperty, json_serialize
if six.PY3:
unicode = str
@ -433,6 +434,26 @@ class Command(HasParam):
topic = classproperty(__topic_getter)
@classobjectproperty
@classmethod
def __signature__(cls, obj):
# signature is cached on the class object
if hasattr(cls, "_signature"):
return cls._signature
# can only create signature for 'final' classes
# help(api.Command.user_show) breaks because pydoc inspects parent
# classes and baseuser plugin is not a registered object.
if cls.__subclasses__():
cls._signature = None
return None
# special, rare case: user calls help() on a plugin class instead of
# an instance
if obj is None:
from ipalib import api
obj = cls(api=api)
cls._signature = signature = create_signature(obj)
return signature
@property
def forwarded_name(self):
return self.full_name

View File

@ -103,10 +103,13 @@ import re
import decimal
import base64
import datetime
import inspect
import typing
from xmlrpc.client import MAXINT, MININT
import six
from cryptography import x509 as crypto_x509
import dns.name
from ipalib.text import _ as ugettext
from ipalib.base import check_name
@ -2159,3 +2162,67 @@ class Principal(Param):
name=self.get_param_name(),
error=_("Service principal is required")
)
_map_types = {
# map internal certificate subclass to generic cryptography class
IPACertificate: crypto_x509.Certificate,
# map internal DNS name class to generic dnspython class
DNSName: dns.name.Name,
# DN, Principal have their names mangled in ipaapi.__init__
}
def create_signature(command):
"""Create an inspect.Signature for a command
:param command: ipa plugin instance (server or client)
:return: inspect.Signature instance
"""
signature_params = []
seen = set()
args_options = [
(command.get_args(), inspect.Parameter.POSITIONAL_OR_KEYWORD),
(command.get_options(), inspect.Parameter.KEYWORD_ONLY)
]
for ipaparams, kind in args_options:
for ipaparam in ipaparams:
# filter out duplicates, for example user_del has a preserve flag
# and preserve bool.
if ipaparam.name in seen:
continue
seen.add(ipaparam.name)
# ipalib.plugins.misc.env has wrong type
if not isinstance(ipaparam, Param):
continue
if ipaparam.required:
default = inspect.Parameter.empty
else:
default = ipaparam.default
allowed_types = tuple(
_map_types.get(t, t) for t in ipaparam.allowed_types
)
# ipalib.parameters.DNSNameParam also handles text
if isinstance(ipaparam, DNSNameParam):
allowed_types += (six.text_type,)
ann = typing.Union[allowed_types]
if ipaparam.multivalue:
ann = typing.List[ann]
signature_params.append(
inspect.Parameter(
ipaparam.name, kind, default=default, annotation=ann
)
)
# cannot describe return parameter with typing yet. TypedDict
# is only available with mypy_extension.
signature = inspect.Signature(
signature_params,
return_annotation=typing.Dict[typing.Text, typing.Any]
)
return signature

View File

@ -1027,6 +1027,7 @@ class classproperty:
__slots__ = ('__doc__', 'fget')
def __init__(self, fget=None, doc=None):
assert isinstance(fget, classmethod)
if doc is None and fget is not None:
doc = fget.__doc__
@ -1049,6 +1050,17 @@ class classproperty:
return self
class classobjectproperty(classproperty):
# A class property that also passes the object to the getter
# obj is None for class objects and 'self' for instance objects.
__slots__ = ('__doc__',)
def __get__(self, obj, obj_type):
if self.fget is not None:
return self.fget.__get__(obj, obj_type)(obj)
raise AttributeError("unreadable attribute")
def normalize_hostname(hostname):
"""Use common fqdn form without the trailing dot"""
if hostname.endswith(u'.'):