Failed Conditions
Pull Request — master (#1990)
by Mischa
02:35 queued 43s
created

FunctionMetadata.merge()   C

Complexity

Conditions 8

Size

Total Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
c 1
b 0
f 0
dl 0
loc 73
rs 5.0295

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
        You can use this function to safely pass parameters from a given
170
        dictionary:
171
172
        >>> def multiply(a, b=2, c=0):
173
        ...     return a * b + c
174
        >>> metadata = FunctionMetadata.from_function(multiply)
175
        >>> args = metadata.filter_parameters({'a': 10, 'b': 20, 'd': 30})
176
177
        You can safely pass the arguments to the function now:
178
179
        >>> multiply(**args)  # 10 * 20
180
        200
181
182
        :param dct:
183
            The dict to filter.
184
        :return:
185
            A new dict containing the filtered items.
186
        """
187
        return {key: dct[key]
188
                for key in (self.non_optional_params.keys() |
189
                            self.optional_params.keys())
190
                if key in dct}
191
192
    @classmethod
193
    def merge(cls, *metadatas):
194
        """
195
        Merges signatures of ``FunctionMetadata`` objects.
196
197
        Parameter (either optional or non-optional) and non-parameter
198
        descriptions are merged from left to right, meaning the right hand
199
        metadata overrides the left hand one.
200
201
        >>> def a(x, y):
202
        ...     '''
203
        ...     desc of *a*
204
        ...     :param x: x of a
205
        ...     :param y: y of a
206
        ...     :return:  5*x*y
207
        ...     '''
208
        ...     return 5 * x * y
209
        >>> def b(x):
210
        ...     '''
211
        ...     desc of *b*
212
        ...     :param x: x of b
213
        ...     :return:  100*x
214
        ...     '''
215
        ...     return 100 * x
216
        >>> metadata1 = FunctionMetadata.from_function(a)
217
        >>> metadata2 = FunctionMetadata.from_function(b)
218
        >>> merged = FunctionMetadata.merge(metadata1, metadata2)
219
        >>> merged.name
220
        "<Merged signature of 'a', 'b'>"
221
        >>> merged.desc
222
        'desc of *b*'
223
        >>> merged.retval_desc
224
        '100*x'
225
        >>> merged.non_optional_params['x'][0]
226
        'x of b'
227
        >>> merged.non_optional_params['y'][0]
228
        'y of a'
229
230
        :param metadatas:
231
            The sequence of metadatas to merge.
232
        :return:
233
            A ``FunctionMetadata`` object containing the merged signature of
234
            all given metadatas.
235
        """
236
        # Collect the metadatas, as we operate on them more often and we want
237
        # to support arbitrary sequences.
238
        metadatas = tuple(metadatas)
239
240
        merged_name = ("<Merged signature of " +
241
                       ", ".join(repr(metadata.name)
242
                                 for metadata in metadatas) +
243
                       ">")
244
245
        merged_desc = next((m.desc for m in reversed(metadatas) if m.desc), "")
246
        merged_retval_desc = next(
247
            (m.retval_desc for m in reversed(metadatas) if m.retval_desc), "")
248
        merged_non_optional_params = {}
249
        merged_optional_params = {}
250
251
        for metadata in metadatas:
252
            # Use the fields and not the properties to get also omitted
253
            # parameters.
254
            merged_non_optional_params.update(metadata._non_optional_params)
255
            merged_optional_params.update(metadata._optional_params)
256
257
        merged_omit = set.union(*(metadata.omit for metadata in metadatas))
258
259
        return cls(merged_name,
260
                   merged_desc,
261
                   merged_retval_desc,
262
                   merged_non_optional_params,
263
                   merged_optional_params,
264
                   merged_omit)
265