Failed Conditions
Pull Request — master (#1990)
by Mischa
01:41
created

FunctionMetadata.merge()   B

Complexity

Conditions 5

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 5
dl 0
loc 45
rs 8.0894
1
from collections import OrderedDict
2
from copy import copy
3
from inspect import getfullargspec, ismethod
4
5
from coalib.settings.DocumentationComment import DocumentationComment
6
7
8
class FunctionMetadata:
9
    str_nodesc = "No description given."
10
    str_optional = "Optional, defaults to '{}'."
11
12
    def __init__(self,
13
                 name,
14
                 desc="",
15
                 retval_desc="",
16
                 non_optional_params=None,
17
                 optional_params=None,
18
                 omit=frozenset()):
19
        """
20
        Creates the FunctionMetadata object.
21
22
        :param name:                The name of the function.
23
        :param desc:                The description of the function.
24
        :param retval_desc:         The retval description of the function.
25
        :param non_optional_params: A dict containing the name of non optional
26
                                    parameters as the key and a tuple of a
27
                                    description and the python annotation. To
28
                                    preserve the order, use OrderedDict.
29
        :param optional_params:     A dict containing the name of optional
30
                                    parameters as the key and a tuple
31
                                    of a description, the python annotation and
32
                                    the default value. To preserve the order,
33
                                    use OrderedDict.
34
        :param omit:                A set of parameters to omit.
35
        """
36
        if non_optional_params is None:
37
            non_optional_params = OrderedDict()
38
        if optional_params is None:
39
            optional_params = OrderedDict()
40
41
        self.name = name
42
        self.desc = desc
43
        self.retval_desc = retval_desc
44
        self._non_optional_params = non_optional_params
45
        self._optional_params = optional_params
46
        self.omit = set(omit)
47
48
    def _filter_out_omitted(self, params):
49
        """
50
        Filters out parameters that are to omit. This is a helper method for
51
        the param related properties.
52
53
        :param params: The parameter dictionary to filter.
54
        :return:       The filtered dictionary.
55
        """
56
        return OrderedDict(filter(lambda p: p[0] not in self.omit,
57
                                  tuple(params.items())))
58
59
    @property
60
    def non_optional_params(self):
61
        """
62
        Retrieves a dict containing the name of non optional parameters as the
63
        key and a tuple of a description and the python annotation. Values that
64
        are present in self.omit will be omitted.
65
        """
66
        return self._filter_out_omitted(self._non_optional_params)
67
68
    @property
69
    def optional_params(self):
70
        """
71
        Retrieves a dict containing the name of optional parameters as the key
72
        and a tuple of a description, the python annotation and the default
73
        value. Values that are present in self.omit will be omitted.
74
        """
75
        return self._filter_out_omitted(self._optional_params)
76
77
    def create_params_from_section(self, section):
78
        """
79
        Create a params dictionary for this function that holds all values the
80
        function needs plus optional ones that are available.
81
82
        :param section:    The section to retrieve the values from.
83
        :return:           The params dictionary.
84
        """
85
        params = {}
86
87
        for param in self.non_optional_params:
88
            _, annotation = self.non_optional_params[param]
89
            params[param] = self._get_param(param, section, annotation)
90
91
        for param in self.optional_params:
92
            if param in section:
93
                _, annotation, _ = self.optional_params[param]
94
                params[param] = self._get_param(param, section, annotation)
95
96
        return params
97
98
    @staticmethod
99
    def _get_param(param, section, annotation):
100
        if annotation is None:
101
            annotation = lambda x: x
102
103
        try:
104
            return annotation(section[param])
105
        except (TypeError, ValueError):
106
            raise ValueError("Unable to convert parameter {} into type "
107
                             "{}.".format(repr(param), annotation))
108
109
    @classmethod
110
    def from_function(cls, func, omit=frozenset()):
111
        """
112
        Creates a FunctionMetadata object from a function. Please note that any
113
        variable argument lists are not supported. If you do not want the
114
        first (usual named 'self') argument to appear please pass the method of
115
        an actual INSTANCE of a class; passing the method of the class isn't
116
        enough. Alternatively you can add "self" to the omit set.
117
118
        :param func: The function. If __metadata__ of the unbound function is
119
                     present it will be copied and used, otherwise it will be
120
                     generated.
121
        :param omit: A set of parameter names that are to be ignored.
122
        :return:     The FunctionMetadata object corresponding to the given
123
                     function.
124
        """
125
        if hasattr(func, "__metadata__"):
126
            metadata = copy(func.__metadata__)
127
            metadata.omit = omit
128
            return metadata
129
130
        doc = func.__doc__ or ""
131
        doc_comment = DocumentationComment.from_docstring(doc)
132
133
        non_optional_params = OrderedDict()
134
        optional_params = OrderedDict()
135
136
        argspec = getfullargspec(func)
137
        args = argspec.args or ()
138
        defaults = argspec.defaults or ()
139
        num_non_defaults = len(args) - len(defaults)
140
        for i, arg in enumerate(args):
141
            # Implicit self argument or omitted explicitly
142
            if i < 1 and ismethod(func):
143
                continue
144
145
            if i < num_non_defaults:
146
                non_optional_params[arg] = (
147
                    doc_comment.param_dict.get(arg, cls.str_nodesc),
148
                    argspec.annotations.get(arg, None))
149
            else:
150
                optional_params[arg] = (
151
                    doc_comment.param_dict.get(arg, cls.str_nodesc) + " (" +
152
                    cls.str_optional.format(
153
                        str(defaults[i-num_non_defaults])) + ")",
154
                    argspec.annotations.get(arg, None),
155
                    defaults[i-num_non_defaults])
156
157
        return cls(name=func.__name__,
158
                   desc=doc_comment.desc,
159
                   retval_desc=doc_comment.retval_desc,
160
                   non_optional_params=non_optional_params,
161
                   optional_params=optional_params,
162
                   omit=omit)
163
164
    def filter_parameters(self, dct):
165
        """
166
        Filters the given dict for keys that are declared as parameters inside
167
        this metadata (either optional or non-optional).
168
169
        :param dct:
170
            The dict to filter.
171
        :return:
172
            A new dict containing the filtered items.
173
        """
174
        return {key: dct[key]
175
                for key in self.non_optional_params.keys() |
176
                self.optional_params.keys()
177
                if key in dct}
178
179
    @classmethod
180
    def merge(cls, *metadatas):
181
        """
182
        Merges signatures of ``FunctionMetadata`` objects.
183
184
        Parameter (either optional or non-optional) and non-parameter
185
        descriptions are merged from left to right, meaning the right hand
186
        metadata overrides the left hand one.
187
188
        :param metadatas:
189
            The sequence of metadatas to merge.
190
        :raises ValueError:
191
            Raised when less than 2 metadatas are provided.
192
        :return:
193
            A ``FunctionMetadata`` object containing the merged signature of
194
            all given metadatas.
195
        """
196
        # Collect the metadatas, as we operate on them more often and we want
197
        # to support arbitrary sequences.
198
        metadatas = tuple(metadatas)
199
        if len(metadatas) < 2:
200
            raise ValueError("Less than 2 metadata objects provided.")
201
202
        merged_name = ("<Merged signature of " +
203
                       ", ".join(repr(metadata.name)
204
                                 for metadata in metadatas) +
205
                       ">")
206
207
        merged_desc = metadatas[-1].desc
208
        merged_retval_desc = metadatas[-1].retval_desc
209
        merged_non_optional_params = {}
210
        merged_optional_params = {}
211
212
        for metadata in metadatas:
213
            merged_non_optional_params.update(metadata.non_optional_params)
214
            merged_optional_params.update(metadata.optional_params)
215
216
        merged_omit = set.union(*(metadata.omit for metadata in metadatas))
217
218
        return cls(merged_name,
219
                   merged_desc,
220
                   merged_retval_desc,
221
                   merged_non_optional_params,
222
                   merged_optional_params,
223
                   merged_omit)
224