4545 from git .repo .base import Repo
4646
4747T_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 )
4949
5050if sys .version_info [:3 ] < (3 , 7 , 2 ):
5151 # typing.Ordereddict not added until Python 3.7.2.
@@ -291,6 +291,12 @@ class GitConfigParser(cp.RawConfigParser, metaclass=MetaParserBuilder):
291291
292292 :note:
293293 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`.
294300 """
295301
296302 # { Configuration
@@ -348,7 +354,7 @@ def __init__(
348354 Reference to repository to use if ``[includeIf]`` sections are found in
349355 configuration files.
350356 """
351- cp .RawConfigParser .__init__ (self , dict_type = _OMD )
357+ cp .RawConfigParser .__init__ (self , dict_type = _OMD , allow_no_value = True )
352358 self ._dict : Callable [..., _OMD ]
353359 self ._defaults : _OMD
354360 self ._sections : _OMD
@@ -587,8 +593,12 @@ def parse_value(value: str) -> str:
587593 # Preserves multiple values for duplicate optnames.
588594 cursect .add (optname , optval )
589595 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 :
592602 if not e :
593603 e = cp .ParsingError (fpname )
594604 e .append (lineno , repr (line ))
@@ -625,6 +635,7 @@ def _all_items(section: str) -> List[Tuple[str, str]]:
625635 for key , values in self ._sections [section ].items_all ()
626636 if key != "__name__"
627637 for value in values
638+ if value is not None
628639 ]
629640
630641 paths = []
@@ -760,13 +771,16 @@ def _write(self, fp: IO) -> None:
760771 def write_section (name : str , section_dict : _OMD ) -> None :
761772 fp .write (("[%s]\n " % name ).encode (defenc ))
762773
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
765776 for key , values in section_dict .items_all ():
766777 if key == "__name__" :
767778 continue
768779
769780 for v in values :
781+ if v is None :
782+ fp .write (("\t %s\n " % key ).encode (defenc ))
783+ continue
770784 value = self ._value_to_string (v )
771785 if any (char in value for char in '\n \t \b \\ "#;' ) or value [:1 ].isspace () or value [- 1 :].isspace ():
772786 value = value .replace ("\\ " , "\\ \\ " ).replace ('"' , '\\ "' )
@@ -783,11 +797,11 @@ def write_section(name: str, section_dict: _OMD) -> None:
783797 for name , value in self ._sections .items ():
784798 write_section (name , value )
785799
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]
787801 """:return: list((option, value), ...) pairs of all items in the given section"""
788802 return [(k , v ) for k , v in super ().items (section_name ) if k != "__name__" ]
789803
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 ] ]]]:
791805 """:return: list((option, [values...]), ...) pairs of all items in the given section"""
792806 rv = _OMD (self ._defaults )
793807
@@ -841,6 +855,8 @@ def write(self) -> None:
841855 for key , values in section .items_all ():
842856 if key != "__name__" :
843857 for raw_value in values :
858+ if raw_value is None :
859+ continue
844860 if "\r " in self ._value_to_string (raw_value ) or "\x00 " in self ._value_to_string (raw_value ):
845861 raise ValueError ("Git config values must not contain CR or NUL" )
846862
@@ -877,7 +893,6 @@ def read_only(self) -> bool:
877893 """:return: ``True`` if this instance may change the configuration file"""
878894 return self ._read_only
879895
880- # FIXME: Figure out if default or return type can really include bool.
881896 def get_value (
882897 self ,
883898 section : str ,
@@ -894,7 +909,7 @@ def get_value(
894909 did not exist.
895910
896911 :return:
897- A properly typed value, either int, float or string
912+ A properly typed value, either int, float, string or bool
898913
899914 :raise TypeError:
900915 In case the value could not be understood.
@@ -925,7 +940,7 @@ def get_values(
925940 in case the option did not exist.
926941
927942 :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
929944
930945 :raise TypeError:
931946 In case the value could not be understood.
@@ -941,7 +956,19 @@ def get_values(
941956
942957 return [self ._string_to_value (valuestr ) for valuestr in lst ]
943958
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 ""
945972 types = (int , float )
946973 for numtype in types :
947974 try :
0 commit comments