bdb5ee2b4c723ed8bf03ec5769ed0c3885e10c00
[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         This could be a protected method (name starting with underscore),
102         but then pylint does not understand add_equals and add_with_value
103         are allowed to call this on the temp instance.
104         TODO: Is there a way to make pylint understand?
105
106         :param part: Unchecked part to add to list of parts.
107         :param prefixed: Whether to add prefix when adding.
108         :type part: object
109         :type prefixed: object
110         :returns: The converted part without prefix, empty means not added.
111         :rtype: str
112         """
113         part = u"" if part is None else str(part).strip()
114         if part:
115             prefixed_part = self.prefix + part if prefixed else part
116             self.parts.append(prefixed_part)
117         return part
118
119     def add(self, parameter):
120         """Add parameter if nonempty to the list of parts.
121
122         Parameter object is converted to string and stripped.
123         If parameter converts to empty string, nothing is added.
124         Parameter is prefixed before adding.
125
126         :param parameter: Parameter object, usually a word starting with dash.
127         :type parameter: object
128         :returns: Self, to enable method chaining.
129         :rtype: OptionString
130         """
131         self.check_and_add(parameter, prefixed=True)
132         return self
133
134     def add_if(self, parameter, condition):
135         """Add parameter if nonempty and condition is true to the list of parts.
136
137         If condition truth value is false, nothing is added.
138         Parameter object is converted to string and stripped.
139         If parameter converts to empty string, nothing is added.
140         Parameter is prefixed before adding.
141
142         :param parameter: Parameter object, usually a word starting with dash.
143         :param condition: Do not add if truth value of this is false.
144         :type parameter: object
145         :type condition: object
146         :returns: Self, to enable method chaining.
147         :rtype: OptionString
148         """
149         if condition:
150             self.add(parameter)
151         return self
152
153     def add_with_value(self, parameter, value):
154         """Add parameter, if followed by a value to the list of parts.
155
156         Parameter and value are converted to string and stripped.
157         If parameter or value converts to empty string, nothing is added.
158         If added, parameter (but not value) is prefixed.
159
160         :param parameter: Parameter object, usually a word starting with dash.
161         :param value: Value object. Prefix is never added.
162         :type parameter: object
163         :type value: object
164         :returns: Self, to enable method chaining.
165         :rtype: OptionString
166         """
167         temp = OptionString(prefix=self.prefix)
168         if temp.check_and_add(parameter, prefixed=True):
169             if temp.check_and_add(value, prefixed=False):
170                 self.extend(temp)
171         return self
172
173     def add_equals(self, parameter, value):
174         """Add parameter=value to the list of parts.
175
176         Parameter and value are converted to string and stripped.
177         If parameter or value converts to empty string, nothing is added.
178         If added, parameter (but not value) is prefixed.
179
180         :param parameter: Parameter object, usually a word starting with dash.
181         :param value: Value object. Prefix is never added.
182         :type parameter: object
183         :type value: object
184         :returns: Self, to enable method chaining.
185         :rtype: OptionString
186         """
187         temp = OptionString(prefix=self.prefix)
188         if temp.check_and_add(parameter, prefixed=True):
189             if temp.check_and_add(value, prefixed=False):
190                 self.parts.append(u"=".join(temp.parts))
191         return self
192
193     def add_with_value_if(self, parameter, value, condition):
194         """Add parameter and value if condition is true and nothing is empty.
195
196         If condition truth value is false, nothing is added.
197         Parameter and value are converted to string and stripped.
198         If parameter or value converts to empty string, nothing is added.
199         If added, parameter (but not value) is prefixed.
200
201         :param parameter: Parameter object, usually a word starting with dash.
202         :param value: Value object. Prefix is never added.
203         :param condition: Do not add if truth value of this is false.
204         :type parameter: object
205         :type value: object
206         :type condition: object
207         :returns: Self, to enable method chaining.
208         :rtype: OptionString
209         """
210         if condition:
211             self.add_with_value(parameter, value)
212         return self
213
214     def add_equals_if(self, parameter, value, condition):
215         """Add parameter=value to the list of parts if condition is true.
216
217         If condition truth value is false, nothing is added.
218         Parameter and value are converted to string and stripped.
219         If parameter or value converts to empty string, nothing is added.
220         If added, parameter (but not value) is prefixed.
221
222         :param parameter: Parameter object, usually a word starting with dash.
223         :param value: Value object. Prefix is never added.
224         :param condition: Do not add if truth value of this is false.
225         :type parameter: object
226         :type value: object
227         :type condition: object
228         :returns: Self, to enable method chaining.
229         :rtype: OptionString
230         """
231         if condition:
232             self.add_equals(parameter, value)
233         return self
234
235     def add_with_value_from_dict(self, parameter, key, mapping, default=u""):
236         """Add parameter with value from dict under key, or default.
237
238         If key is missing, default is used as value.
239         Parameter and value are converted to string and stripped.
240         If parameter or value converts to empty string, nothing is added.
241         If added, parameter (but not value) is prefixed.
242
243         :param parameter: The parameter part to add with prefix.
244         :param key: The key to look the value for.
245         :param mapping: Mapping with keys and values to use.
246         :param default: The value to use if key is missing.
247         :type parameter: object
248         :type key: str
249         :type mapping: dict
250         :type default: object
251         :returns: Self, to enable method chaining.
252         :rtype: OptionString
253         """
254         value = mapping.get(key, default)
255         return self.add_with_value(parameter, value)
256
257     def add_equals_from_dict(self, parameter, key, mapping, default=u""):
258         """Add parameter=value to options where value is from dict.
259
260         If key is missing, default is used as value.
261         Parameter and value are converted to string and stripped.
262         If parameter or value converts to empty string, nothing is added.
263         If added, parameter (but not value) is prefixed.
264
265         :param parameter: The parameter part to add with prefix.
266         :param key: The key to look the value for.
267         :param mapping: Mapping with keys and values to use.
268         :param default: The value to use if key is missing.
269         :type parameter: object
270         :type key: str
271         :type mapping: dict
272         :type default: object
273         :returns: Self, to enable method chaining.
274         :rtype: OptionString
275         """
276         value = mapping.get(key, default)
277         return self.add_equals(parameter, value)
278
279     def add_if_from_dict(self, parameter, key, mapping, default=u"False"):
280         """Add parameter based on if the condition in dict is true.
281
282         If key is missing, default is used as condition.
283         If condition truth value is false, nothing is added.
284         Parameter is converted to string and stripped.
285         If parameter converts to empty string, nothing is added.
286         Parameter is prefixed before adding.
287
288         :param parameter: The parameter part to add with prefix.
289         :param key: The key to look the value for.
290         :param mapping: Mapping with keys and values to use.
291         :param default: The value to use if key is missing.
292         :type parameter: object
293         :type key: str
294         :type mapping: dict
295         :type default: object
296         :returns: Self, to enable method chaining.
297         :rtype: OptionString
298         """
299         condition = mapping.get(key, default)
300         return self.add_if(parameter, condition)
301
302     def add_with_value_if_from_dict(
303             self, parameter, value, key, mapping, default=u"False"):
304         """Add parameter and value based on condition in dict.
305
306         If key is missing, default is used as condition.
307         If condition truth value is false, nothing is added.
308         Parameter and value are converted to string and stripped.
309         If parameter or value converts to empty string, nothing is added.
310         If added, parameter (but not value) is prefixed.
311
312         :param parameter: The parameter part to add with prefix.
313         :param value: Value object. Prefix is never added.
314         :param key: The key to look the value for.
315         :param mapping: Mapping with keys and values to use.
316         :param default: The value to use if key is missing.
317         :type parameter: object
318         :type value: object
319         :type key: str
320         :type mapping: dict
321         :type default: object
322         :returns: Self, to enable method chaining.
323         :rtype: OptionString
324         """
325         condition = mapping.get(key, default)
326         return self.add_with_value_if(parameter, value, condition)
327
328     def add_equals_if_from_dict(
329             self, parameter, value, key, mapping, default=u"False"):
330         """Add parameter=value based on condition in dict.
331
332         If key is missing, default is used as condition.
333         If condition truth value is false, nothing is added.
334         Parameter and value are converted to string and stripped.
335         If parameter or value converts to empty string, nothing is added.
336         If added, parameter (but not value) is prefixed.
337
338         :param parameter: The parameter part to add with prefix.
339         :param value: Value object. Prefix is never added.
340         :param key: The key to look the value for.
341         :param mapping: Mapping with keys and values to use.
342         :param default: The value to use if key is missing.
343         :type parameter: object
344         :type value: object
345         :type key: str
346         :type mapping: dict
347         :type default: object
348         :returns: Self, to enable method chaining.
349         :rtype: OptionString
350         """
351         condition = mapping.get(key, default)
352         return self.add_equals_if(parameter, value, condition)
353
354     def __str__(self):
355         """Return space separated string of nonempty parts.
356
357         The format is suitable to be pasted as (part of) command line.
358         Do not call str() prematurely just to get a substring, consider
359         converting the surrounding text manipulation to OptionString as well.
360
361         :returns: Space separated string of options.
362         :rtype: str
363         """
364         return u" ".join(self.parts)