diff options
Diffstat (limited to 'venv/lib/python3.9/site-packages/git/config.py')
-rw-r--r-- | venv/lib/python3.9/site-packages/git/config.py | 897 |
1 files changed, 897 insertions, 0 deletions
diff --git a/venv/lib/python3.9/site-packages/git/config.py b/venv/lib/python3.9/site-packages/git/config.py new file mode 100644 index 00000000..e05a297a --- /dev/null +++ b/venv/lib/python3.9/site-packages/git/config.py @@ -0,0 +1,897 @@ +# config.py +# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors +# +# This module is part of GitPython and is released under +# the BSD License: http://www.opensource.org/licenses/bsd-license.php +"""Module containing module parser implementation able to properly read and write +configuration files""" + +import sys +import abc +from functools import wraps +import inspect +from io import BufferedReader, IOBase +import logging +import os +import re +import fnmatch + +from git.compat import ( + defenc, + force_text, + is_win, +) + +from git.util import LockFile + +import os.path as osp + +import configparser as cp + +# typing------------------------------------------------------- + +from typing import ( + Any, + Callable, + Generic, + IO, + List, + Dict, + Sequence, + TYPE_CHECKING, + Tuple, + TypeVar, + Union, + cast, +) + +from git.types import Lit_config_levels, ConfigLevels_Tup, PathLike, assert_never, _T + +if TYPE_CHECKING: + from git.repo.base import Repo + from io import BytesIO + +T_ConfigParser = TypeVar("T_ConfigParser", bound="GitConfigParser") +T_OMD_value = TypeVar("T_OMD_value", str, bytes, int, float, bool) + +if sys.version_info[:3] < (3, 7, 2): + # typing.Ordereddict not added until py 3.7.2 + from collections import OrderedDict + + OrderedDict_OMD = OrderedDict +else: + from typing import OrderedDict + + OrderedDict_OMD = OrderedDict[str, List[T_OMD_value]] # type: ignore[assignment, misc] + +# ------------------------------------------------------------- + +__all__ = ("GitConfigParser", "SectionConstraint") + + +log = logging.getLogger("git.config") +log.addHandler(logging.NullHandler()) + +# invariants +# represents the configuration level of a configuration file + + +CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository") + + +# Section pattern to detect conditional includes. +# https://git-scm.com/docs/git-config#_conditional_includes +CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeIf )\"(gitdir|gitdir/i|onbranch):(.+)\"") + + +class MetaParserBuilder(abc.ABCMeta): # noqa: B024 + """Utility class wrapping base-class methods into decorators that assure read-only properties""" + + def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> "MetaParserBuilder": + """ + Equip all base-class methods with a needs_values decorator, and all non-const methods + with a set_dirty_and_flush_changes decorator in addition to that.""" + kmm = "_mutating_methods_" + if kmm in clsdict: + mutating_methods = clsdict[kmm] + for base in bases: + methods = (t for t in inspect.getmembers(base, inspect.isroutine) if not t[0].startswith("_")) + for name, method in methods: + if name in clsdict: + continue + method_with_values = needs_values(method) + if name in mutating_methods: + method_with_values = set_dirty_and_flush_changes(method_with_values) + # END mutating methods handling + + clsdict[name] = method_with_values + # END for each name/method pair + # END for each base + # END if mutating methods configuration is set + + new_type = super(MetaParserBuilder, cls).__new__(cls, name, bases, clsdict) + return new_type + + +def needs_values(func: Callable[..., _T]) -> Callable[..., _T]: + """Returns method assuring we read values (on demand) before we try to access them""" + + @wraps(func) + def assure_data_present(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T: + self.read() + return func(self, *args, **kwargs) + + # END wrapper method + return assure_data_present + + +def set_dirty_and_flush_changes(non_const_func: Callable[..., _T]) -> Callable[..., _T]: + """Return method that checks whether given non constant function may be called. + If so, the instance will be set dirty. + Additionally, we flush the changes right to disk""" + + def flush_changes(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T: + rval = non_const_func(self, *args, **kwargs) + self._dirty = True + self.write() + return rval + + # END wrapper method + flush_changes.__name__ = non_const_func.__name__ + return flush_changes + + +class SectionConstraint(Generic[T_ConfigParser]): + + """Constrains a ConfigParser to only option commands which are constrained to + always use the section we have been initialized with. + + It supports all ConfigParser methods that operate on an option. + + :note: + If used as a context manager, will release the wrapped ConfigParser.""" + + __slots__ = ("_config", "_section_name") + _valid_attrs_ = ( + "get_value", + "set_value", + "get", + "set", + "getint", + "getfloat", + "getboolean", + "has_option", + "remove_section", + "remove_option", + "options", + ) + + def __init__(self, config: T_ConfigParser, section: str) -> None: + self._config = config + self._section_name = section + + def __del__(self) -> None: + # Yes, for some reason, we have to call it explicitly for it to work in PY3 ! + # Apparently __del__ doesn't get call anymore if refcount becomes 0 + # Ridiculous ... . + self._config.release() + + def __getattr__(self, attr: str) -> Any: + if attr in self._valid_attrs_: + return lambda *args, **kwargs: self._call_config(attr, *args, **kwargs) + return super(SectionConstraint, self).__getattribute__(attr) + + def _call_config(self, method: str, *args: Any, **kwargs: Any) -> Any: + """Call the configuration at the given method which must take a section name + as first argument""" + return getattr(self._config, method)(self._section_name, *args, **kwargs) + + @property + def config(self) -> T_ConfigParser: + """return: Configparser instance we constrain""" + return self._config + + def release(self) -> None: + """Equivalent to GitConfigParser.release(), which is called on our underlying parser instance""" + return self._config.release() + + def __enter__(self) -> "SectionConstraint[T_ConfigParser]": + self._config.__enter__() + return self + + def __exit__(self, exception_type: str, exception_value: str, traceback: str) -> None: + self._config.__exit__(exception_type, exception_value, traceback) + + +class _OMD(OrderedDict_OMD): + """Ordered multi-dict.""" + + def __setitem__(self, key: str, value: _T) -> None: + super(_OMD, self).__setitem__(key, [value]) + + def add(self, key: str, value: Any) -> None: + if key not in self: + super(_OMD, self).__setitem__(key, [value]) + return None + super(_OMD, self).__getitem__(key).append(value) + + def setall(self, key: str, values: List[_T]) -> None: + super(_OMD, self).__setitem__(key, values) + + def __getitem__(self, key: str) -> Any: + return super(_OMD, self).__getitem__(key)[-1] + + def getlast(self, key: str) -> Any: + return super(_OMD, self).__getitem__(key)[-1] + + def setlast(self, key: str, value: Any) -> None: + if key not in self: + super(_OMD, self).__setitem__(key, [value]) + return + + prior = super(_OMD, self).__getitem__(key) + prior[-1] = value + + def get(self, key: str, default: Union[_T, None] = None) -> Union[_T, None]: + return super(_OMD, self).get(key, [default])[-1] + + def getall(self, key: str) -> List[_T]: + return super(_OMD, self).__getitem__(key) + + def items(self) -> List[Tuple[str, _T]]: # type: ignore[override] + """List of (key, last value for key).""" + return [(k, self[k]) for k in self] + + def items_all(self) -> List[Tuple[str, List[_T]]]: + """List of (key, list of values for key).""" + return [(k, self.getall(k)) for k in self] + + +def get_config_path(config_level: Lit_config_levels) -> str: + + # we do not support an absolute path of the gitconfig on windows , + # use the global config instead + if is_win and config_level == "system": + config_level = "global" + + if config_level == "system": + return "/etc/gitconfig" + elif config_level == "user": + config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", "~"), ".config") + return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config"))) + elif config_level == "global": + return osp.normpath(osp.expanduser("~/.gitconfig")) + elif config_level == "repository": + raise ValueError("No repo to get repository configuration from. Use Repo._get_config_path") + else: + # Should not reach here. Will raise ValueError if does. Static typing will warn missing elifs + assert_never( + config_level, # type: ignore[unreachable] + ValueError(f"Invalid configuration level: {config_level!r}"), + ) + + +class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): + + """Implements specifics required to read git style configuration files. + + This variation behaves much like the git.config command such that the configuration + will be read on demand based on the filepath given during initialization. + + The changes will automatically be written once the instance goes out of scope, but + can be triggered manually as well. + + The configuration file will be locked if you intend to change values preventing other + instances to write concurrently. + + :note: + The config is case-sensitive even when queried, hence section and option names + must match perfectly. + If used as a context manager, will release the locked file.""" + + # { Configuration + # The lock type determines the type of lock to use in new configuration readers. + # They must be compatible to the LockFile interface. + # A suitable alternative would be the BlockingLockFile + t_lock = LockFile + re_comment = re.compile(r"^\s*[#;]") + + # } END configuration + + optvalueonly_source = r"\s*(?P<option>[^:=\s][^:=]*)" + + OPTVALUEONLY = re.compile(optvalueonly_source) + + OPTCRE = re.compile(optvalueonly_source + r"\s*(?P<vi>[:=])\s*" + r"(?P<value>.*)$") + + del optvalueonly_source + + # list of RawConfigParser methods able to change the instance + _mutating_methods_ = ("add_section", "remove_section", "remove_option", "set") + + def __init__( + self, + file_or_files: Union[None, PathLike, "BytesIO", Sequence[Union[PathLike, "BytesIO"]]] = None, + read_only: bool = True, + merge_includes: bool = True, + config_level: Union[Lit_config_levels, None] = None, + repo: Union["Repo", None] = None, + ) -> None: + """Initialize a configuration reader to read the given file_or_files and to + possibly allow changes to it by setting read_only False + + :param file_or_files: + A single file path or file objects or multiple of these + + :param read_only: + If True, the ConfigParser may only read the data , but not change it. + If False, only a single file path or file object may be given. We will write back the changes + when they happen, or when the ConfigParser is released. This will not happen if other + configuration files have been included + :param merge_includes: if True, we will read files mentioned in [include] sections and merge their + contents into ours. This makes it impossible to write back an individual configuration file. + Thus, if you want to modify a single configuration file, turn this off to leave the original + dataset unaltered when reading it. + :param repo: Reference to repository to use if [includeIf] sections are found in configuration files. + + """ + cp.RawConfigParser.__init__(self, dict_type=_OMD) + self._dict: Callable[..., _OMD] # type: ignore # mypy/typeshed bug? + self._defaults: _OMD + self._sections: _OMD # type: ignore # mypy/typeshed bug? + + # Used in python 3, needs to stay in sync with sections for underlying implementation to work + if not hasattr(self, "_proxies"): + self._proxies = self._dict() + + if file_or_files is not None: + self._file_or_files: Union[PathLike, "BytesIO", Sequence[Union[PathLike, "BytesIO"]]] = file_or_files + else: + if config_level is None: + if read_only: + self._file_or_files = [ + get_config_path(cast(Lit_config_levels, f)) for f in CONFIG_LEVELS if f != "repository" + ] + else: + raise ValueError("No configuration level or configuration files specified") + else: + self._file_or_files = [get_config_path(config_level)] + + self._read_only = read_only + self._dirty = False + self._is_initialized = False + self._merge_includes = merge_includes + self._repo = repo + self._lock: Union["LockFile", None] = None + self._acquire_lock() + + def _acquire_lock(self) -> None: + if not self._read_only: + if not self._lock: + if isinstance(self._file_or_files, (str, os.PathLike)): + file_or_files = self._file_or_files + elif isinstance(self._file_or_files, (tuple, list, Sequence)): + raise ValueError( + "Write-ConfigParsers can operate on a single file only, multiple files have been passed" + ) + else: + file_or_files = self._file_or_files.name + + # END get filename from handle/stream + # initialize lock base - we want to write + self._lock = self.t_lock(file_or_files) + # END lock check + + self._lock._obtain_lock() + # END read-only check + + def __del__(self) -> None: + """Write pending changes if required and release locks""" + # NOTE: only consistent in PY2 + self.release() + + def __enter__(self) -> "GitConfigParser": + self._acquire_lock() + return self + + def __exit__(self, *args: Any) -> None: + self.release() + + def release(self) -> None: + """Flush changes and release the configuration write lock. This instance must not be used anymore afterwards. + In Python 3, it's required to explicitly release locks and flush changes, as __del__ is not called + deterministically anymore.""" + # checking for the lock here makes sure we do not raise during write() + # in case an invalid parser was created who could not get a lock + if self.read_only or (self._lock and not self._lock._has_lock()): + return + + try: + try: + self.write() + except IOError: + log.error("Exception during destruction of GitConfigParser", exc_info=True) + except ReferenceError: + # This happens in PY3 ... and usually means that some state cannot be written + # as the sections dict cannot be iterated + # Usually when shutting down the interpreter, don'y know how to fix this + pass + finally: + if self._lock is not None: + self._lock._release_lock() + + def optionxform(self, optionstr: str) -> str: + """Do not transform options in any way when writing""" + return optionstr + + def _read(self, fp: Union[BufferedReader, IO[bytes]], fpname: str) -> None: + """A direct copy of the py2.4 version of the super class's _read method + to assure it uses ordered dicts. Had to change one line to make it work. + + Future versions have this fixed, but in fact its quite embarrassing for the + guys not to have done it right in the first place ! + + Removed big comments to make it more compact. + + Made sure it ignores initial whitespace as git uses tabs""" + cursect = None # None, or a dictionary + optname = None + lineno = 0 + is_multi_line = False + e = None # None, or an exception + + def string_decode(v: str) -> str: + if v[-1] == "\\": + v = v[:-1] + # end cut trailing escapes to prevent decode error + + return v.encode(defenc).decode("unicode_escape") + # end + + # end + + while True: + # we assume to read binary ! + line = fp.readline().decode(defenc) + if not line: + break + lineno = lineno + 1 + # comment or blank line? + if line.strip() == "" or self.re_comment.match(line): + continue + if line.split(None, 1)[0].lower() == "rem" and line[0] in "rR": + # no leading whitespace + continue + + # is it a section header? + mo = self.SECTCRE.match(line.strip()) + if not is_multi_line and mo: + sectname: str = mo.group("header").strip() + if sectname in self._sections: + cursect = self._sections[sectname] + elif sectname == cp.DEFAULTSECT: + cursect = self._defaults + else: + cursect = self._dict((("__name__", sectname),)) + self._sections[sectname] = cursect + self._proxies[sectname] = None + # So sections can't start with a continuation line + optname = None + # no section header in the file? + elif cursect is None: + raise cp.MissingSectionHeaderError(fpname, lineno, line) + # an option line? + elif not is_multi_line: + mo = self.OPTCRE.match(line) + if mo: + # We might just have handled the last line, which could contain a quotation we want to remove + optname, vi, optval = mo.group("option", "vi", "value") + if vi in ("=", ":") and ";" in optval and not optval.strip().startswith('"'): + pos = optval.find(";") + if pos != -1 and optval[pos - 1].isspace(): + optval = optval[:pos] + optval = optval.strip() + if optval == '""': + optval = "" + # end handle empty string + optname = self.optionxform(optname.rstrip()) + if len(optval) > 1 and optval[0] == '"' and optval[-1] != '"': + is_multi_line = True + optval = string_decode(optval[1:]) + # end handle multi-line + # preserves multiple values for duplicate optnames + cursect.add(optname, optval) + else: + # check if it's an option with no value - it's just ignored by git + if not self.OPTVALUEONLY.match(line): + if not e: + e = cp.ParsingError(fpname) + e.append(lineno, repr(line)) + continue + else: + line = line.rstrip() + if line.endswith('"'): + is_multi_line = False + line = line[:-1] + # end handle quotations + optval = cursect.getlast(optname) + cursect.setlast(optname, optval + string_decode(line)) + # END parse section or option + # END while reading + + # if any parsing errors occurred, raise an exception + if e: + raise e + + def _has_includes(self) -> Union[bool, int]: + return self._merge_includes and len(self._included_paths()) + + def _included_paths(self) -> List[Tuple[str, str]]: + """Return List all paths that must be included to configuration + as Tuples of (option, value). + """ + paths = [] + + for section in self.sections(): + if section == "include": + paths += self.items(section) + + match = CONDITIONAL_INCLUDE_REGEXP.search(section) + if match is None or self._repo is None: + continue + + keyword = match.group(1) + value = match.group(2).strip() + + if keyword in ["gitdir", "gitdir/i"]: + value = osp.expanduser(value) + + if not any(value.startswith(s) for s in ["./", "/"]): + value = "**/" + value + if value.endswith("/"): + value += "**" + + # Ensure that glob is always case insensitive if required. + if keyword.endswith("/i"): + value = re.sub( + r"[a-zA-Z]", + lambda m: "[{}{}]".format(m.group().lower(), m.group().upper()), + value, + ) + if self._repo.git_dir: + if fnmatch.fnmatchcase(str(self._repo.git_dir), value): + paths += self.items(section) + + elif keyword == "onbranch": + try: + branch_name = self._repo.active_branch.name + except TypeError: + # Ignore section if active branch cannot be retrieved. + continue + + if fnmatch.fnmatchcase(branch_name, value): + paths += self.items(section) + + return paths + + def read(self) -> None: # type: ignore[override] + """Reads the data stored in the files we have been initialized with. It will + ignore files that cannot be read, possibly leaving an empty configuration + + :return: Nothing + :raise IOError: if a file cannot be handled""" + if self._is_initialized: + return None + self._is_initialized = True + + files_to_read: List[Union[PathLike, IO]] = [""] + if isinstance(self._file_or_files, (str, os.PathLike)): + # for str or Path, as str is a type of Sequence + files_to_read = [self._file_or_files] + elif not isinstance(self._file_or_files, (tuple, list, Sequence)): + # could merge with above isinstance once runtime type known + files_to_read = [self._file_or_files] + else: # for lists or tuples + files_to_read = list(self._file_or_files) + # end assure we have a copy of the paths to handle + + seen = set(files_to_read) + num_read_include_files = 0 + while files_to_read: + file_path = files_to_read.pop(0) + file_ok = False + + if hasattr(file_path, "seek"): + # must be a file objectfile-object + file_path = cast(IO[bytes], file_path) # replace with assert to narrow type, once sure + self._read(file_path, file_path.name) + else: + # assume a path if it is not a file-object + file_path = cast(PathLike, file_path) + try: + with open(file_path, "rb") as fp: + file_ok = True + self._read(fp, fp.name) + except IOError: + continue + + # Read includes and append those that we didn't handle yet + # We expect all paths to be normalized and absolute (and will assure that is the case) + if self._has_includes(): + for _, include_path in self._included_paths(): + if include_path.startswith("~"): + include_path = osp.expanduser(include_path) + if not osp.isabs(include_path): + if not file_ok: + continue + # end ignore relative paths if we don't know the configuration file path + file_path = cast(PathLike, file_path) + assert osp.isabs(file_path), "Need absolute paths to be sure our cycle checks will work" + include_path = osp.join(osp.dirname(file_path), include_path) + # end make include path absolute + include_path = osp.normpath(include_path) + if include_path in seen or not os.access(include_path, os.R_OK): + continue + seen.add(include_path) + # insert included file to the top to be considered first + files_to_read.insert(0, include_path) + num_read_include_files += 1 + # each include path in configuration file + # end handle includes + # END for each file object to read + + # If there was no file included, we can safely write back (potentially) the configuration file + # without altering it's meaning + if num_read_include_files == 0: + self._merge_includes = False + # end + + def _write(self, fp: IO) -> None: + """Write an .ini-format representation of the configuration state in + git compatible format""" + + def write_section(name: str, section_dict: _OMD) -> None: + fp.write(("[%s]\n" % name).encode(defenc)) + + values: Sequence[str] # runtime only gets str in tests, but should be whatever _OMD stores + v: str + for (key, values) in section_dict.items_all(): + if key == "__name__": + continue + + for v in values: + fp.write(("\t%s = %s\n" % (key, self._value_to_string(v).replace("\n", "\n\t"))).encode(defenc)) + # END if key is not __name__ + + # END section writing + + if self._defaults: + write_section(cp.DEFAULTSECT, self._defaults) + value: _OMD + + for name, value in self._sections.items(): + write_section(name, value) + + def items(self, section_name: str) -> List[Tuple[str, str]]: # type: ignore[override] + """:return: list((option, value), ...) pairs of all items in the given section""" + return [(k, v) for k, v in super(GitConfigParser, self).items(section_name) if k != "__name__"] + + def items_all(self, section_name: str) -> List[Tuple[str, List[str]]]: + """:return: list((option, [values...]), ...) pairs of all items in the given section""" + rv = _OMD(self._defaults) + + for k, vs in self._sections[section_name].items_all(): + if k == "__name__": + continue + + if k in rv and rv.getall(k) == vs: + continue + + for v in vs: + rv.add(k, v) + + return rv.items_all() + + @needs_values + def write(self) -> None: + """Write changes to our file, if there are changes at all + + :raise IOError: if this is a read-only writer instance or if we could not obtain + a file lock""" + self._assure_writable("write") + if not self._dirty: + return None + + if isinstance(self._file_or_files, (list, tuple)): + raise AssertionError( + "Cannot write back if there is not exactly a single file to write to, have %i files" + % len(self._file_or_files) + ) + # end assert multiple files + + if self._has_includes(): + log.debug( + "Skipping write-back of configuration file as include files were merged in." + + "Set merge_includes=False to prevent this." + ) + return None + # end + + fp = self._file_or_files + + # we have a physical file on disk, so get a lock + is_file_lock = isinstance(fp, (str, os.PathLike, IOBase)) # can't use Pathlike until 3.5 dropped + if is_file_lock and self._lock is not None: # else raise Error? + self._lock._obtain_lock() + + if not hasattr(fp, "seek"): + fp = cast(PathLike, fp) + with open(fp, "wb") as fp_open: + self._write(fp_open) + else: + fp = cast("BytesIO", fp) + fp.seek(0) + # make sure we do not overwrite into an existing file + if hasattr(fp, "truncate"): + fp.truncate() + self._write(fp) + + def _assure_writable(self, method_name: str) -> None: + if self.read_only: + raise IOError("Cannot execute non-constant method %s.%s" % (self, method_name)) + + def add_section(self, section: str) -> None: + """Assures added options will stay in order""" + return super(GitConfigParser, self).add_section(section) + + @property + def read_only(self) -> bool: + """:return: True if this instance may change the configuration file""" + return self._read_only + + def get_value( + self, + section: str, + option: str, + default: Union[int, float, str, bool, None] = None, + ) -> Union[int, float, str, bool]: + # can default or return type include bool? + """Get an option's value. + + If multiple values are specified for this option in the section, the + last one specified is returned. + + :param default: + If not None, the given default value will be returned in case + the option did not exist + :return: a properly typed value, either int, float or string + + :raise TypeError: in case the value could not be understood + Otherwise the exceptions known to the ConfigParser will be raised.""" + try: + valuestr = self.get(section, option) + except Exception: + if default is not None: + return default + raise + + return self._string_to_value(valuestr) + + def get_values( + self, + section: str, + option: str, + default: Union[int, float, str, bool, None] = None, + ) -> List[Union[int, float, str, bool]]: + """Get an option's values. + + If multiple values are specified for this option in the section, all are + returned. + + :param default: + If not None, a list containing the given default value will be + returned in case the option did not exist + :return: a list of properly typed values, either int, float or string + + :raise TypeError: in case the value could not be understood + Otherwise the exceptions known to the ConfigParser will be raised.""" + try: + self.sections() + lst = self._sections[section].getall(option) + except Exception: + if default is not None: + return [default] + raise + + return [self._string_to_value(valuestr) for valuestr in lst] + + def _string_to_value(self, valuestr: str) -> Union[int, float, str, bool]: + types = (int, float) + for numtype in types: + try: + val = numtype(valuestr) + # truncated value ? + if val != float(valuestr): + continue + return val + except (ValueError, TypeError): + continue + # END for each numeric type + + # try boolean values as git uses them + vl = valuestr.lower() + if vl == "false": + return False + if vl == "true": + return True + + if not isinstance(valuestr, str): + raise TypeError( + "Invalid value type: only int, long, float and str are allowed", + valuestr, + ) + + return valuestr + + def _value_to_string(self, value: Union[str, bytes, int, float, bool]) -> str: + if isinstance(value, (int, float, bool)): + return str(value) + return force_text(value) + + @needs_values + @set_dirty_and_flush_changes + def set_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser": + """Sets the given option in section to the given value. + It will create the section if required, and will not throw as opposed to the default + ConfigParser 'set' method. + + :param section: Name of the section in which the option resides or should reside + :param option: Name of the options whose value to set + + :param value: Value to set the option to. It must be a string or convertible + to a string + :return: this instance""" + if not self.has_section(section): + self.add_section(section) + self.set(section, option, self._value_to_string(value)) + return self + + @needs_values + @set_dirty_and_flush_changes + def add_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser": + """Adds a value for the given option in section. + It will create the section if required, and will not throw as opposed to the default + ConfigParser 'set' method. The value becomes the new value of the option as returned + by 'get_value', and appends to the list of values returned by 'get_values`'. + + :param section: Name of the section in which the option resides or should reside + :param option: Name of the option + + :param value: Value to add to option. It must be a string or convertible + to a string + :return: this instance""" + if not self.has_section(section): + self.add_section(section) + self._sections[section].add(option, self._value_to_string(value)) + return self + + def rename_section(self, section: str, new_name: str) -> "GitConfigParser": + """rename the given section to new_name + :raise ValueError: if section doesn't exit + :raise ValueError: if a section with new_name does already exist + :return: this instance + """ + if not self.has_section(section): + raise ValueError("Source section '%s' doesn't exist" % section) + if self.has_section(new_name): + raise ValueError("Destination section '%s' already exists" % new_name) + + super(GitConfigParser, self).add_section(new_name) + new_section = self._sections[new_name] + for k, vs in self.items_all(section): + new_section.setall(k, vs) + # end for each value to copy + + # This call writes back the changes, which is why we don't have the respective decorator + self.remove_section(section) + return self |