From 14a8b308da44feb16c472da2099b1eda8ce4e8dc Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Fri, 17 Apr 2020 16:30:34 +1200 Subject: [PATCH] Add function definition handling Story: 1651346 Task: 10551 Change-Id: Ia569192f7958852cfdc5479c159d92b289c7a717 --- ironic/api/expose.py | 10 +- ironic/api/functions.py | 181 ++++++++++++++++++++++++ ironic/tests/unit/api/test_functions.py | 88 ++++++++++++ 3 files changed, 274 insertions(+), 5 deletions(-) create mode 100644 ironic/api/functions.py create mode 100644 ironic/tests/unit/api/test_functions.py diff --git a/ironic/api/expose.py b/ironic/api/expose.py index 593ccacc9c..94c0e8d76e 100644 --- a/ironic/api/expose.py +++ b/ironic/api/expose.py @@ -25,9 +25,9 @@ import traceback from oslo_config import cfg from oslo_log import log import pecan -import wsme import wsme.rest.args +from ironic.api import functions from ironic.api import types as atypes LOG = log.getLogger(__name__) @@ -58,11 +58,11 @@ pecan_json_decorate = pecan.expose( def expose(*args, **kwargs): - sig = wsme.signature(*args, **kwargs) + sig = functions.signature(*args, **kwargs) def decorate(f): sig(f) - funcdef = wsme.api.FunctionDefinition.get(f) + funcdef = functions.FunctionDefinition.get(f) funcdef.resolve_types(atypes.registry) @functools.wraps(f) @@ -212,7 +212,7 @@ class validate(object): self.param_types = param_types def __call__(self, func): - argspec = wsme.api.getargspec(func) - fd = wsme.api.FunctionDefinition.get(func) + argspec = functions.getargspec(func) + fd = functions.FunctionDefinition.get(func) fd.set_arg_types(argspec, self.param_types) return func diff --git a/ironic/api/functions.py b/ironic/api/functions.py new file mode 100644 index 0000000000..8b4cebbdd7 --- /dev/null +++ b/ironic/api/functions.py @@ -0,0 +1,181 @@ +# Copyright 2011-2019 the WSME authors and contributors +# (See https://opendev.org/x/wsme/) +# +# This module is part of WSME and is also released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import functools +import inspect +import logging + +log = logging.getLogger(__name__) + + +def iswsmefunction(f): + return hasattr(f, '_wsme_definition') + + +def wrapfunc(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + return f(*args, **kwargs) + wrapper._wsme_original_func = f + return wrapper + + +def getargspec(f): + f = getattr(f, '_wsme_original_func', f) + return inspect.getargspec(f) + + +class FunctionArgument(object): + """An argument definition of an api entry""" + def __init__(self, name, datatype, mandatory, default): + #: argument name + self.name = name + + #: Data type + self.datatype = datatype + + #: True if the argument is mandatory + self.mandatory = mandatory + + #: Default value if argument is omitted + self.default = default + + def resolve_type(self, registry): + self.datatype = registry.resolve_type(self.datatype) + + +class FunctionDefinition(object): + """An api entry definition""" + def __init__(self, func): + #: Function name + self.name = func.__name__ + + #: Function documentation + self.doc = func.__doc__ + + #: Return type + self.return_type = None + + #: The function arguments (list of :class:`FunctionArgument`) + self.arguments = [] + + #: If the body carry the datas of a single argument, its type + self.body_type = None + + #: Status code + self.status_code = 200 + + #: True if extra arguments should be ignored, NOT inserted in + #: the kwargs of the function and not raise UnknownArgument + #: exceptions + self.ignore_extra_args = False + + #: Dictionnary of protocol-specific options. + self.extra_options = None + + @staticmethod + def get(func): + """Returns the :class:`FunctionDefinition` of a method.""" + if not hasattr(func, '_wsme_definition'): + fd = FunctionDefinition(func) + func._wsme_definition = fd + + return func._wsme_definition + + def get_arg(self, name): + """Returns a :class:`FunctionArgument` from its name""" + for arg in self.arguments: + if arg.name == name: + return arg + return None + + def resolve_types(self, registry): + self.return_type = registry.resolve_type(self.return_type) + self.body_type = registry.resolve_type(self.body_type) + for arg in self.arguments: + arg.resolve_type(registry) + + def set_options(self, body=None, ignore_extra_args=False, status_code=200, + rest_content_types=('json', 'xml'), **extra_options): + self.body_type = body + self.status_code = status_code + self.ignore_extra_args = ignore_extra_args + self.rest_content_types = rest_content_types + self.extra_options = extra_options + + def set_arg_types(self, argspec, arg_types): + args, varargs, keywords, defaults = argspec + if args[0] == 'self': + args = args[1:] + arg_types = list(arg_types) + if self.body_type is not None: + arg_types.append(self.body_type) + for i, argname in enumerate(args): + datatype = arg_types[i] + mandatory = defaults is None or i < (len(args) - len(defaults)) + default = None + if not mandatory: + default = defaults[i - (len(args) - len(defaults))] + self.arguments.append(FunctionArgument(argname, datatype, + mandatory, default)) + + +class signature(object): + + """Decorator that specify the argument types of an exposed function. + + :param return_type: Type of the value returned by the function + :param argN: Type of the Nth argument + :param body: If the function takes a final argument that is supposed to be + the request body by itself, its type. + :param status_code: HTTP return status code of the function. + :param ignore_extra_args: Allow extra/unknow arguments (default to False) + + Most of the time this decorator is not supposed to be used directly, + unless you are not using WSME on top of another framework. + + If an adapter is used, it will provide either a specialised version of this + decororator, either a new decorator named @wsexpose that takes the same + parameters (it will in addition expose the function, hence its name). + """ + + def __init__(self, *types, **options): + self.return_type = types[0] if types else None + self.arg_types = [] + if len(types) > 1: + self.arg_types.extend(types[1:]) + if 'body' in options: + self.arg_types.append(options['body']) + self.wrap = options.pop('wrap', False) + self.options = options + + def __call__(self, func): + argspec = getargspec(func) + if self.wrap: + func = wrapfunc(func) + fd = FunctionDefinition.get(func) + if fd.extra_options is not None: + raise ValueError("This function is already exposed") + fd.return_type = self.return_type + fd.set_options(**self.options) + if self.arg_types: + fd.set_arg_types(argspec, self.arg_types) + return func + + +sig = signature diff --git a/ironic/tests/unit/api/test_functions.py b/ironic/tests/unit/api/test_functions.py new file mode 100644 index 0000000000..2ccd4134da --- /dev/null +++ b/ironic/tests/unit/api/test_functions.py @@ -0,0 +1,88 @@ +# Copyright 2020 Red Hat, Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from ironic.api import functions +from ironic.tests import base as test_base + + +class TestFunctionDefinition(test_base.TestCase): + + def test_get_arg(self): + def myfunc(self, a): + pass + + fd = functions.FunctionDefinition(myfunc) + fd.arguments.append(functions.FunctionArgument('a', int, True, 0)) + arg = fd.get_arg('a') + self.assertEqual(int, arg.datatype) + self.assertEqual('a', arg.name) + self.assertEqual(True, arg.mandatory) + self.assertEqual(0, arg.default) + self.assertIsNone(fd.get_arg('b')) + + def test_set_arg_types(self): + def myfunc(self, string, integer, boolean=True): + pass + + fd = functions.FunctionDefinition(myfunc) + argspec = functions.getargspec(myfunc) + fd.set_arg_types(argspec, [str, int, bool]) + + arg = fd.get_arg('string') + self.assertEqual(str, arg.datatype) + self.assertEqual('string', arg.name) + self.assertEqual(True, arg.mandatory) + self.assertIsNone(arg.default) + + arg = fd.get_arg('integer') + self.assertEqual(int, arg.datatype) + self.assertEqual('integer', arg.name) + self.assertEqual(True, arg.mandatory) + self.assertIsNone(arg.default) + + arg = fd.get_arg('boolean') + self.assertEqual(bool, arg.datatype) + self.assertEqual('boolean', arg.name) + self.assertEqual(False, arg.mandatory) + self.assertTrue(arg.default) + + def test_signature(self): + @functions.signature(str, str, int, bool) + def myfunc(self, string, integer, boolean=True): + '''Do the thing with the thing ''' + return 'result' + + fd = myfunc._wsme_definition + self.assertEqual('myfunc', fd.name) + self.assertEqual('Do the thing with the thing ', fd.doc) + self.assertEqual(str, fd.return_type) + + arg = fd.get_arg('string') + self.assertEqual(str, arg.datatype) + self.assertEqual('string', arg.name) + self.assertEqual(True, arg.mandatory) + self.assertIsNone(arg.default) + + arg = fd.get_arg('integer') + self.assertEqual(int, arg.datatype) + self.assertEqual('integer', arg.name) + self.assertEqual(True, arg.mandatory) + self.assertIsNone(arg.default) + + arg = fd.get_arg('boolean') + self.assertEqual(bool, arg.datatype) + self.assertEqual('boolean', arg.name) + self.assertEqual(False, arg.mandatory) + self.assertTrue(arg.default)