ConfigUpdater 4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,417 @@
1
+ """This module focus in the top level data layer API of ConfigUpdater, i.e.
2
+ how to access and modify the sections of the configurations.
3
+
4
+ Differently from :mod:`configparser`, the different aspects of the ConfigUpdater API are
5
+ split between several modules.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Iterable, Iterator, MutableMapping
11
+ from configparser import (
12
+ ConfigParser,
13
+ DuplicateSectionError,
14
+ NoOptionError,
15
+ NoSectionError,
16
+ )
17
+ from enum import Enum
18
+ from typing import Any, Self, TypeVar, overload
19
+
20
+ from .block import Comment, Space
21
+ from .container import Container
22
+ from .option import Option
23
+ from .section import Section
24
+
25
+ # Used in parser getters to indicate the default behaviour when a specific
26
+ # option is not found it to raise an exception. Created to enable 'None' as
27
+ # a valid fallback value.
28
+ _UniqueValues = Enum("_UniqueValues", "_UNSET")
29
+ _UNSET = _UniqueValues._UNSET
30
+
31
+ T = TypeVar("T")
32
+
33
+ ConfigContent = Section | Comment | Space
34
+
35
+
36
+ class Document(Container[ConfigContent], MutableMapping[str, Section]):
37
+ """Access to the data manipulation API from **ConfigUpdater**.
38
+
39
+ A ``Document`` object tries to implement a familiar *dict-like* interface,
40
+ via :class:`MutableMapping`. However, it also tries to be as compatible as possible
41
+ with the stdlib's :class:`~configparser.ConfigParser`. This means that there are a
42
+ few methods that will work differently from what users familiar with *dict-like*
43
+ interfaces would expect. The most notable example is :meth:`get`.
44
+
45
+ A important difference between ConfigUpdater's ``Document`` model and
46
+ :class:`~configparser.ConfigParser` is the behaviour of the :class:`Section`
47
+ objects.
48
+ If we represent the type of a *dict-like* (or :class:`MutableMapping`) object by
49
+ ``M[K, V]``, where ``K`` is the type of its keys and ``V`` is the type of the
50
+ associated values, ConfigUpdater's sections would be equivalent to
51
+ ``M[str, Option]``, while ConfigParser's would be ``M[str, str]``.
52
+
53
+ This means that when you try to access a key inside a section in ConfigUpdater, you
54
+ are going to receive a :class:`Option` object, not its value.
55
+ To access the value of the option you need to call :class:`Option.value`.
56
+ """
57
+
58
+ def _get_section_idx(self, name: str) -> int:
59
+ return next(
60
+ i
61
+ for i, entry in enumerate(self._structure)
62
+ if isinstance(entry, Section) and entry.name == name
63
+ )
64
+
65
+ def optionxform(self, optionstr: str) -> str:
66
+ """Converts an option key for unification
67
+
68
+ By default it uses :meth:`str.lower`, which means that ConfigUpdater will
69
+ compare options in a case insensitive way.
70
+
71
+ This implementation mimics ConfigParser API, and can be configured as described
72
+ in :meth:`configparser.ConfigParser.optionxform`.
73
+
74
+ Warning:
75
+ As in ConfigParser, a custom ``optionxform`` must be idempotent, i.e.
76
+ ``optionxform(optionxform(key)) == optionxform(key)``. The keys that
77
+ ConfigUpdater returns, e.g. when iterating over a section, are already
78
+ transformed and are transformed again when they are used for a lookup.
79
+ :meth:`str.lower`, :meth:`str.upper` and :class:`str` (to keep keys
80
+ case-sensitive) satisfy this, while something like
81
+ ``lambda key: "prefix_" + key`` does not.
82
+
83
+ Args:
84
+ optionstr (str): key name
85
+
86
+ Returns:
87
+ str: unified option name
88
+ """
89
+ return optionstr.lower()
90
+
91
+ def validate_format(self, **kwargs: Any) -> bool:
92
+ """Call ConfigParser to validate config
93
+
94
+ Args:
95
+ kwargs: are passed to :class:`configparser.ConfigParser`
96
+
97
+ Raises:
98
+ configparser.ParsingError: if syntax errors are found
99
+
100
+ Returns:
101
+ ``True`` when no error is found
102
+ """
103
+ kwargs.pop("space_around_delimiters", None)
104
+ parser = ConfigParser(**kwargs)
105
+ parser.read_string(str(self))
106
+ return True
107
+
108
+ def iter_sections(self) -> Iterator[Section]:
109
+ """Iterate only over section blocks"""
110
+ return (block for block in self._structure if isinstance(block, Section))
111
+
112
+ def section_blocks(self) -> list[Section]:
113
+ """Returns all section blocks
114
+
115
+ Returns:
116
+ list: list of :class:`Section` blocks
117
+ """
118
+ return list(self.iter_sections())
119
+
120
+ def sections(self) -> list[str]:
121
+ """Return a list of section names
122
+
123
+ Returns:
124
+ list: list of section names
125
+ """
126
+ return [section.name for section in self.iter_sections()]
127
+
128
+ def __iter__(self) -> Iterator[str]:
129
+ return (b.name for b in self.iter_blocks() if isinstance(b, Section))
130
+
131
+ def __str__(self) -> str:
132
+ return "".join(str(block) for block in self._structure)
133
+
134
+ def __getitem__(self, key: str) -> Section:
135
+ for section in self.section_blocks():
136
+ if section.name == key:
137
+ return section
138
+
139
+ raise KeyError(f"No section `{key}` found", {"key": key})
140
+
141
+ def __setitem__(self, key: str, value: Section) -> None:
142
+ if not isinstance(value, Section):
143
+ raise ValueError("Value must be of type Section!")
144
+ if key in self:
145
+ idx = self._get_section_idx(key)
146
+ self.__getitem__(key).detach()
147
+ value.attach(self)
148
+ self._structure.insert(idx, value)
149
+ else:
150
+ if value.name != key:
151
+ raise ValueError(
152
+ f"Set key `{key}` does not equal option key `{value.name}`"
153
+ )
154
+ self.add_section(value)
155
+
156
+ def __delitem__(self, key: str) -> None:
157
+ if not self.remove_section(key):
158
+ raise KeyError(f"No section `{key}` found", {"key": key})
159
+
160
+ # MutableMapping[str, Section] accepts key: object, which is fine here since
161
+ # the key is only compared with the section names
162
+ def __contains__(self, key: object) -> bool:
163
+ """Returns whether the given section exists.
164
+
165
+ Args:
166
+ key (str): name of section
167
+
168
+ Returns:
169
+ bool: whether the section exists
170
+ """
171
+ return next((True for s in self.iter_sections() if s.name == key), False)
172
+
173
+ has_section = __contains__
174
+
175
+ def __eq__(self, other: object) -> bool:
176
+ if isinstance(other, self.__class__):
177
+ return self._structure == other._structure
178
+ else:
179
+ return False
180
+
181
+ def clear(self) -> None:
182
+ for block in self._structure:
183
+ block.detach()
184
+ self._structure.clear()
185
+
186
+ def add_section(self, section: str | Section) -> None:
187
+ """Create a new section in the configuration.
188
+
189
+ Raise DuplicateSectionError if a section by the specified name
190
+ already exists. Raise ValueError if name is DEFAULT.
191
+
192
+ Args:
193
+ section (str or :class:`Section`): name or Section type
194
+ """
195
+ if isinstance(section, str):
196
+ # create a new section
197
+ section_obj = Section(section)
198
+ elif isinstance(section, Section):
199
+ section_obj = section
200
+ else:
201
+ raise ValueError("Parameter must be a string or Section type!")
202
+
203
+ if self.has_section(section_obj.name):
204
+ raise DuplicateSectionError(section_obj.name)
205
+
206
+ # Ensure a newline before the new section if the last block
207
+ # doesn't end with one (e.g. file without trailing newline)
208
+ if self._structure and not str(self._structure[-1]).endswith("\n"):
209
+ space = Space(container=self)
210
+ space.add_line("\n")
211
+ self._structure.append(space)
212
+
213
+ section_obj.attach(self)
214
+ self._structure.append(section_obj)
215
+
216
+ def options(self, section: str) -> list[str]:
217
+ """Returns list of configuration options for the named section.
218
+
219
+ Args:
220
+ section (str): name of section
221
+
222
+ Returns:
223
+ list: list of option names
224
+ """
225
+ if not self.has_section(section):
226
+ raise NoSectionError(section) from None
227
+ return self.__getitem__(section).options()
228
+
229
+ # The following is a pragmatic violation of Liskov substitution principle:
230
+ # As dicts, Mappings should have get(self, key: str, default: T) -> T
231
+ # but ConfigParser overwrites it and uses the function to offer a different
232
+ # functionality
233
+ @overload # type: ignore[override]
234
+ def get(self, section: str, option: str) -> Option: ...
235
+
236
+ @overload
237
+ def get(self, section: str, option: str, fallback: T) -> Option | T: ...
238
+
239
+ def get(
240
+ self, section: str, option: str, fallback: T | _UniqueValues = _UNSET
241
+ ) -> Option | T:
242
+ """Gets an option object for a given section or a fallback value.
243
+
244
+ Warning:
245
+ Please notice this method works differently from what is expected of
246
+ :meth:`MutableMapping.get` (or :meth:`dict.get`).
247
+ Similarly to :meth:`configparser.ConfigParser.get`, will take least 2
248
+ arguments, and the second argument does not correspond to a default value.
249
+
250
+ This happens because this function is not designed to return a
251
+ :obj:`Section` of the :obj:`ConfigUpdater` document, but instead a nested
252
+ :obj:`Option`.
253
+
254
+ See :meth:`get_section`, if instead, you want to retrieve a :obj:`Section`.
255
+
256
+ Args:
257
+ section (str): section name
258
+ option (str): option name
259
+ fallback (T): if the key is not found and fallback is provided, the
260
+ ``fallback`` value will be returned. ``None`` is a valid fallback value.
261
+
262
+ .. attention::
263
+ When ``option`` is not present, the ``fallback`` value itself is
264
+ returned. If you want instead to obtain a new ``Option`` object
265
+ with a default value associated with it, you can try the following::
266
+
267
+ configupdater.get("section", "option", fallback=Option("name", value))
268
+
269
+ ... which roughly corresponds to::
270
+
271
+ configupdater["section"].get("option", Option("name", value))
272
+
273
+
274
+ Raises:
275
+ :class:`NoSectionError`: if ``section`` cannot be found
276
+ :class:`NoOptionError`: if the option cannot be found and no ``fallback``
277
+ was given
278
+
279
+ Returns:
280
+ :class:`Option` object holding key/value pair when it exists. Otherwise,
281
+ the value passed via the ``fallback`` argument itself (type ``T``).
282
+ """
283
+ section_obj = self.get_section(section, _UNSET)
284
+ if section_obj is _UNSET:
285
+ raise NoSectionError(section) from None
286
+
287
+ value = section_obj.get(option, fallback)
288
+ # ^ we checked section_obj against _UNSET, so we are sure about its type
289
+
290
+ if value is _UNSET:
291
+ raise NoOptionError(self.optionxform(option), section)
292
+
293
+ return value
294
+
295
+ @overload
296
+ def get_section(self, name: str) -> Section | None: ...
297
+
298
+ @overload
299
+ def get_section(self, name: str, default: T) -> Section | T: ...
300
+
301
+ def get_section(self, name: str, default: T | None = None) -> Section | T | None:
302
+ """This method works similarly to :meth:`dict.get`, and allows you
303
+ to retrieve an entire section by its name, or provide a ``default`` value in
304
+ case it cannot be found.
305
+ """
306
+ return next((s for s in self.iter_sections() if s.name == name), default)
307
+
308
+ # The following is a pragmatic violation of Liskov substitution principle
309
+ # For some reason MutableMapping.items return a Set-like object
310
+ # but we want to preserve ordering
311
+ @overload # type: ignore[override]
312
+ def items(self) -> list[tuple[str, Section]]: ...
313
+
314
+ @overload
315
+ def items(self, section: str) -> list[tuple[str, Option]]: ...
316
+
317
+ def items(
318
+ self, section: str | _UniqueValues = _UNSET
319
+ ) -> list[tuple[str, Section]] | list[tuple[str, Option]]:
320
+ """Return a list of (name, value) tuples for options or sections.
321
+
322
+ If section is given, return a list of tuples with (name, value) for
323
+ each option in the section. Otherwise, return a list of tuples with
324
+ (section_name, section_type) for each section.
325
+
326
+ Args:
327
+ section (str): optional section name, default UNSET
328
+
329
+ Returns:
330
+ list: list of :class:`Section` or :class:`Option` objects
331
+ """
332
+ if section is _UNSET:
333
+ return [(sect.name, sect) for sect in self.iter_sections()]
334
+
335
+ section_obj = self.__getitem__(section)
336
+ return [(opt.key, opt) for opt in section_obj.iter_options()]
337
+
338
+ def has_option(self, section: str, option: str) -> bool:
339
+ """Checks for the existence of a given option in a given section.
340
+
341
+ Args:
342
+ section (str): name of section
343
+ option (str): name of option
344
+
345
+ Returns:
346
+ bool: whether the option exists in the given section
347
+ """
348
+ return option in self.get_section(section, {})
349
+
350
+ def set(
351
+ self,
352
+ section: str,
353
+ option: str,
354
+ value: str | Iterable[str] | None = None,
355
+ ) -> Self:
356
+ """Set an option.
357
+
358
+ A new option keeps the given ``option`` name as
359
+ :attr:`~configupdater.Option.raw_key`, see :meth:`~configupdater.Section.set`.
360
+ Unlike :meth:`configparser.ConfigParser.set`, which writes the key transformed
361
+ by ``optionxform`` (lowercase by default), the file gets the given spelling.
362
+
363
+ Args:
364
+ section: section name
365
+ option: option name
366
+ value: value, default None
367
+ """
368
+ try:
369
+ section_obj = self.__getitem__(section)
370
+ except KeyError:
371
+ raise NoSectionError(section) from None
372
+ section_obj.set(option, value)
373
+ return self
374
+
375
+ def remove_option(self, section: str, option: str) -> bool:
376
+ """Remove an option.
377
+
378
+ Args:
379
+ section (str): section name
380
+ option (str): option name
381
+
382
+ Returns:
383
+ bool: whether the option was actually removed
384
+ """
385
+ try:
386
+ section_obj = self.__getitem__(section)
387
+ except KeyError:
388
+ raise NoSectionError(section) from None
389
+ try:
390
+ del section_obj[option]
391
+ except KeyError:
392
+ return False
393
+ return True
394
+
395
+ def remove_section(self, name: str) -> bool:
396
+ """Remove a file section.
397
+
398
+ Args:
399
+ name: name of the section
400
+
401
+ Returns:
402
+ bool: whether the section was actually removed
403
+ """
404
+ try:
405
+ idx = self._get_section_idx(name)
406
+ del self._structure[idx]
407
+ return True
408
+ except StopIteration:
409
+ return False
410
+
411
+ def to_dict(self) -> dict[str, dict[str, str | None]]:
412
+ """Transform to dictionary
413
+
414
+ Returns:
415
+ dict: dictionary with same content
416
+ """
417
+ return {sect.name: sect.to_dict() for sect in self.iter_sections()}
@@ -0,0 +1,263 @@
1
+ """Options are the ultimate mean of configuration inside a configuration value.
2
+
3
+ They are always associated with a :attr:`~Option.key` (or the name of the configuration
4
+ parameter) and a :attr:`~Option.value`.
5
+
6
+ Options can also have multi-line values that are usually interpreted as a list of
7
+ values.
8
+
9
+ When editing configuration files with ConfigUpdater, a handy way of setting a multi-line
10
+ (or comma separated value) for an specific option is to use the
11
+ :meth:`~Option.set_values` method.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import warnings
17
+ from collections.abc import Iterable
18
+ from typing import TYPE_CHECKING, Any, Self, cast
19
+
20
+ if TYPE_CHECKING:
21
+ from .document import Document
22
+ from .section import Section
23
+
24
+ from .block import AssignMultilineValueError, Block
25
+
26
+
27
+ def is_multi_line(value: Any) -> bool:
28
+ """Checks if a given value has multiple lines"""
29
+ if isinstance(value, str):
30
+ return "\n" in value
31
+ else:
32
+ return False
33
+
34
+
35
+ class NoneValueDisallowed(SyntaxWarning):
36
+ """Warns that an option without a value is omitted from the output.
37
+
38
+ This happens when such an option is converted to a string while the
39
+ :class:`~configupdater.ConfigUpdater` does not use ``allow_no_value=True``.
40
+ """
41
+
42
+ @classmethod
43
+ def warn(cls, option: str) -> None:
44
+ msg = (
45
+ f"Cannot represent <{option} = None>, the option is omitted. "
46
+ "Please use ``allow_no_value=True`` with ``ConfigUpdater``."
47
+ )
48
+ warnings.warn(msg, cls, stacklevel=2)
49
+
50
+
51
+ class Option(Block):
52
+ """Option block holding a key/value pair.
53
+
54
+ ``space_around_delimiters`` decides whether the delimiter is surrounded by spaces
55
+ when the option is rendered after a change. ``True`` and ``False`` take precedence
56
+ over the syntax options of the :class:`~configupdater.ConfigUpdater` holding the
57
+ option. ``None`` (the default) follows its ``space_around_delimiters`` and adds
58
+ spaces if the option does not belong to a :class:`~configupdater.ConfigUpdater`.
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ key: str,
64
+ value: str | None = None,
65
+ container: Section | None = None,
66
+ delimiter: str = "=",
67
+ space_around_delimiters: bool | None = None,
68
+ line: str | None = None,
69
+ ):
70
+ super().__init__(container=container)
71
+ self._key = key
72
+ self._values: list[str | None] = [] if value is None else [value]
73
+ self._value_is_none = value is None
74
+ self._delimiter = delimiter
75
+ self._value: str | None = None # will be filled after join_multiline_value
76
+ self._updated = False
77
+ self._multiline_value_joined = False
78
+ self._space_around_delimiters = space_around_delimiters
79
+ if line:
80
+ super().add_line(line)
81
+ if value is not None:
82
+ self._set_value(value)
83
+
84
+ def add_value(self, value: str | None) -> None:
85
+ """PRIVATE: this function is not part of the public API of Option.
86
+ It is only used internally by other classes of the package during parsing.
87
+ """
88
+ self._value_is_none = value is None
89
+ self._values.append(value)
90
+
91
+ def add_line(self, line: str) -> Self:
92
+ """PRIVATE: this function is not part of the public API of Option.
93
+ It is only used internally by other classes of the package during parsing.
94
+ """
95
+ super().add_line(line)
96
+ self.add_value(line.strip())
97
+ return self
98
+
99
+ def _join_multiline_value(self) -> None:
100
+ if not self._multiline_value_joined and not self._value_is_none:
101
+ # do what `_join_multiline_value` in ConfigParser would do
102
+ self._value = "\n".join(cast(list[str], self._values)).rstrip()
103
+ self._multiline_value_joined = True
104
+
105
+ def _get_delim(self, determine_suffix: bool = True) -> str:
106
+ document = self._document()
107
+ opts = getattr(document, "syntax_options", None) or {}
108
+ value = self._value
109
+ space = self._space_around_delimiters
110
+ if space is None:
111
+ space = opts.get("space_around_delimiters", True)
112
+ # no space is needed if we use multi-line arguments
113
+ suffix = "" if determine_suffix and str(value).startswith("\n") else " "
114
+ delim = f" {self._delimiter}{suffix}" if space else self._delimiter
115
+ return delim
116
+
117
+ def value_start_idx(self) -> int:
118
+ """Index where the value of the option starts, good for indentation"""
119
+ delim = self._get_delim(determine_suffix=False)
120
+ return len(f"{self._key}{delim}")
121
+
122
+ def __str__(self) -> str:
123
+ if not self.updated:
124
+ return super().__str__()
125
+
126
+ document = self._document()
127
+ opts = getattr(document, "syntax_options", None) or {}
128
+ value = self._value
129
+
130
+ if value is None:
131
+ if document is None or opts.get("allow_no_value"):
132
+ return f"{self._key}\n"
133
+ NoneValueDisallowed.warn(self._key)
134
+ return ""
135
+
136
+ delim = self._get_delim()
137
+ return f"{self._key}{delim}{value}\n"
138
+
139
+ def __repr__(self) -> str:
140
+ return f"<Option: {self._key} = {self.value!r}>"
141
+
142
+ def _instantiate_copy(self) -> Self:
143
+ """Will be called by :meth:`Block.__deepcopy__`"""
144
+ self._join_multiline_value()
145
+ return self.__class__(
146
+ self._key,
147
+ self._value,
148
+ container=None,
149
+ delimiter=self._delimiter,
150
+ space_around_delimiters=self._space_around_delimiters,
151
+ )
152
+
153
+ def _document(self) -> Document | None:
154
+ if self._container is None:
155
+ return None
156
+ return cast("Section", self._container)._container
157
+
158
+ @property
159
+ def section(self) -> Section:
160
+ return cast("Section", self.container)
161
+
162
+ @property
163
+ def key(self) -> str:
164
+ """Key string associated with the option.
165
+
166
+ Please notice that the option key is normalized with
167
+ :meth:`~configupdater.document.Document.optionxform`.
168
+
169
+ When the option object is :obj:`detached <configupdater.block.Block.detach>`,
170
+ this method will raise a :obj:`NotAttachedError`.
171
+ """
172
+ return self.section.document.optionxform(self._key)
173
+
174
+ @key.setter
175
+ def key(self, value: str) -> None:
176
+ self._join_multiline_value()
177
+ self._key = value
178
+ self._updated = True
179
+
180
+ @property
181
+ def raw_key(self) -> str:
182
+ """Equivalent to :obj:`key`, but before applying :meth:`optionxform`."""
183
+ return self._key
184
+
185
+ @property
186
+ def value(self) -> str | None:
187
+ """Value associated with the given option."""
188
+ self._join_multiline_value()
189
+ return self._value
190
+
191
+ @value.setter
192
+ def value(self, value: str | None) -> None:
193
+ if is_multi_line(value):
194
+ raise AssignMultilineValueError(self)
195
+ self._set_value(value)
196
+
197
+ def _set_value(self, value: str | None) -> None:
198
+ self._updated = True
199
+ self._multiline_value_joined = True
200
+ self._value = value
201
+ self._values = [value]
202
+
203
+ def as_list(self, separator: str = "\n") -> list[str]:
204
+ """Returns the (multi-line/element) value as a list
205
+
206
+ Empty list if value is None, single-element list for a one-element
207
+ value and an element for each line in a multi-element value.
208
+
209
+ Args:
210
+ separator (str): separator for values, default: line separator
211
+ """
212
+ if self._value_is_none:
213
+ return []
214
+ else:
215
+ return [v.strip() for v in cast(str, self.value).strip().split(separator)]
216
+
217
+ def append(self, value: str, **kwargs: Any) -> Option:
218
+ """Append a value to a mult-line value
219
+
220
+ Args:
221
+ value (str): value
222
+ kwargs: keyword arguments for `set_values`
223
+ """
224
+ sep = kwargs.get("separator")
225
+ new_values = self.as_list() if sep is None else self.as_list(separator=sep)
226
+
227
+ new_values.append(value)
228
+ self.set_values(new_values, **kwargs)
229
+ return self
230
+
231
+ def set_values(
232
+ self,
233
+ values: Iterable[str],
234
+ separator: str = "\n",
235
+ indent: str | None = None,
236
+ prepend_newline: bool = True,
237
+ ) -> None:
238
+ """Sets the value to a given list of options, e.g. multi-line values
239
+
240
+ Args:
241
+ values (iterable): sequence of values
242
+ separator (str): separator for values, default: line separator
243
+ indent (optional str): indentation in case of line separator.
244
+ If prepend_newline is `True` 4 whitespaces by default, otherwise
245
+ determine automatically if `None`.
246
+ prepend_newline (bool): start with a new line or not, resp.
247
+ """
248
+ values = list(values).copy()
249
+ self._updated = True
250
+ self._multiline_value_joined = True
251
+ self._values = cast(list[str | None], values)
252
+
253
+ if indent is None:
254
+ indent = 4 * " " if prepend_newline else self.value_start_idx() * " "
255
+
256
+ # The most common case of multiline values being preceded by a new line
257
+ if prepend_newline and "\n" in separator:
258
+ values = ["", *values]
259
+ separator = separator + indent
260
+ elif "\n" in separator:
261
+ separator = separator + indent
262
+
263
+ self._value = separator.join(values)