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

FunctionMetadata.__init__()   B

Complexity

Conditions 3

Size

Total Lines 35

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 3
dl 0
loc 35
rs 8.8571
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(str(defaults[i-num_non_defaults]))
153
                    + ")",
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
    @classmethod
165
    def merge(cls, *metadatas):
166
        """
167
        Merges signatures of ``FunctionMetadata`` objects.
168
169
        Parameter descriptions (either optional or non-optional) are merged
170
        from left to right, meaning the right hand metadata overrides the left
171
        hand one.
172
173
        :param metadatas:
174
            The sequence of metadatas to merge.
175
        :raises ValueError:
176
            Raised when less than 2 metadatas are provided.
177
        :return:
178
            A ``FunctionMetadata`` object containing the merged signature of
179
            all given metadatas.
180
        """
181
        # Collect the metadatas, as we operate on them more often and we want
182
        # to support arbitrary sequences.
183
        metadatas = tuple(metadatas)
184
        if len(metadatas) < 2:
185
            raise ValueError("Less than 2 metadata objects provided.")
186
187
        merged_name = ("<Merged signature of " +
188
                       ", ".join(repr(metadata.name)
189
                                 for metadata in metadatas) +
190
                       ">")
191
192
        merged_desc = "\n".join("{}:\n{}".format(metadata.name, metadata.desc)
193
                                for metadata in metadatas)
194
195
        merged_retval_desc = "\n".join(
196
            "{}:\n{}".format(metadata.name, metadata.retval_desc)
197
            for metadata in metadatas)
198
199
        merged_non_optional_params = {}
200
        merged_optional_params = {}
201
        for metadata in metadatas:
202
            merged_non_optional_params.update(metadata.non_optional_params)
203
            merged_optional_params.update(metadata.optional_params)
204
205
        merged_omit = set.union(*(metadata.omit for metadata in metadatas))
206
207
        return cls(merged_name,
208
                   merged_desc,
209
                   merged_retval_desc,
210
                   merged_non_optional_params,
211
                   merged_optional_params,
212
                   merged_omit)
213