Python3: resources and libraries
[csit.git] / resources / libraries / python / OptionString.py
1 # Copyright (c) 2019 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 """Utility function for handling options without doubled or trailing spaces."""
15
16
17 class OptionString:
18     """Class serving as a builder for option strings.
19
20     Motivation: Both manual concatenation and .join() methods
21     are prone to leaving superfluous spaces if some parts of options
22     are optional (missing, empty).
23
24     The scope of this class is more general than just command line options,
25     it can concatenate any string consisting of words that may be missing.
26     But options were the first usage, so method arguments are frequently
27     named "parameter" and "value".
28     To keep this generality, automated adding of dashes is optional,
29     and disabled by default.
30
31     Parts of the whole option string are kept as list items (string, stipped),
32     with prefix already added.
33     Empty strings are never added to the list (except by constructor).
34
35     The class offers many methods for adding, so that callers can pick
36     the best fitting one, without much logic near the call site.
37     """
38
39     def __init__(self, parts=tuple(), prefix=u""):
40         """Create instance with listed strings as parts to use.
41
42         Prefix will be converted to string and stripped.
43         The typical (nonempty) prefix values are "-" and "--".
44
45         TODO: Support users calling with parts being a string?
46
47         :param parts: List of stringifiable objects to become parts.
48         :param prefix: Substring to prepend to every parameter (not value).
49         :type parts: Iterable of object
50         :type prefix: object
51         """
52         self.parts = [str(part) for part in parts]
53         self.prefix = str(prefix).strip()  # Not worth to call change_prefix.
54
55     def __repr__(self):
56         """Return string executable as Python constructor call.
57
58         :returns: Executable constructor call as string.
59         :rtype: str
60         """
61         return f"OptionString(parts={self.parts!r},prefix={self.prefix!r})"
62
63     # TODO: Would we ever need a copy() method?
64     # Currently, superstring "master" is mutable but unique,
65     # substring "slave" can be used to extend, but does not need to be mutated.
66
67     def change_prefix(self, prefix):
68         """Change the prefix field from the initialized value.
69
70         Sometimes it is more convenient to change the prefix in the middle
71         of string construction.
72         Typical use is for constructing a command, where the first part
73         (executeble filename) does not have a dash, but the other parameters do.
74         You could put the first part into constructor argument,
75         but using .add and only then enabling prefix is horizontally shorter.
76
77         :param prefix: New prefix value, to be converted and tripped.
78         :type prefix: object
79         :returns: Self, to enable method chaining.
80         :rtype: OptionString
81         """
82         self.prefix = str(prefix).strip()
83
84     def extend(self, other):
85         """Extend self by contents of other option string.
86
87         :param other: Another instance to add to the end of self.
88         :type other: OptionString
89         :returns: Self, to enable method chaining.
90         :rtype: OptionString
91         """
92         self.parts.extend(other.parts)
93         return self
94
95     def _check_and_add(self, part, prefixed):
96         """Convert to string, strip, conditionally add prefixed if non-empty.
97
98         Value of None is converted to empty string.
99         Emptiness is tested before adding prefix.
100
101         :param part: Unchecked part to add to list of parts.
102         :param prefixed: Whether to add prefix when adding.
103         :type part: object
104         :type prefixed: object
105         :returns: The converted part without prefix, empty means not added.
106         :rtype: str
107         """
108         part = u"" if part is None else str(part).strip()
109         if part:
110             prefixed_part = self.prefix + part if prefixed else part
111             self.parts.append(prefixed_part)
112         return part
113
114     def add(self, parameter):
115         """Add parameter if nonempty to the list of parts.
116
117         Parameter object is converted to string and stripped.
118         If parameter converts to empty string, nothing is added.
119         Parameter is prefixed before adding.
120
121         :param parameter: Parameter object, usually a word starting with dash.
122         :type parameter: object
123         :returns: Self, to enable method chaining.
124         :rtype: OptionString
125         """
126         self._check_and_add(parameter, prefixed=True)
127         return self
128
129     def add_if(self, parameter, condition):
130         """Add parameter if nonempty and condition is true to the list of parts.
131
132         If condition truth value is false, nothing is added.
133         Parameter object is converted to string and stripped.
134         If parameter converts to empty string, nothing is added.
135         Parameter is prefixed before adding.
136
137         :param parameter: Parameter object, usually a word starting with dash.
138         :param condition: Do not add if truth value of this is false.
139         :type parameter: object
140         :type condition: object
141         :returns: Self, to enable method chaining.
142         :rtype: OptionString
143         """
144         if condition:
145             self.add(parameter)
146         return self
147
148     def add_with_value(self, parameter, value):
149         """Add parameter, if followed by a value to the list of parts.
150
151         Parameter and value are converted to string and stripped.
152         If parameter or value converts to empty string, nothing is added.
153         If added, parameter (but not value) is prefixed.
154
155         :param parameter: Parameter object, usually a word starting with dash.
156         :param value: Value object. Prefix is never added.
157         :type parameter: object
158         :type value: object
159         :returns: Self, to enable method chaining.
160         :rtype: OptionString
161         """
162         temp = OptionString(prefix=self.prefix)
163         # TODO: Is pylint really that ignorant?
164         # How could it not understand temp is of type of this class?
165         # pylint: disable=protected-access
166         if temp._check_and_add(parameter, prefixed=True):
167             if temp._check_and_add(value, prefixed=False):
168                 self.extend(temp)
169         return self
170
171     def add_equals(self, parameter, value):
172         """Add parameter=value to the list of parts.
173
174         Parameter and value are converted to string and stripped.
175         If parameter or value converts to empty string, nothing is added.
176         If added, parameter (but not value) is prefixed.
177
178         :param parameter: Parameter object, usually a word starting with dash.
179         :param value: Value object. Prefix is never added.
180         :type parameter: object
181         :type value: object
182         :returns: Self, to enable method chaining.
183         :rtype: OptionString
184         """
185         temp = OptionString(prefix=self.prefix)
186         # pylint: disable=protected-access
187         if temp._check_and_add(parameter, prefixed=True):
188             if temp._check_and_add(value, prefixed=False):
189                 self.parts.append(u"=".join(temp.parts))
190         return self
191
192     def add_with_value_if(self, parameter, value, condition):
193         """Add parameter and value if condition is true and nothing is empty.
194
195         If condition truth value is false, nothing is added.
196         Parameter and value are converted to string and stripped.
197         If parameter or value converts to empty string, nothing is added.
198         If added, parameter (but not value) is prefixed.
199
200         :param parameter: Parameter object, usually a word starting with dash.
201         :param value: Value object. Prefix is never added.
202         :param condition: Do not add if truth value of this is false.
203         :type parameter: object
204         :type value: object
205         :type condition: object
206         :returns: Self, to enable method chaining.
207         :rtype: OptionString
208         """
209         if condition:
210             self.add_with_value(parameter, value)
211         return self
212
213     def add_equals_if(self, parameter, value, condition):
214         """Add parameter=value to the list of parts if condition is true.
215
216         If condition truth value is false, nothing is added.
217         Parameter and value are converted to string and stripped.
218         If parameter or value converts to empty string, nothing is added.
219         If added, parameter (but not value) is prefixed.
220
221         :param parameter: Parameter object, usually a word starting with dash.
222         :param value: Value object. Prefix is never added.
223         :param condition: Do not add if truth value of this is false.
224         :type parameter: object
225         :type value: object
226         :type condition: object
227         :returns: Self, to enable method chaining.
228         :rtype: OptionString
229         """
230         if condition:
231             self.add_equals(parameter, value)
232         return self
233
234     def add_with_value_from_dict(self, parameter, key, mapping, default=u""):
235         """Add parameter with value from dict under key, or default.
236
237         If key is missing, default is used as value.
238         Parameter and value are converted to string and stripped.
239         If parameter or value converts to empty string, nothing is added.
240         If added, parameter (but not value) is prefixed.
241
242         :param parameter: The parameter part to add with prefix.
243         :param key: The key to look the value for.
244         :param mapping: Mapping with keys and values to use.
245         :param default: The value to use if key is missing.
246         :type parameter: object
247         :type key: str
248         :type mapping: dict
249         :type default: object
250         :returns: Self, to enable method chaining.
251         :rtype: OptionString
252         """
253         value = mapping.get(key, default)
254         return self.add_with_value(parameter, value)
255
256     def add_equals_from_dict(self, parameter, key, mapping, default=u""):
257         """Add parameter=value to options where value is from dict.
258
259         If key is missing, default is used as value.
260         Parameter and value are converted to string and stripped.
261         If parameter or value converts to empty string, nothing is added.
262         If added, parameter (but not value) is prefixed.
263
264         :param parameter: The parameter part to add with prefix.
265         :param key: The key to look the value for.
266         :param mapping: Mapping with keys and values to use.
267         :param default: The value to use if key is missing.
268         :type parameter: object
269         :type key: str
270         :type mapping: dict
271         :type default: object
272         :returns: Self, to enable method chaining.
273         :rtype: OptionString
274         """
275         value = mapping.get(key, default)
276         return self.add_equals(parameter, value)
277
278     def add_if_from_dict(self, parameter, key, mapping, default=u"False"):
279         """Add parameter based on if the condition in dict is true.
280
281         If key is missing, default is used as condition.
282         If condition truth value is false, nothing is added.
283         Parameter is converted to string and stripped.
284         If parameter converts to empty string, nothing is added.
285         Parameter is prefixed before adding.
286
287         :param parameter: The parameter part to add with prefix.
288         :param key: The key to look the value for.
289         :param mapping: Mapping with keys and values to use.
290         :param default: The value to use if key is missing.
291         :type parameter: object
292         :type key: str
293         :type mapping: dict
294         :type default: object
295         :returns: Self, to enable method chaining.
296         :rtype: OptionString
297         """
298         condition = mapping.get(key, default)
299         return self.add_if(parameter, condition)
300
301     def add_with_value_if_from_dict(
302             self, parameter, value, key, mapping, default=u"False"):
303         """Add parameter and value based on condition in dict.
304
305         If key is missing, default is used as condition.
306         If condition truth value is false, nothing is added.
307         Parameter and value are converted to string and stripped.
308         If parameter or value converts to empty string, nothing is added.
309         If added, parameter (but not value) is prefixed.
310
311         :param parameter: The parameter part to add with prefix.
312         :param value: Value object. Prefix is never added.
313         :param key: The key to look the value for.
314         :param mapping: Mapping with keys and values to use.
315         :param default: The value to use if key is missing.
316         :type parameter: object
317         :type value: object
318         :type key: str
319         :type mapping: dict
320         :type default: object
321         :returns: Self, to enable method chaining.
322         :rtype: OptionString
323         """
324         condition = mapping.get(key, default)
325         return self.add_with_value_if(parameter, value, condition)
326
327     def add_equals_if_from_dict(
328             self, parameter, value, key, mapping, default=u"False"):
329         """Add parameter=value based on condition in dict.
330
331         If key is missing, default is used as condition.
332         If condition truth value is false, nothing is added.
333         Parameter and value are converted to string and stripped.
334         If parameter or value converts to empty string, nothing is added.
335         If added, parameter (but not value) is prefixed.
336
337         :param parameter: The parameter part to add with prefix.
338         :param value: Value object. Prefix is never added.
339         :param key: The key to look the value for.
340         :param mapping: Mapping with keys and values to use.
341         :param default: The value to use if key is missing.
342         :type parameter: object
343         :type value: object
344         :type key: str
345         :type mapping: dict
346         :type default: object
347         :returns: Self, to enable method chaining.
348         :rtype: OptionString
349         """
350         condition = mapping.get(key, default)
351         return self.add_equals_if(parameter, value, condition)
352
353     def __str__(self):
354         """Return space separated string of nonempty parts.
355
356         The format is suitable to be pasted as (part of) command line.
357         Do not call str() prematurely just to get a substring, consider
358         converting the surrounding text manipulation to OptionString as well.
359
360         :returns: Space separated string of options.
361         :rtype: str
362         """
363         return u" ".join(self.parts)