Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/git/config.py: 74%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

613 statements  

1# Copyright (C) 2008, 2009 Michael Trier (mtrier@gmail.com) and contributors 

2# 

3# This module is part of GitPython and is released under the 

4# 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ 

5 

6"""Parser for reading and writing configuration files.""" 

7 

8__all__ = ["GitConfigParser", "SectionConstraint"] 

9 

10import abc 

11import configparser as cp 

12import fnmatch 

13import inspect 

14import logging 

15import os 

16import os.path as osp 

17import re 

18import sys 

19from functools import wraps 

20from io import BufferedReader, IOBase 

21 

22# typing------------------------------------------------------- 

23from typing import ( 

24 IO, 

25 TYPE_CHECKING, 

26 Any, 

27 Callable, 

28 Dict, 

29 Generic, 

30 List, 

31 OrderedDict, 

32 Sequence, 

33 Tuple, 

34 TypeVar, 

35 Union, 

36 cast, 

37) 

38 

39from git.compat import defenc, force_text 

40from git.types import _T, ConfigLevels_Tup, Lit_config_levels, PathLike, assert_never 

41from git.util import LockFile 

42 

43if TYPE_CHECKING: 

44 from io import BytesIO 

45 

46 from git.repo.base import Repo 

47 

48T_ConfigParser = TypeVar("T_ConfigParser", bound="GitConfigParser") 

49T_OMD_value = TypeVar("T_OMD_value", str, bytes, int, float, bool, None) 

50 

51OrderedDict_OMD = OrderedDict[str, List[T_OMD_value]] 

52 

53# ------------------------------------------------------------- 

54 

55_logger = logging.getLogger(__name__) 

56 

57CONFIG_LEVELS: ConfigLevels_Tup = ("system", "user", "global", "repository") 

58"""The configuration level of a configuration file.""" 

59 

60CONDITIONAL_INCLUDE_REGEXP = re.compile(r"(?<=includeif )\"(gitdir|gitdir/i|onbranch|hasconfig:remote\.\*\.url):(.+)\"") 

61"""Section pattern to detect conditional includes. 

62 

63See: https://git-scm.com/docs/git-config#_conditional_includes 

64""" 

65 

66UNSAFE_CONFIG_CHARS_RE = re.compile(r"[\r\n\x00]") 

67"""Characters that cannot be safely written in config names or values.""" 

68 

69VALID_CONFIG_OPTION_NAME_RE = re.compile(r"^[A-Za-z0-9_.-]+$") 

70"""Pattern for option names that can be written without changing config syntax.""" 

71 

72 

73class MetaParserBuilder(abc.ABCMeta): # noqa: B024 

74 """Utility class wrapping base-class methods into decorators that assure read-only 

75 properties.""" 

76 

77 def __new__(cls, name: str, bases: Tuple, clsdict: Dict[str, Any]) -> "MetaParserBuilder": 

78 """Equip all base-class methods with a needs_values decorator, and all non-const 

79 methods with a :func:`set_dirty_and_flush_changes` decorator in addition to 

80 that. 

81 """ 

82 kmm = "_mutating_methods_" 

83 if kmm in clsdict: 

84 mutating_methods = clsdict[kmm] 

85 for base in bases: 

86 methods = (t for t in inspect.getmembers(base, inspect.isroutine) if not t[0].startswith("_")) 

87 for method_name, method in methods: 

88 if method_name in clsdict: 

89 continue 

90 method_with_values = needs_values(method) 

91 if method_name in mutating_methods: 

92 method_with_values = set_dirty_and_flush_changes(method_with_values) 

93 # END mutating methods handling 

94 

95 clsdict[method_name] = method_with_values 

96 # END for each name/method pair 

97 # END for each base 

98 # END if mutating methods configuration is set 

99 

100 new_type = super().__new__(cls, name, bases, clsdict) 

101 return new_type 

102 

103 

104def needs_values(func: Callable[..., _T]) -> Callable[..., _T]: 

105 """Return a method for ensuring we read values (on demand) before we try to access 

106 them.""" 

107 

108 @wraps(func) 

109 def assure_data_present(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T: 

110 self.read() 

111 return func(self, *args, **kwargs) 

112 

113 # END wrapper method 

114 return assure_data_present 

115 

116 

117def set_dirty_and_flush_changes(non_const_func: Callable[..., _T]) -> Callable[..., _T]: 

118 """Return a method that checks whether given non constant function may be called. 

119 

120 If so, the instance will be set dirty. Additionally, we flush the changes right to 

121 disk. 

122 """ 

123 

124 def flush_changes(self: "GitConfigParser", *args: Any, **kwargs: Any) -> _T: 

125 rval = non_const_func(self, *args, **kwargs) 

126 self._dirty = True 

127 self.write() 

128 return rval 

129 

130 # END wrapper method 

131 flush_changes.__name__ = non_const_func.__name__ 

132 return flush_changes 

133 

134 

135class SectionConstraint(Generic[T_ConfigParser]): 

136 """Constrains a ConfigParser to only option commands which are constrained to 

137 always use the section we have been initialized with. 

138 

139 It supports all ConfigParser methods that operate on an option. 

140 

141 :note: 

142 If used as a context manager, will release the wrapped ConfigParser. 

143 """ 

144 

145 __slots__ = ("_config", "_section_name") 

146 

147 _valid_attrs_ = ( 

148 "get_value", 

149 "set_value", 

150 "get", 

151 "set", 

152 "getint", 

153 "getfloat", 

154 "getboolean", 

155 "has_option", 

156 "remove_section", 

157 "remove_option", 

158 "options", 

159 ) 

160 

161 def __init__(self, config: T_ConfigParser, section: str) -> None: 

162 self._config = config 

163 self._section_name = section 

164 

165 def __del__(self) -> None: 

166 # Yes, for some reason, we have to call it explicitly for it to work in PY3 ! 

167 # Apparently __del__ doesn't get call anymore if refcount becomes 0 

168 # Ridiculous ... . 

169 self._config.release() 

170 

171 def __getattr__(self, attr: str) -> Any: 

172 if attr in self._valid_attrs_: 

173 return lambda *args, **kwargs: self._call_config(attr, *args, **kwargs) 

174 return super().__getattribute__(attr) 

175 

176 def _call_config(self, method: str, *args: Any, **kwargs: Any) -> Any: 

177 """Call the configuration at the given method which must take a section name as 

178 first argument.""" 

179 return getattr(self._config, method)(self._section_name, *args, **kwargs) 

180 

181 @property 

182 def config(self) -> T_ConfigParser: 

183 """return: ConfigParser instance we constrain""" 

184 return self._config 

185 

186 def release(self) -> None: 

187 """Equivalent to :meth:`GitConfigParser.release`, which is called on our 

188 underlying parser instance.""" 

189 return self._config.release() 

190 

191 def __enter__(self) -> "SectionConstraint[T_ConfigParser]": 

192 self._config.__enter__() 

193 return self 

194 

195 def __exit__(self, exception_type: str, exception_value: str, traceback: str) -> None: 

196 self._config.__exit__(exception_type, exception_value, traceback) 

197 

198 

199def _normalize_name(name: str) -> str: 

200 """Fold section and option names, leaving quoted subsections unchanged.""" 

201 prefix, separator, subsection = name.partition('"') 

202 return prefix.lower() + separator + subsection 

203 

204 

205class _OMD(OrderedDict_OMD): 

206 """Ordered multi-dict matching config names while retaining their first spelling.""" 

207 

208 def __init__(self, *args: Any, **kwargs: Any) -> None: 

209 self._keymap: Dict[str, str] = {} 

210 super().__init__(*args, **kwargs) 

211 

212 def _key(self, key: str) -> str: 

213 stored = self._keymap.get(_normalize_name(key), key) 

214 return stored if super().__contains__(stored) else key 

215 

216 def __contains__(self, key: object) -> bool: 

217 return isinstance(key, str) and super().__contains__(self._key(key)) 

218 

219 def __delitem__(self, key: str) -> None: 

220 super().__delitem__(self._key(key)) 

221 del self._keymap[_normalize_name(key)] 

222 

223 def __setitem__(self, key: str, value: _T) -> None: 

224 self.setall(key, [value]) 

225 

226 def clear(self) -> None: 

227 super().clear() 

228 self._keymap.clear() 

229 

230 def add(self, key: str, value: Any) -> None: 

231 if key not in self: 

232 self[key] = value 

233 return 

234 

235 self.getall(key).append(value) 

236 

237 def setall(self, key: str, values: List[_T]) -> None: 

238 key = self._key(key) 

239 super().__setitem__(key, values) 

240 self._keymap[_normalize_name(key)] = key 

241 

242 def __getitem__(self, key: str) -> Any: 

243 return super().__getitem__(self._key(key))[-1] 

244 

245 def getlast(self, key: str) -> Any: 

246 return self[key] 

247 

248 def setlast(self, key: str, value: Any) -> None: 

249 if key not in self: 

250 self[key] = value 

251 return 

252 

253 self.getall(key)[-1] = value 

254 

255 def get(self, key: str, default: Union[_T, None] = None) -> Union[_T, None]: # type: ignore[override] 

256 return super().get(self._key(key), [default])[-1] 

257 

258 def getall(self, key: str) -> List[_T]: 

259 return super().__getitem__(self._key(key)) 

260 

261 def items(self) -> List[Tuple[str, _T]]: # type: ignore[override] 

262 """List of (key, last value for key).""" 

263 return [(k, self[k]) for k in self] 

264 

265 def items_all(self) -> List[Tuple[str, List[_T]]]: 

266 """List of (key, list of values for key).""" 

267 return [(k, self.getall(k)) for k in self] 

268 

269 

270def get_config_path(config_level: Lit_config_levels) -> str: 

271 # We do not support an absolute path of the gitconfig on Windows. 

272 # Use the global config instead. 

273 if sys.platform == "win32" and config_level == "system": 

274 config_level = "global" 

275 

276 if config_level == "system": 

277 return "/etc/gitconfig" 

278 elif config_level == "user": 

279 config_home = os.environ.get("XDG_CONFIG_HOME") or osp.join(os.environ.get("HOME", "~"), ".config") 

280 return osp.normpath(osp.expanduser(osp.join(config_home, "git", "config"))) 

281 elif config_level == "global": 

282 return osp.normpath(osp.expanduser("~/.gitconfig")) 

283 elif config_level == "repository": 

284 raise ValueError("No repo to get repository configuration from. Use Repo._get_config_path") 

285 else: 

286 # Should not reach here. Will raise ValueError if does. Static typing will warn 

287 # about missing elifs. 

288 assert_never( # type: ignore[unreachable] 

289 config_level, 

290 ValueError(f"Invalid configuration level: {config_level!r}"), 

291 ) 

292 

293 

294class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): 

295 """Implements specifics required to read git style configuration files. 

296 

297 This variation behaves much like the :manpage:`git-config(1)` command, such that the 

298 configuration will be read on demand based on the filepath given during 

299 initialization. 

300 

301 The changes will automatically be written once the instance goes out of scope, but 

302 can be triggered manually as well. 

303 

304 The configuration file will be locked if you intend to change values preventing 

305 other instances to write concurrently. 

306 

307 :note: 

308 Section and option names are case-insensitive; quoted subsection names are 

309 case-sensitive. Names retain their first spelling when enumerated or written. 

310 Case variants are merged, preserving all values in the order they are read. 

311 

312 :note: 

313 If used as a context manager, this will release the locked file. 

314 

315 :note: 

316 Options without a value are stored as ``None`` and written without ``=``. 

317 :meth:`get_value` and :meth:`get_values` return an empty string for them, 

318 while :meth:`getboolean` returns ``True``. An explicit empty value is 

319 stored as an empty string and reads as ``False`` with :meth:`getboolean`. 

320 """ 

321 

322 # { Configuration 

323 t_lock = LockFile 

324 """The lock type determines the type of lock to use in new configuration readers. 

325 

326 They must be compatible to the :class:`~git.util.LockFile` interface. 

327 A suitable alternative would be the :class:`~git.util.BlockingLockFile`. 

328 """ 

329 

330 re_comment = re.compile(r"^\s*[#;]") 

331 # } END configuration 

332 

333 optvalueonly_source = r"\s*(?P<option>[^:=\s#;][^:=#;]*)" 

334 

335 OPTVALUEONLY = re.compile(optvalueonly_source) 

336 

337 # The option name class [^:=#;]* already consumes any spaces up to the ":" or "=", 

338 # so a second \s* before the indicator would overlap it and backtrack quadratically 

339 # on a line that never reaches an indicator (for example a key followed by a long 

340 # whitespace run). Drop the redundant \s*; the name is right-stripped after parsing. 

341 OPTCRE = re.compile(optvalueonly_source + r"(?P<vi>[:=])\s*" + r"(?P<value>.*)$") 

342 

343 del optvalueonly_source 

344 

345 _mutating_methods_ = ("add_section", "remove_section", "remove_option", "set") 

346 """Names of :class:`~configparser.RawConfigParser` methods able to change the 

347 instance.""" 

348 

349 def __init__( 

350 self, 

351 file_or_files: Union[None, PathLike, "BytesIO", Sequence[Union[PathLike, "BytesIO"]]] = None, 

352 read_only: bool = True, 

353 merge_includes: bool = True, 

354 config_level: Union[Lit_config_levels, None] = None, 

355 repo: Union["Repo", None] = None, 

356 ) -> None: 

357 """Initialize a configuration reader to read the given `file_or_files` and to 

358 possibly allow changes to it by setting `read_only` False. 

359 

360 :param file_or_files: 

361 A file path or file object, or a sequence of possibly more than one of them. 

362 

363 :param read_only: 

364 If ``True``, the ConfigParser may only read the data, but not change it. 

365 If ``False``, only a single file path or file object may be given. We will 

366 write back the changes when they happen, or when the ConfigParser is 

367 released. This will not happen if other configuration files have been 

368 included. 

369 

370 :param merge_includes: 

371 If ``True``, we will read files mentioned in ``[include]`` sections and 

372 merge their contents into ours. This makes it impossible to write back an 

373 individual configuration file. Thus, if you want to modify a single 

374 configuration file, turn this off to leave the original dataset unaltered 

375 when reading it. 

376 

377 :param repo: 

378 Reference to repository to use if ``[includeIf]`` sections are found in 

379 configuration files. 

380 """ 

381 cp.RawConfigParser.__init__(self, dict_type=cast(Any, _OMD), allow_no_value=True) 

382 self._dict: Callable[..., _OMD] 

383 self._defaults: _OMD 

384 self._sections: _OMD 

385 

386 # Used in Python 3. Needs to stay in sync with sections for underlying 

387 # implementation to work. 

388 if not hasattr(self, "_proxies"): 

389 self._proxies = self._dict() 

390 

391 if file_or_files is not None: 

392 self._file_or_files: Union[PathLike, "BytesIO", Sequence[Union[PathLike, "BytesIO"]]] = file_or_files 

393 else: 

394 if config_level is None: 

395 if read_only: 

396 self._file_or_files = [ 

397 get_config_path(cast(Lit_config_levels, f)) for f in CONFIG_LEVELS if f != "repository" 

398 ] 

399 else: 

400 raise ValueError("No configuration level or configuration files specified") 

401 else: 

402 self._file_or_files = [get_config_path(config_level)] 

403 

404 self._read_only = read_only 

405 self._dirty = False 

406 self._is_initialized = False 

407 self._merge_includes = merge_includes 

408 self._repo = repo 

409 self._lock: Union["LockFile", None] = None 

410 self._acquire_lock() 

411 

412 def _acquire_lock(self) -> None: 

413 if not self._read_only: 

414 if not self._lock: 

415 if isinstance(self._file_or_files, (str, os.PathLike)): 

416 file_or_files = self._file_or_files 

417 elif isinstance(self._file_or_files, (tuple, list, Sequence)): 

418 raise ValueError( 

419 "Write-ConfigParsers can operate on a single file only, multiple files have been passed" 

420 ) 

421 else: 

422 file_or_files = self._file_or_files.name 

423 

424 # END get filename from handle/stream 

425 # Initialize lock base - we want to write. 

426 self._lock = self.t_lock(file_or_files) 

427 # END lock check 

428 

429 self._lock._obtain_lock() 

430 # END read-only check 

431 

432 def __del__(self) -> None: 

433 """Write pending changes if required and release locks.""" 

434 # NOTE: Only consistent in Python 2. 

435 self.release() 

436 

437 def __enter__(self) -> "GitConfigParser": 

438 self._acquire_lock() 

439 return self 

440 

441 def __exit__(self, *args: Any) -> None: 

442 self.release() 

443 

444 def release(self) -> None: 

445 """Flush changes and release the configuration write lock. This instance must 

446 not be used anymore afterwards. 

447 

448 In Python 3, it's required to explicitly release locks and flush changes, as 

449 ``__del__`` is not called deterministically anymore. 

450 """ 

451 # Checking for the lock here makes sure we do not raise during write() 

452 # in case an invalid parser was created who could not get a lock. 

453 if self.read_only or (self._lock and not self._lock._has_lock()): 

454 return 

455 

456 try: 

457 self.write() 

458 except OSError: 

459 _logger.error("Exception during destruction of GitConfigParser", exc_info=True) 

460 except ReferenceError: 

461 # This happens in Python 3... and usually means that some state cannot be 

462 # written as the sections dict cannot be iterated. This usually happens when 

463 # the interpreter is shutting down. Can it be fixed? 

464 pass 

465 finally: 

466 if self._lock is not None: 

467 self._lock._release_lock() 

468 

469 def optionxform(self, optionstr: str) -> str: 

470 """Do not transform options in any way when writing.""" 

471 return optionstr 

472 

473 def _read(self, fp: Union[BufferedReader, IO[bytes]], fpname: str) -> None: 

474 """Originally a direct copy of the Python 2.4 version of 

475 :meth:`RawConfigParser._read <configparser.RawConfigParser._read>`, to ensure it 

476 uses ordered dicts. 

477 

478 The ordering bug was fixed in Python 2.4, and dict itself keeps ordering since 

479 Python 3.7. This has some other changes, especially that it ignores initial 

480 whitespace, since git uses tabs. (Big comments are removed to be more compact.) 

481 """ 

482 cursect = None # None, or a dictionary. 

483 optname = None 

484 lineno = 0 

485 is_multi_line = False 

486 e = None # None, or an exception. 

487 

488 def string_decode(v: str) -> str: 

489 if v and v.endswith("\\"): 

490 v = v[:-1] 

491 # END cut trailing escapes to prevent decode error 

492 

493 escapes = {"b": "\b", "n": "\n", "r": "\r", "t": "\t", '"': '"', "\\": "\\"} 

494 return re.sub(r"\\(.)", lambda match: escapes.get(match.group(1), match.group(0)), v) 

495 

496 # END string_decode 

497 

498 def is_line_continuation(value: str) -> bool: 

499 quoted = escaped = False 

500 for char in value: 

501 if escaped: 

502 escaped = False 

503 elif char == "\\": 

504 escaped = True 

505 elif char == '"': 

506 quoted = not quoted 

507 elif char in "#;" and not quoted: 

508 return False 

509 return escaped 

510 

511 def strip_inline_comment(value: str) -> Tuple[str, bool]: 

512 """Cut an unquoted ``#`` or ``;`` comment and report whether a quote is open. 

513 

514 Quoting and backslash escapes are honoured, so a ``#`` inside a quoted 

515 value is literal and an unterminated quote swallows the rest of the line. 

516 """ 

517 quoted = escaped = False 

518 for index, char in enumerate(value): 

519 if escaped: 

520 escaped = False 

521 elif char == "\\": 

522 escaped = True 

523 elif char == '"': 

524 quoted = not quoted 

525 elif char in "#;" and not quoted: 

526 return value[:index], False 

527 return value, quoted 

528 

529 def parse_value(value: str) -> str: 

530 parsed: List[str] = [] 

531 whitespace: List[str] = [] 

532 quoted = escaped = False 

533 escapes = {"b": "\b", "n": "\n", "t": "\t", '"': '"', "\\": "\\"} 

534 for char in value: 

535 if escaped: 

536 parsed.append(escapes.get(char, "\\" + char)) 

537 escaped = False 

538 continue 

539 if char.isspace() and not quoted: 

540 if parsed: 

541 whitespace.append(char) 

542 continue 

543 if char in "#;" and not quoted: 

544 break 

545 parsed.extend(whitespace) 

546 whitespace.clear() 

547 if char == "\\": 

548 escaped = True 

549 elif char == '"': 

550 quoted = not quoted 

551 else: 

552 parsed.append(char) 

553 return "".join(parsed) 

554 

555 while True: 

556 # We assume to read binary! 

557 raw_line = fp.readline() 

558 if not raw_line: 

559 break 

560 if lineno == 0 and raw_line.startswith(b"\xef\xbb\xbf"): 

561 # A UTF-8 BOM is not part of the content. git skips it, so a 

562 # config file written by a Windows editor still parses. 

563 raw_line = raw_line[3:] 

564 line = raw_line.decode(defenc) 

565 lineno = lineno + 1 

566 # Comment or blank line? 

567 if line.strip() == "" or self.re_comment.match(line): 

568 continue 

569 if line.split(None, 1)[0].lower() == "rem" and line[0] in "rR": 

570 # No leading whitespace. 

571 continue 

572 

573 # Is it a section header? 

574 mo = self.SECTCRE.match(line.strip()) 

575 if not is_multi_line and mo: 

576 sectname: str = mo.group("header").strip() 

577 if sectname in self._sections: 

578 cursect = self._sections[sectname] 

579 elif sectname == cp.DEFAULTSECT: 

580 cursect = self._defaults 

581 else: 

582 cursect = self._dict((("__name__", sectname),)) 

583 self._sections[sectname] = cursect 

584 self._proxies[sectname] = None 

585 # So sections can't start with a continuation line. 

586 optname = None 

587 # No section header in the file? 

588 elif cursect is None: 

589 raise cp.MissingSectionHeaderError(fpname, lineno, line) 

590 # An option line? 

591 elif not is_multi_line: 

592 mo = self.OPTCRE.match(line) 

593 if mo: 

594 # We might just have handled the last line, which could contain a quotation we want to remove. 

595 optname, vi, optval = mo.group("option", "vi", "value") 

596 optname = self.optionxform(optname.rstrip()) 

597 

598 optval, quote_open = strip_inline_comment(optval) 

599 optval = optval.strip() 

600 

601 if len(optval) < 2 or optval[0] != '"': 

602 # Does not open quoting. 

603 # A value ending in an odd number of backslashes 

604 # continues on the next line, exactly as git does: the 

605 # final backslash and the newline are removed and the 

606 # next line is appended before the complete value is 

607 # parsed. An even number means the last backslash is 

608 # escaped and the value ends there. 

609 continued = False 

610 while True: 

611 if not is_line_continuation(optval): 

612 break 

613 continuation = fp.readline() 

614 if not continuation: 

615 # Backslash at end of file: git drops it. 

616 optval = optval[:-1] 

617 break 

618 lineno = lineno + 1 

619 joined = continuation.decode(defenc) 

620 while joined.endswith("\n") or joined.endswith("\r"): 

621 joined = joined[:-1] 

622 optval = optval[:-1] + joined 

623 continued = True 

624 if continued: 

625 optval = parse_value(optval) 

626 elif quote_open: 

627 # Opens quoting and does not close: appears to start multi-line quoting. 

628 is_multi_line = True 

629 optval = string_decode(optval[1:]) 

630 elif re.search(r'(?:^|[^\\])(?:\\\\)*"', optval[1:-1]): 

631 # Preserve values containing additional unescaped quotes. 

632 pass 

633 else: 

634 # Opens and closes quoting. 

635 optval = string_decode(optval[1:-1]) 

636 

637 # Preserves multiple values for duplicate optnames. 

638 cursect.add(optname, optval) 

639 else: 

640 # A valueless option is an implicit boolean true, not an empty value. 

641 mo = self.OPTVALUEONLY.fullmatch(line) 

642 if mo: 

643 optname = self.optionxform(mo.group("option").rstrip()) 

644 cursect.add(optname, None) 

645 else: 

646 if not e: 

647 e = cp.ParsingError(fpname) 

648 e.append(lineno, repr(line)) 

649 continue 

650 else: 

651 line = line.rstrip() 

652 if line.endswith('"'): 

653 is_multi_line = False 

654 line = line[:-1] 

655 # END handle quotations 

656 optval = cursect.getlast(optname) 

657 cursect.setlast(optname, optval + string_decode(line)) 

658 # END parse section or option 

659 # END while reading 

660 

661 # If any parsing errors occurred, raise an exception. 

662 if e: 

663 raise e 

664 

665 def _has_includes(self) -> Union[bool, int]: 

666 return self._merge_includes and len(self._included_paths()) 

667 

668 def _included_paths(self) -> List[Tuple[str, str]]: 

669 """List all paths that must be included to configuration. 

670 

671 :return: 

672 The list of paths, where each path is a tuple of (option, value). 

673 """ 

674 

675 def _all_items(section: str) -> List[Tuple[str, str]]: 

676 """Return all (key, value) pairs for a section, including duplicate keys.""" 

677 return [ 

678 (key, value) 

679 for key, values in self._sections[section].items_all() 

680 if key != "__name__" 

681 for value in values 

682 if value is not None 

683 ] 

684 

685 paths = [] 

686 

687 for section in self.sections(): 

688 normalized_section = _normalize_name(section) 

689 if normalized_section == "include": 

690 paths += _all_items(section) 

691 

692 match = CONDITIONAL_INCLUDE_REGEXP.search(normalized_section) 

693 if match is None or self._repo is None: 

694 continue 

695 

696 keyword = match.group(1) 

697 value = match.group(2).strip() 

698 

699 if keyword in ["gitdir", "gitdir/i"]: 

700 value = osp.expanduser(value) 

701 git_dir = os.fspath(self._repo.git_dir) if self._repo.git_dir else None 

702 if sys.platform == "win32": 

703 git_dir = git_dir.replace("\\", "/") if git_dir else None 

704 

705 drive, _tail = osp.splitdrive(value) 

706 if not drive and not any(value.startswith(s) for s in ["./", "/"]): 

707 value = "**/" + value 

708 if value.endswith("/"): 

709 value += "**" 

710 

711 # Ensure that glob is always case insensitive if required. 

712 if keyword.endswith("/i"): 

713 value = re.sub( 

714 r"[a-zA-Z]", 

715 lambda m: f"[{m.group().lower()!r}{m.group().upper()!r}]", 

716 value, 

717 ) 

718 if git_dir and fnmatch.fnmatchcase(git_dir, value): 

719 paths += _all_items(section) 

720 

721 elif keyword == "onbranch": 

722 try: 

723 branch_name = self._repo.active_branch.name 

724 except TypeError: 

725 # Ignore section if active branch cannot be retrieved. 

726 continue 

727 

728 if fnmatch.fnmatchcase(branch_name, value): 

729 paths += _all_items(section) 

730 elif keyword == "hasconfig:remote.*.url": 

731 for remote in self._repo.remotes: 

732 if fnmatch.fnmatchcase(remote.url, value): 

733 paths += _all_items(section) 

734 break 

735 return paths 

736 

737 def read(self) -> None: # type: ignore[override] 

738 """Read the data stored in the files we have been initialized with. 

739 

740 This will ignore files that cannot be read, possibly leaving an empty 

741 configuration. 

742 

743 :raise IOError: 

744 If a file cannot be handled. 

745 """ 

746 if self._is_initialized: 

747 return 

748 self._is_initialized = True 

749 

750 files_to_read: List[Union[PathLike, IO]] = [""] 

751 if isinstance(self._file_or_files, (str, os.PathLike)): 

752 # For str or Path, as str is a type of Sequence. 

753 files_to_read = [self._file_or_files] 

754 elif not isinstance(self._file_or_files, (tuple, list, Sequence)): 

755 # Could merge with above isinstance once runtime type known. 

756 files_to_read = [self._file_or_files] 

757 else: # For lists or tuples. 

758 files_to_read = list(self._file_or_files) 

759 # END ensure we have a copy of the paths to handle 

760 

761 files_to_read = [osp.abspath(path) if isinstance(path, (str, os.PathLike)) else path for path in files_to_read] 

762 

763 seen = set(files_to_read) 

764 num_read_include_files = 0 

765 while files_to_read: 

766 file_path = files_to_read.pop(0) 

767 file_ok = False 

768 

769 if hasattr(file_path, "seek"): 

770 # Must be a file-object. 

771 # TODO: Replace cast with assert to narrow type, once sure. 

772 file_path = cast(IO[bytes], file_path) 

773 self._read(file_path, file_path.name) 

774 else: 

775 try: 

776 with open(file_path, "rb") as fp: 

777 file_ok = True 

778 self._read(fp, fp.name) 

779 except OSError: 

780 continue 

781 

782 # Read includes and append those that we didn't handle yet. We expect all 

783 # paths to be normalized and absolute (and will ensure that is the case). 

784 if self._has_includes(): 

785 for _, include_path in self._included_paths(): 

786 if include_path.startswith("~"): 

787 include_path = osp.expanduser(include_path) 

788 if not osp.isabs(include_path): 

789 if not file_ok: 

790 continue 

791 # END ignore relative paths if we don't know the configuration file path 

792 file_path = cast(PathLike, file_path) 

793 assert osp.isabs(file_path), "Need absolute paths to be sure our cycle checks will work" 

794 include_path = osp.join(osp.dirname(file_path), include_path) 

795 # END make include path absolute 

796 include_path = osp.normpath(include_path) 

797 if include_path in seen or not os.access(include_path, os.R_OK): 

798 continue 

799 seen.add(include_path) 

800 # Insert included file to the top to be considered first. 

801 files_to_read.insert(0, include_path) 

802 num_read_include_files += 1 

803 # END each include path in configuration file 

804 # END handle includes 

805 # END for each file object to read 

806 

807 # If there was no file included, we can safely write back (potentially) the 

808 # configuration file without altering its meaning. 

809 if num_read_include_files == 0: 

810 self._merge_includes = False 

811 

812 def _write(self, fp: IO) -> None: 

813 """Write an .ini-format representation of the configuration state in 

814 git compatible format.""" 

815 

816 def write_section(name: str, section_dict: _OMD) -> None: 

817 fp.write(("[%s]\n" % name).encode(defenc)) 

818 

819 values: List[Any] 

820 v: Any 

821 for key, values in section_dict.items_all(): 

822 if key == "__name__": 

823 continue 

824 

825 for v in values: 

826 if v is None: 

827 fp.write(("\t%s\n" % key).encode(defenc)) 

828 continue 

829 value = self._value_to_string(v) 

830 if any(char in value for char in '\n\t\b\\"#;') or value[:1].isspace() or value[-1:].isspace(): 

831 value = value.replace("\\", "\\\\").replace('"', '\\"') 

832 value = '"%s\\\n"' % value.replace("\n", "\\n").replace("\t", "\\t").replace("\b", "\\b") 

833 fp.write(("\t%s = %s\n" % (key, value)).encode(defenc)) 

834 # END if key is not __name__ 

835 

836 # END section writing 

837 

838 if self._defaults: 

839 write_section(cp.DEFAULTSECT, self._defaults) 

840 value: _OMD 

841 

842 for name, value in self._sections.items(): 

843 write_section(name, value) 

844 

845 def items(self, section_name: str) -> List[Tuple[str, Union[str, None]]]: # type: ignore[override] 

846 """:return: list((option, value), ...) pairs of all items in the given section""" 

847 return [(k, v) for k, v in super().items(section_name) if k != "__name__"] 

848 

849 def items_all(self, section_name: str) -> List[Tuple[str, List[Union[str, None]]]]: 

850 """:return: list((option, [values...]), ...) pairs of all items in the given section""" 

851 rv = _OMD(self._defaults) 

852 

853 for k, vs in self._sections[section_name].items_all(): 

854 if k == "__name__": 

855 continue 

856 

857 if k in rv and rv.getall(k) == vs: 

858 continue 

859 

860 for v in vs: 

861 rv.add(k, v) 

862 

863 return rv.items_all() 

864 

865 @needs_values 

866 def write(self) -> None: 

867 """Write changes to our file, if there are changes at all. 

868 

869 :raise IOError: 

870 If this is a read-only writer instance or if we could not obtain a file 

871 lock. 

872 """ 

873 self._assure_writable("write") 

874 if not self._dirty: 

875 return 

876 

877 if isinstance(self._file_or_files, (list, tuple)): 

878 raise AssertionError( 

879 "Cannot write back if there is not exactly a single file to write to, have %i files" 

880 % len(self._file_or_files) 

881 ) 

882 # END assert multiple files 

883 

884 if self._has_includes(): 

885 _logger.debug( 

886 "Skipping write-back of configuration file as include files were merged in." 

887 + "Set merge_includes=False to prevent this." 

888 ) 

889 return 

890 # END stop if we have include files 

891 

892 sections: List[_OMD] = [self._defaults] 

893 section: _OMD 

894 stored_section: _OMD 

895 values: List[Any] 

896 raw_value: Any 

897 for _, stored_section in self._sections.items(): 

898 sections.append(stored_section) 

899 for section in sections: 

900 for key, values in section.items_all(): 

901 if key != "__name__": 

902 for raw_value in values: 

903 if raw_value is None: 

904 continue 

905 if "\r" in self._value_to_string(raw_value) or "\x00" in self._value_to_string(raw_value): 

906 raise ValueError("Git config values must not contain CR or NUL") 

907 

908 fp = self._file_or_files 

909 

910 # We have a physical file on disk, so get a lock. 

911 is_file_lock = isinstance(fp, (str, os.PathLike, IOBase)) # TODO: Use PathLike (having dropped 3.5). 

912 if is_file_lock and self._lock is not None: # Else raise error? 

913 self._lock._obtain_lock() 

914 

915 if not hasattr(fp, "seek"): 

916 fp = cast(PathLike, fp) 

917 with open(fp, "wb") as fp_open: 

918 self._write(fp_open) 

919 else: 

920 fp = cast("BytesIO", fp) 

921 fp.seek(0) 

922 # Make sure we do not overwrite into an existing file. 

923 if hasattr(fp, "truncate"): 

924 fp.truncate() 

925 self._write(fp) 

926 

927 def _assure_writable(self, method_name: str) -> None: 

928 if self.read_only: 

929 raise OSError(f"Cannot execute non-constant method {self}.{method_name}") 

930 

931 def add_section(self, section: "cp._SectionName") -> None: 

932 """Assures added options will stay in order.""" 

933 self._assure_config_name_safe(section, "section") 

934 return super().add_section(section) 

935 

936 @property 

937 def read_only(self) -> bool: 

938 """:return: ``True`` if this instance may change the configuration file""" 

939 return self._read_only 

940 

941 def get_value( 

942 self, 

943 section: str, 

944 option: str, 

945 default: Union[int, float, str, bool, None] = None, 

946 ) -> Union[int, float, str, bool]: 

947 """Get an option's value. 

948 

949 If multiple values are specified for this option in the section, the last one 

950 specified is returned. 

951 

952 :param default: 

953 If not ``None``, the given default value will be returned in case the option 

954 did not exist. 

955 

956 :return: 

957 A properly typed value, either int, float, string or bool 

958 

959 :raise TypeError: 

960 In case the value could not be understood. 

961 Otherwise the exceptions known to the ConfigParser will be raised. 

962 """ 

963 try: 

964 valuestr = self.get(section, option) 

965 except Exception: 

966 if default is not None: 

967 return default 

968 raise 

969 

970 return self._string_to_value(valuestr) 

971 

972 def get_values( 

973 self, 

974 section: str, 

975 option: str, 

976 default: Union[int, float, str, bool, None] = None, 

977 ) -> List[Union[int, float, str, bool]]: 

978 """Get an option's values. 

979 

980 If multiple values are specified for this option in the section, all are 

981 returned. 

982 

983 :param default: 

984 If not ``None``, a list containing the given default value will be returned 

985 in case the option did not exist. 

986 

987 :return: 

988 A list of properly typed values, either int, float, string or bool 

989 

990 :raise TypeError: 

991 In case the value could not be understood. 

992 Otherwise the exceptions known to the ConfigParser will be raised. 

993 """ 

994 try: 

995 self.sections() 

996 lst = self._sections[section].getall(option) 

997 except Exception: 

998 if default is not None: 

999 return [default] 

1000 raise 

1001 

1002 return [self._string_to_value(valuestr) for valuestr in lst] 

1003 

1004 def _convert_to_boolean(self, value: Union[str, None]) -> bool: 

1005 if value is None: 

1006 return True 

1007 if value == "": 

1008 return False 

1009 try: 

1010 return self.BOOLEAN_STATES[value.lower()] 

1011 except KeyError: 

1012 raise ValueError("Not a boolean: %s" % value) from None 

1013 

1014 def _string_to_value(self, valuestr: Union[str, None]) -> Union[int, float, str, bool]: 

1015 if valuestr is None: 

1016 return "" 

1017 types = (int, float) 

1018 for numtype in types: 

1019 try: 

1020 val = numtype(valuestr) 

1021 # truncated value ? 

1022 if val != float(valuestr): 

1023 continue 

1024 return val 

1025 except (ValueError, TypeError): 

1026 continue 

1027 # END for each numeric type 

1028 

1029 # Try boolean values as git uses them. 

1030 vl = valuestr.lower() 

1031 if vl in ("false", "no", "off"): 

1032 return False 

1033 if vl in ("true", "yes", "on"): 

1034 return True 

1035 

1036 if not isinstance(valuestr, str): 

1037 raise TypeError( 

1038 "Invalid value type: only int, long, float and str are allowed", 

1039 valuestr, 

1040 ) 

1041 

1042 return valuestr 

1043 

1044 def _value_to_string(self, value: Union[str, bytes, int, float, bool]) -> str: 

1045 if isinstance(value, (int, float, bool)): 

1046 return str(value) 

1047 return force_text(value) 

1048 

1049 def _value_to_string_safe(self, value: Union[str, bytes, int, float, bool]) -> str: 

1050 value_str = self._value_to_string(value) 

1051 if UNSAFE_CONFIG_CHARS_RE.search(value_str): 

1052 raise ValueError("Git config values must not contain CR, LF, or NUL") 

1053 return value_str 

1054 

1055 def _assure_config_name_safe(self, name: "cp._SectionName", label: str) -> None: 

1056 if isinstance(name, str) and UNSAFE_CONFIG_CHARS_RE.search(name): 

1057 raise ValueError("Git config %s names must not contain CR, LF, or NUL" % label) 

1058 if label == "option" and isinstance(name, str) and not VALID_CONFIG_OPTION_NAME_RE.fullmatch(name): 

1059 raise ValueError("Git config option names may contain only letters, digits, '-', '_', or '.'") 

1060 if label == "section" and isinstance(name, str): 

1061 in_quotes = False 

1062 escaped = False 

1063 for index, char in enumerate(name): 

1064 if escaped: 

1065 escaped = False 

1066 elif in_quotes and char == "\\": 

1067 escaped = True 

1068 elif char == '"': 

1069 if not in_quotes and (index == 0 or name[index - 1] not in " \t"): 

1070 raise ValueError("Git config quoted subsection names must begin after whitespace") 

1071 in_quotes = not in_quotes 

1072 elif char == "]" and not in_quotes: 

1073 raise ValueError("Git config section names must not contain an unquoted closing bracket") 

1074 if in_quotes: 

1075 raise ValueError("Git config section names must not contain an unterminated quote") 

1076 

1077 @needs_values 

1078 @set_dirty_and_flush_changes 

1079 def set( 

1080 self, 

1081 section: str, 

1082 option: str, 

1083 value: Union[str, bytes, int, float, bool, None] = None, 

1084 ) -> None: 

1085 self._assure_config_name_safe(section, "section") 

1086 self._assure_config_name_safe(option, "option") 

1087 if value is not None: 

1088 value = self._value_to_string_safe(value) 

1089 return super().set(section, option, value) 

1090 

1091 @needs_values 

1092 @set_dirty_and_flush_changes 

1093 def set_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser": 

1094 """Set the given option in section to the given value. 

1095 

1096 This will create the section if required, and will not throw as opposed to the 

1097 default ConfigParser ``set`` method. 

1098 

1099 :param section: 

1100 Name of the section in which the option resides or should reside. 

1101 

1102 :param option: 

1103 Name of the options whose value to set. 

1104 

1105 :param value: 

1106 Value to set the option to. It must be a string or convertible to a string. 

1107 

1108 :return: 

1109 This instance 

1110 """ 

1111 self._assure_config_name_safe(section, "section") 

1112 self._assure_config_name_safe(option, "option") 

1113 value_str = self._value_to_string_safe(value) 

1114 if not self.has_section(section): 

1115 self.add_section(section) 

1116 super().set(section, option, value_str) 

1117 return self 

1118 

1119 @needs_values 

1120 @set_dirty_and_flush_changes 

1121 def add_value(self, section: str, option: str, value: Union[str, bytes, int, float, bool]) -> "GitConfigParser": 

1122 """Add a value for the given option in section. 

1123 

1124 This will create the section if required, and will not throw as opposed to the 

1125 default ConfigParser ``set`` method. The value becomes the new value of the 

1126 option as returned by :meth:`get_value`, and appends to the list of values 

1127 returned by :meth:`get_values`. 

1128 

1129 :param section: 

1130 Name of the section in which the option resides or should reside. 

1131 

1132 :param option: 

1133 Name of the option. 

1134 

1135 :param value: 

1136 Value to add to option. It must be a string or convertible to a string. 

1137 

1138 :return: 

1139 This instance 

1140 """ 

1141 self._assure_config_name_safe(section, "section") 

1142 self._assure_config_name_safe(option, "option") 

1143 value_str = self._value_to_string_safe(value) 

1144 if not self.has_section(section): 

1145 self.add_section(section) 

1146 self._sections[section].add(option, value_str) 

1147 return self 

1148 

1149 def rename_section(self, section: str, new_name: str) -> "GitConfigParser": 

1150 """Rename the given section to `new_name`. 

1151 

1152 :raise ValueError: 

1153 If: 

1154 

1155 * `section` doesn't exist. 

1156 * A section with `new_name` does already exist. 

1157 

1158 :return: 

1159 This instance 

1160 """ 

1161 if not self.has_section(section): 

1162 raise ValueError("Source section '%s' doesn't exist" % section) 

1163 self._assure_config_name_safe(new_name, "section") 

1164 if self.has_section(new_name): 

1165 raise ValueError("Destination section '%s' already exists" % new_name) 

1166 

1167 super().add_section(new_name) 

1168 new_section = self._sections[new_name] 

1169 for k, vs in self.items_all(section): 

1170 new_section.setall(k, vs) 

1171 # END for each value to copy 

1172 

1173 # This call writes back the changes, which is why we don't have the respective 

1174 # decorator. 

1175 self.remove_section(section) 

1176 return self