1515import inspect
1616import logging
1717import abc
18+ import os
1819
1920from git .odict import OrderedDict
2021from git .util import LockFile
@@ -164,7 +165,7 @@ class GitConfigParser(with_metaclass(MetaParserBuilder, cp.RawConfigParser, obje
164165 # list of RawConfigParser methods able to change the instance
165166 _mutating_methods_ = ("add_section" , "remove_section" , "remove_option" , "set" )
166167
167- def __init__ (self , file_or_files , read_only = True ):
168+ def __init__ (self , file_or_files , read_only = True , merge_includes = True ):
168169 """Initialize a configuration reader to read the given file_or_files and to
169170 possibly allow changes to it by setting read_only False
170171
@@ -173,7 +174,13 @@ def __init__(self, file_or_files, read_only=True):
173174
174175 :param read_only:
175176 If True, the ConfigParser may only read the data , but not change it.
176- If False, only a single file path or file object may be given."""
177+ If False, only a single file path or file object may be given. We will write back the changes
178+ when they happen, or when the ConfigParser is released. This will not happen if other
179+ configuration files have been included
180+ :param merge_includes: if True, we will read files mentioned in [include] sections and merge their
181+ contents into ours. This makes it impossible to write back an individual configuration file.
182+ Thus, if you want to modify a single conifguration file, turn this off to leave the original
183+ dataset unaltered when reading it."""
177184 cp .RawConfigParser .__init__ (self , dict_type = OrderedDict )
178185
179186 # Used in python 3, needs to stay in sync with sections for underlying implementation to work
@@ -183,6 +190,7 @@ def __init__(self, file_or_files, read_only=True):
183190 self ._file_or_files = file_or_files
184191 self ._read_only = read_only
185192 self ._is_initialized = False
193+ self ._merge_includes = merge_includes
186194 self ._lock = None
187195
188196 if not read_only :
@@ -313,7 +321,6 @@ def string_decode(v):
313321 if not e :
314322 e = cp .ParsingError (fpname )
315323 e .append (lineno , repr (line ))
316- print (lineno , line )
317324 continue
318325 else :
319326 line = line .rstrip ()
@@ -329,6 +336,9 @@ def string_decode(v):
329336 if e :
330337 raise e
331338
339+ def _has_includes (self ):
340+ return self ._merge_includes and self .has_section ('include' )
341+
332342 def read (self ):
333343 """Reads the data stored in the files we have been initialized with. It will
334344 ignore files that cannot be read, possibly leaving an empty configuration
@@ -337,18 +347,25 @@ def read(self):
337347 :raise IOError: if a file cannot be handled"""
338348 if self ._is_initialized :
339349 return
350+ self ._is_initialized = True
340351
341- files_to_read = self ._file_or_files
342- if not isinstance (files_to_read , (tuple , list )):
343- files_to_read = [files_to_read ]
344-
345- for file_object in files_to_read :
346- fp = file_object
352+ if not isinstance (self ._file_or_files , (tuple , list )):
353+ files_to_read = [self ._file_or_files ]
354+ else :
355+ files_to_read = list (self ._file_or_files )
356+ # end assure we have a copy of the paths to handle
357+
358+ seen = set (files_to_read )
359+ num_read_include_files = 0
360+ while files_to_read :
361+ file_path = files_to_read .pop (0 )
362+ fp = file_path
347363 close_fp = False
364+
348365 # assume a path if it is not a file-object
349- if not hasattr (file_object , "seek" ):
366+ if not hasattr (fp , "seek" ):
350367 try :
351- fp = open (file_object , 'rb' )
368+ fp = open (file_path , 'rb' )
352369 close_fp = True
353370 except IOError :
354371 continue
@@ -360,8 +377,33 @@ def read(self):
360377 if close_fp :
361378 fp .close ()
362379 # END read-handling
363- # END for each file object to read
364- self ._is_initialized = True
380+
381+ # Read includes and append those that we didn't handle yet
382+ # We expect all paths to be normalized and absolute (and will assure that is the case)
383+ if self ._has_includes ():
384+ for _ , include_path in self .items ('include' ):
385+ if not os .path .isabs (include_path ):
386+ if not close_fp :
387+ continue
388+ # end ignore relative paths if we don't know the configuration file path
389+ assert os .path .isabs (file_path ), "Need absolute paths to be sure our cycle checks will work"
390+ include_path = os .path .join (os .path .dirname (file_path ), include_path )
391+ # end make include path absolute
392+ include_path = os .path .normpath (include_path )
393+ if include_path in seen or not os .access (include_path , os .R_OK ):
394+ continue
395+ seen .add (include_path )
396+ files_to_read .append (include_path )
397+ num_read_include_files += 1
398+ # each include path in configuration file
399+ # end handle includes
400+ # END for each file object to read
401+
402+ # If there was no file included, we can safely write back (potentially) the configuration file
403+ # without altering it's meaning
404+ if num_read_include_files == 0 :
405+ self ._merge_includes = False
406+ # end
365407
366408 def _write (self , fp ):
367409 """Write an .ini-format representation of the configuration state in
@@ -379,6 +421,10 @@ def write_section(name, section_dict):
379421 for name , value in self ._sections .items ():
380422 write_section (name , value )
381423
424+ def items (self , section_name ):
425+ """:return: list((option, value), ...) pairs of all items in the given section"""
426+ return [(k , v ) for k , v in super (GitConfigParser , self ).items (section_name ) if k != '__name__' ]
427+
382428 @needs_values
383429 def write (self ):
384430 """Write changes to our file, if there are changes at all
@@ -387,6 +433,17 @@ def write(self):
387433 a file lock"""
388434 self ._assure_writable ("write" )
389435
436+ if isinstance (self ._file_or_files , (list , tuple )):
437+ raise AssertionError ("Cannot write back if there is not exactly a single file to write to, have %i files"
438+ % len (self ._file_or_files ))
439+ # end assert multiple files
440+
441+ if self ._has_includes ():
442+ log .debug ("Skipping write-back of confiuration file as include files were merged in." +
443+ "Set merge_includes=False to prevent this." )
444+ return
445+ # end
446+
390447 fp = self ._file_or_files
391448 close_fp = False
392449
0 commit comments