Completed
Pull Request — master (#2432)
by Zatreanu
01:47
created

FunctionMetadata   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Importance

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