| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -45,7 +45,7 @@ | |||
| 45 | 45 | from git.repo.base import Repo | |
| 46 | 46 | ||
| 47 | 47 | T_ConfigParser = TypeVar("T_ConfigParser", bound="GitConfigParser") | |
| 48 | - T_OMD_value = TypeVar("T_OMD_value", str, bytes, int, float, bool) | ||
| 48 | + T_OMD_value = TypeVar("T_OMD_value", str, bytes, int, float, bool, None) | ||
| 49 | 49 | ||
| 50 | 50 | if sys.version_info[:3] < (3, 7, 2): | |
| 51 | 51 | # typing.Ordereddict not added until Python 3.7.2. | |
@@ -291,6 +291,12 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder): | |||
| 291 | 291 | ||
| 292 | 292 | :note: | |
| 293 | 293 | If used as a context manager, this will release the locked file. | |
| 294 | + | ||
| 295 | + :note: | ||
| 296 | + Options without a value are stored as ``None`` and written without ``=``. | ||
| 297 | + :meth:`get_value` and :meth:`get_values` return an empty string for them, | ||
| 298 | + while :meth:`getboolean` returns ``True``. An explicit empty value is | ||
| 299 | + stored as an empty string and reads as ``False`` with :meth:`getboolean`. | ||
| 294 | 300 | """ | |
| 295 | 301 | ||
| 296 | 302 | # { Configuration | |
@@ -348,7 +354,7 @@ def __init__( | |||
| 348 | 354 | Reference to repository to use if ``[includeIf]`` sections are found in | |
| 349 | 355 | configuration files. | |
| 350 | 356 | """ | |
| 351 | - cp.RawConfigParser.__init__(self, dict_type=_OMD) | ||
| 357 | + cp.RawConfigParser.__init__(self, dict_type=_OMD, allow_no_value=True) | ||
| 352 | 358 | self._dict: Callable[..., _OMD] | |
| 353 | 359 | self._defaults: _OMD | |
| 354 | 360 | self._sections: _OMD | |
@@ -587,8 +593,12 @@ def parse_value(value: str) -> str: | |||
| 587 | 593 | # Preserves multiple values for duplicate optnames. | |
| 588 | 594 | cursect.add(optname, optval) | |
| 589 | 595 | else: | |
| 590 | - # Check if it's an option with no value - it's just ignored by git. | ||
| 591 | - if not self.OPTVALUEONLY.match(line): | ||
| 596 | + # A valueless option is an implicit boolean true, not an empty value. | ||
| 597 | + mo = self.OPTVALUEONLY.match(line) | ||
| 598 | + if mo: | ||
| 599 | + optname = self.optionxform(mo.group("option").rstrip()) | ||
| 600 | + cursect.add(optname, None) | ||
| 601 | + else: | ||
| 592 | 602 | if not e: | |
| 593 | 603 | e = cp.ParsingError(fpname) | |
| 594 | 604 | e.append(lineno, repr(line)) | |
@@ -625,6 +635,7 @@ def _all_items(section: str) -> List[Tuple[str, str]]: | |||
| 625 | 635 | for key, values in self._sections[section].items_all() | |
| 626 | 636 | if key != "__name__" | |
| 627 | 637 | for value in values | |
| 638 | + if value is not None | ||
| 628 | 639 | ] | |
| 629 | 640 | ||
| 630 | 641 | paths = [] | |
@@ -760,13 +771,16 @@ def _write(self, fp: IO) -> None: | |||
| 760 | 771 | def write_section(name: str, section_dict: _OMD) -> None: | |
| 761 | 772 | fp.write(("[%s]\n" % name).encode(defenc)) | |
| 762 | 773 | ||
| 763 | - values: Sequence[str] # Runtime only gets str in tests, but should be whatever _OMD stores. | ||
| 764 | - v: str | ||
| 774 | + values: List[Any] | ||
| 775 | + v: Any | ||
| 765 | 776 | for key, values in section_dict.items_all(): | |
| 766 | 777 | if key == "__name__": | |
| 767 | 778 | continue | |
| 768 | 779 | ||
| 769 | 780 | for v in values: | |
| 781 | + if v is None: | ||
| 782 | + fp.write(("\t%s\n" % key).encode(defenc)) | ||
| 783 | + continue | ||
| 770 | 784 | value = self._value_to_string(v) | |
| 771 | 785 | if any(char in value for char in '\n\t\b\\"#;') or value[:1].isspace() or value[-1:].isspace(): | |
| 772 | 786 | value = value.replace("\\", "\\\\").replace('"', '\\"') | |
@@ -783,11 +797,11 @@ def write_section(name: str, section_dict: _OMD) -> None: | |||
| 783 | 797 | for name, value in self._sections.items(): | |
| 784 | 798 | write_section(name, value) | |
| 785 | 799 | ||
| 786 | - def items(self, section_name: str) -> List[Tuple[str, str]]: # type: ignore[override] | ||
| 800 | + def items(self, section_name: str) -> List[Tuple[str, Union[str, None]]]: # type: ignore[override] | ||
| 787 | 801 | """:return: list((option, value), ...) pairs of all items in the given section""" | |
| 788 | 802 | return [(k, v) for k, v in super().items(section_name) if k != "__name__"] | |
| 789 | 803 | ||
| 790 | - def items_all(self, section_name: str) -> List[Tuple[str, List[str]]]: | ||
| 804 | + def items_all(self, section_name: str) -> List[Tuple[str, List[Union[str, None]]]]: | ||
| 791 | 805 | """:return: list((option, [values...]), ...) pairs of all items in the given section""" | |
| 792 | 806 | rv = _OMD(self._defaults) | |
| 793 | 807 | ||
@@ -841,6 +855,8 @@ def write(self) -> None: | |||
| 841 | 855 | for key, values in section.items_all(): | |
| 842 | 856 | if key != "__name__": | |
| 843 | 857 | for raw_value in values: | |
| 858 | + if raw_value is None: | ||
| 859 | + continue | ||
| 844 | 860 | if "\r" in self._value_to_string(raw_value) or "\x00" in self._value_to_string(raw_value): | |
| 845 | 861 | raise ValueError("Git config values must not contain CR or NUL") | |
| 846 | 862 | ||
@@ -877,7 +893,6 @@ def read_only(self) -> bool: | |||
| 877 | 893 | """:return: ``True`` if this instance may change the configuration file""" | |
| 878 | 894 | return self._read_only | |
| 879 | 895 | ||
| 880 | - # FIXME: Figure out if default or return type can really include bool. | ||
| 881 | 896 | def get_value( | |
| 882 | 897 | self, | |
| 883 | 898 | section: str, | |
@@ -894,7 +909,7 @@ def get_value( | |||
| 894 | 909 | did not exist. | |
| 895 | 910 | ||
| 896 | 911 | :return: | |
| 897 | - A properly typed value, either int, float or string | ||
| 912 | + A properly typed value, either int, float, string or bool | ||
| 898 | 913 | ||
| 899 | 914 | :raise TypeError: | |
| 900 | 915 | In case the value could not be understood. | |
@@ -925,7 +940,7 @@ def get_values( | |||
| 925 | 940 | in case the option did not exist. | |
| 926 | 941 | ||
| 927 | 942 | :return: | |
| 928 | - A list of properly typed values, either int, float or string | ||
| 943 | + A list of properly typed values, either int, float, string or bool | ||
| 929 | 944 | ||
| 930 | 945 | :raise TypeError: | |
| 931 | 946 | In case the value could not be understood. | |
@@ -941,7 +956,19 @@ def get_values( | |||
| 941 | 956 | ||
| 942 | 957 | return [self._string_to_value(valuestr) for valuestr in lst] | |
| 943 | 958 | ||
| 944 | - def _string_to_value(self, valuestr: str) -> Union[int, float, str, bool]: | ||
| 959 | + def _convert_to_boolean(self, value: Union[str, None]) -> bool: | ||
| 960 | + if value is None: | ||
| 961 | + return True | ||
| 962 | + if value == "": | ||
| 963 | + return False | ||
| 964 | + try: | ||
| 965 | + return self.BOOLEAN_STATES[value.lower()] | ||
| 966 | + except KeyError: | ||
| 967 | + raise ValueError("Not a boolean: %s" % value) from None | ||
| 968 | + | ||
| 969 | + def _string_to_value(self, valuestr: Union[str, None]) -> Union[int, float, str, bool]: | ||
| 970 | + if valuestr is None: | ||
| 971 | + return "" | ||
| 945 | 972 | types = (int, float) | |
| 946 | 973 | for numtype in types: | |
| 947 | 974 | try: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -864,8 +864,58 @@ def test_empty_config_value(self): | |||
| 864 | 864 | ||
| 865 | 865 | assert cr.get_value("core", "filemode"), "Should read keys with values" | |
| 866 | 866 | ||
| 867 | - with self.assertRaises(cp.NoOptionError): | ||
| 868 | - cr.get_value("color", "ui") | ||
| 867 | + self.assertTrue(cr.has_option("color", "ui")) | ||
| 868 | + self.assertIsNone(cr.get("color", "ui")) | ||
| 869 | + self.assertEqual(cr.get_value("color", "ui"), "") | ||
| 870 | + self.assertIs(cr.getboolean("color", "ui"), True) | ||
| 871 | + | ||
| 872 | + @with_rw_directory | ||
| 873 | + def test_implicit_boolean_round_trip(self, rw_dir): | ||
| 874 | + config_path = osp.join(rw_dir, "config") | ||
| 875 | + with open(config_path, "wb") as config_file: | ||
| 876 | + config_file.write( | ||
| 877 | + b"[include]\n" | ||
| 878 | + b"\toptional\n" | ||
| 879 | + b"[flag]\n" | ||
| 880 | + b"\timplicit\n" | ||
| 881 | + b"\ttrailing-space \n" | ||
| 882 | + b"\ttrailing-tab\t\n" | ||
| 883 | + b"\tempty =\n" | ||
| 884 | + b'\tquoted = ""\n' | ||
| 885 | + b"\tmultiple = false\n" | ||
| 886 | + b"\tmultiple\n" | ||
| 887 | + b"\tmultiple =\n" | ||
| 888 | + b"\tmultiple" | ||
| 889 | + ) | ||
| 890 | + git_config = ["git", "config", "--file", config_path] | ||
| 891 | + original = subprocess.check_output(git_config + ["--null", "--list"]) | ||
| 892 | + | ||
| 893 | + with GitConfigParser(config_path, read_only=False) as config: | ||
| 894 | + for option in ("implicit", "trailing-space", "trailing-tab"): | ||
| 895 | + self.assertIsNone(config.get("flag", option)) | ||
| 896 | + self.assertEqual(config.get_value("flag", option), "") | ||
| 897 | + self.assertIs(config.getboolean("flag", option), True) | ||
| 898 | + for option in ("empty", "quoted"): | ||
| 899 | + self.assertEqual(config.get("flag", option), "") | ||
| 900 | + self.assertEqual(config.get_value("flag", option), "") | ||
| 901 | + self.assertIs(config.getboolean("flag", option), False) | ||
| 902 | + self.assertEqual(config.get_values("flag", "multiple"), [False, "", "", ""]) | ||
| 903 | + self.assertIsNone(dict(config.items("flag"))["multiple"]) | ||
| 904 | + self.assertEqual(dict(config.items_all("flag"))["multiple"], ["false", None, "", None]) | ||
| 905 | + config.set_value("other", "value", "updated") | ||
| 906 | + | ||
| 907 | + self.assertEqual( | ||
| 908 | + subprocess.check_output(git_config + ["--null", "--list"]), | ||
| 909 | + original + b"other.value\nupdated\0", | ||
| 910 | + ) | ||
| 911 | + self.assertEqual( | ||
| 912 | + subprocess.check_output(git_config + ["--type=bool", "--get-all", "flag.multiple"]), | ||
| 913 | + b"false\ntrue\nfalse\ntrue\n", | ||
| 914 | + ) | ||
| 915 | + with GitConfigParser(config_path) as config: | ||
| 916 | + self.assertIs(config.getboolean("flag", "implicit"), True) | ||
| 917 | + self.assertIs(config.getboolean("flag", "empty"), False) | ||
| 918 | + self.assertEqual(dict(config.items_all("flag"))["multiple"], ["false", None, "", None]) | ||
| 869 | 919 | ||
| 870 | 920 | def test_config_with_quotes(self): | |
| 871 | 921 | cr = GitConfigParser(fixture_path("git_config_with_quotes"), read_only=True) | |
| Back | FazBrowse Home | New Git URL |
0 commit comments