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."""
177184cp .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):
183190self ._file_or_files = file_or_files
184191self ._read_only = read_only
185192self ._is_initialized = False
193+ self ._merge_includes = merge_includes
186194self ._lock = None
187195
188196if not read_only :
@@ -313,7 +321,6 @@ def string_decode(v):
313321if not e :
314322e = cp .ParsingError (fpname )
315323e .append (lineno ,repr (line ))
316- print (lineno ,line )
317324continue
318325else :
319326line = line .rstrip ()
@@ -329,6 +336,9 @@ def string_decode(v):
329336if e :
330337raise e
331338
339+ def _has_includes (self ):
340+ return self ._merge_includes and self .has_section ('include' )
341+
332342def 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"""
338348if self ._is_initialized :
339349return
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
347363close_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" ):
350367try :
351- fp = open (file_object ,'rb' )
368+ fp = open (file_path ,'rb' )
352369close_fp = True
353370except IOError :
354371continue
@@ -360,8 +377,33 @@ def read(self):
360377if close_fp :
361378fp .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
366408def _write (self ,fp ):
367409"""Write an .ini-format representation of the configuration state in
@@ -379,6 +421,10 @@ def write_section(name, section_dict):
379421for name ,value in self ._sections .items ():
380422write_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
383429def 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"""
388434self ._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+
390447fp = self ._file_or_files
391448close_fp = False
392449