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: |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
![]() |
|||
37 | non_optional_params = OrderedDict() |
||
38 | if optional_params is None: |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
39 | optional_params = OrderedDict() |
||
40 | |||
41 | self.name = name |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
42 | self.desc = desc |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
43 | self.retval_desc = retval_desc |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
44 | self._non_optional_params = non_optional_params |
||
45 | self._optional_params = optional_params |
||
46 | self.omit = set(omit) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
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 |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
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) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
67 | |||
68 | @property |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
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) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
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: |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
88 | _, annotation = self.non_optional_params[param] |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
89 | params[param] = self._get_param(param, section, annotation) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Comprehensibility
Best Practice
introduced
by
|
|||
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 |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
97 | |||
98 | @staticmethod |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
99 | def _get_param(param, section, annotation): |
||
100 | if annotation is None: |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
101 | annotation = lambda x: x |
||
102 | |||
103 | try: |
||
104 | return annotation(section[param]) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Comprehensibility
Best Practice
introduced
by
|
|||
105 | except (TypeError, ValueError): |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Comprehensibility
Best Practice
introduced
by
|
|||
106 | raise ValueError("Unable to convert parameter {} into type " |
||
107 | "{}.".format(repr(param), annotation)) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
108 | |||
109 | @classmethod |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
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__"): |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
126 | metadata = copy(func.__metadata__) |
||
127 | metadata.omit = omit |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
128 | return metadata |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
129 | |||
130 | doc = func.__doc__ or "" |
||
131 | doc_comment = DocumentationComment.from_docstring(doc) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
132 | |||
133 | non_optional_params = OrderedDict() |
||
134 | optional_params = OrderedDict() |
||
135 | |||
136 | argspec = getfullargspec(func) |
||
137 | args = argspec.args or () |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
138 | defaults = argspec.defaults or () |
||
139 | num_non_defaults = len(args) - len(defaults) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Comprehensibility
Best Practice
introduced
by
|
|||
140 | for i, arg in enumerate(args): |
||
141 | # Implicit self argument or omitted explicitly |
||
142 | if i < 1 and ismethod(func): |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
143 | continue |
||
144 | |||
145 | if i < num_non_defaults: |
||
146 | non_optional_params[arg] = ( |
||
147 | doc_comment.param_dict.get(arg, cls.str_nodesc), |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Comprehensibility
Best Practice
introduced
by
|
|||
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])) + ")", |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
154 | argspec.annotations.get(arg, None), |
||
155 | defaults[i-num_non_defaults]) |
||
156 | |||
157 | return cls(name=func.__name__, |
||
158 | desc=doc_comment.desc, |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
159 | retval_desc=doc_comment.retval_desc, |
||
160 | non_optional_params=non_optional_params, |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
161 | optional_params=optional_params, |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
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] |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Comprehensibility
Best Practice
introduced
by
|
|||
188 | for key in (self.non_optional_params.keys() | |
||
189 | self.optional_params.keys()) |
||
190 | if key in dct} |
||
191 | |||
192 | @classmethod |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
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) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
239 | |||
240 | merged_name = ("<Merged signature of " + |
||
241 | ", ".join(repr(metadata.name) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
242 | for metadata in metadatas) + |
||
243 | ">") |
||
244 | |||
245 | merged_desc = next((m.desc for m in reversed(metadatas) if m.desc), "") |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
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, |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
260 | merged_desc, |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
261 | merged_retval_desc, |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
262 | merged_non_optional_params, |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
263 | merged_optional_params, |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
264 | merged_omit) |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
265 |