FunctionMetadata.add_alias()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
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 add_alias(self, original, alias):
95
        """
96
        Adds an alias for the original setting. The alias setting will have
97
        the same metadata as the original one. If the original setting is not
98
        optional, the alias will default to ``None``.
99
100
        :param original:  The name of the original setting.
101
        :param alias:     The name of the alias for the original.
102
        :raises KeyError: If the new setting doesn't exist in the metadata.
103
        """
104
        self._optional_params[alias] = (
105
            self._optional_params[original]
106
            if original in self._optional_params
107
            else self._non_optional_params[original] + (None, ))
108
109
    def create_params_from_section(self, section):
110
        """
111
        Create a params dictionary for this function that holds all values the
112
        function needs plus optional ones that are available.
113
114
        :param section:    The section to retrieve the values from.
115
        :return:           The params dictionary.
116
        """
117
        params = {}
118
119
        for param in self.non_optional_params:
120
            _, annotation = self.non_optional_params[param]
121
            params[param] = self._get_param(param, section, annotation)
122
123
        for param in self.optional_params:
124
            if param in section:
125
                _, annotation, _ = self.optional_params[param]
126
                params[param] = self._get_param(param, section, annotation)
127
128
        return params
129
130
    @staticmethod
131
    def _get_param(param, section, annotation):
132
        if annotation is None:
133
            annotation = lambda x: x
134
135
        try:
136
            return annotation(section[param])
137
        except (TypeError, ValueError):
138
            raise ValueError("Unable to convert parameter {!r} into type "
139
                             "{}.".format(param, annotation))
140
141
    @classmethod
142
    def from_function(cls, func, omit=frozenset()):
143
        """
144
        Creates a FunctionMetadata object from a function. Please note that any
145
        variable argument lists are not supported. If you do not want the
146
        first (usual named 'self') argument to appear please pass the method of
147
        an actual INSTANCE of a class; passing the method of the class isn't
148
        enough. Alternatively you can add "self" to the omit set.
149
150
        :param func: The function. If __metadata__ of the unbound function is
151
                     present it will be copied and used, otherwise it will be
152
                     generated.
153
        :param omit: A set of parameter names that are to be ignored.
154
        :return:     The FunctionMetadata object corresponding to the given
155
                     function.
156
        """
157
        if hasattr(func, "__metadata__"):
158
            metadata = copy(func.__metadata__)
159
            metadata.omit = omit
160
            return metadata
161
162
        doc = func.__doc__ or ""
163
        doc_comment = DocstringMetadata.from_docstring(doc)
164
165
        non_optional_params = OrderedDict()
166
        optional_params = OrderedDict()
167
168
        argspec = getfullargspec(func)
169
        args = () if argspec.args is None else argspec.args
170
        defaults = () if argspec.defaults is None else argspec.defaults
171
        num_non_defaults = len(args) - len(defaults)
172
        for i, arg in enumerate(args):
173
            # Implicit self argument or omitted explicitly
174
            if i < 1 and ismethod(func):
175
                continue
176
177
            if i < num_non_defaults:
178
                non_optional_params[arg] = (
179
                    doc_comment.param_dict.get(arg, cls.str_nodesc),
180
                    argspec.annotations.get(arg, None))
181
            else:
182
                optional_params[arg] = (
183
                    doc_comment.param_dict.get(arg, cls.str_nodesc) + " (" +
184
                    cls.str_optional.format(
185
                        defaults[i-num_non_defaults]) + ")",
186
                    argspec.annotations.get(arg, None),
187
                    defaults[i-num_non_defaults])
188
189
        return cls(name=func.__name__,
190
                   desc=doc_comment.desc,
191
                   retval_desc=doc_comment.retval_desc,
192
                   non_optional_params=non_optional_params,
193
                   optional_params=optional_params,
194
                   omit=omit)
195
196
    def filter_parameters(self, dct):
197
        """
198
        Filters the given dict for keys that are declared as parameters inside
199
        this metadata (either optional or non-optional).
200
201
        You can use this function to safely pass parameters from a given
202
        dictionary:
203
204
        >>> def multiply(a, b=2, c=0):
205
        ...     return a * b + c
206
        >>> metadata = FunctionMetadata.from_function(multiply)
207
        >>> args = metadata.filter_parameters({'a': 10, 'b': 20, 'd': 30})
208
209
        You can safely pass the arguments to the function now:
210
211
        >>> multiply(**args)  # 10 * 20
212
        200
213
214
        :param dct:
215
            The dict to filter.
216
        :return:
217
            A new dict containing the filtered items.
218
        """
219
        return {key: dct[key]
220
                for key in (self.non_optional_params.keys() |
221
                            self.optional_params.keys())
222
                if key in dct}
223
224
    @classmethod
225
    def merge(cls, *metadatas):
226
        """
227
        Merges signatures of ``FunctionMetadata`` objects.
228
229
        Parameter (either optional or non-optional) and non-parameter
230
        descriptions are merged from left to right, meaning the right hand
231
        metadata overrides the left hand one.
232
233
        >>> def a(x, y):
234
        ...     '''
235
        ...     desc of *a*
236
        ...     :param x: x of a
237
        ...     :param y: y of a
238
        ...     :return:  5*x*y
239
        ...     '''
240
        ...     return 5 * x * y
241
        >>> def b(x):
242
        ...     '''
243
        ...     desc of *b*
244
        ...     :param x: x of b
245
        ...     :return:  100*x
246
        ...     '''
247
        ...     return 100 * x
248
        >>> metadata1 = FunctionMetadata.from_function(a)
249
        >>> metadata2 = FunctionMetadata.from_function(b)
250
        >>> merged = FunctionMetadata.merge(metadata1, metadata2)
251
        >>> merged.name
252
        "<Merged signature of 'a', 'b'>"
253
        >>> merged.desc
254
        'desc of *b*'
255
        >>> merged.retval_desc
256
        '100*x'
257
        >>> merged.non_optional_params['x'][0]
258
        'x of b'
259
        >>> merged.non_optional_params['y'][0]
260
        'y of a'
261
262
        :param metadatas:
263
            The sequence of metadatas to merge.
264
        :return:
265
            A ``FunctionMetadata`` object containing the merged signature of
266
            all given metadatas.
267
        """
268
        # Collect the metadatas, as we operate on them more often and we want
269
        # to support arbitrary sequences.
270
        metadatas = tuple(metadatas)
271
272
        merged_name = ("<Merged signature of " +
273
                       ", ".join(repr(metadata.name)
274
                                 for metadata in metadatas) +
275
                       ">")
276
277
        merged_desc = next((m.desc for m in reversed(metadatas) if m.desc), "")
278
        merged_retval_desc = next(
279
            (m.retval_desc for m in reversed(metadatas) if m.retval_desc), "")
280
        merged_non_optional_params = {}
281
        merged_optional_params = {}
282
283
        for metadata in metadatas:
284
            # Use the fields and not the properties to get also omitted
285
            # parameters.
286
            merged_non_optional_params.update(metadata._non_optional_params)
287
            merged_optional_params.update(metadata._optional_params)
288
289
        merged_omit = set.union(*(metadata.omit for metadata in metadatas))
290
291
        return cls(merged_name,
292
                   merged_desc,
293
                   merged_retval_desc,
294
                   merged_non_optional_params,
295
                   merged_optional_params,
296
                   merged_omit)
297