99"""
1010
1111import re
12- from ConfigParser import RawConfigParser
12+ import os
13+ import ConfigParser as cp
14+ from git .odict import OrderedDict
15+ import inspect
1316
1417class _MetaParserBuilder (type ):
1518 """
16- Utlity class wrapping methods into decorators that assure read-only properties
19+ Utlity class wrapping base-class methods into decorators that assure read-only properties
1720 """
21+ def __new__ (metacls , name , bases , clsdict ):
22+ """
23+ Equip all base-class methods with a _needs_values decorator, and all non-const methods
24+ with a _set_dirty_and_flush_changes decorator in addition to that.
25+ """
26+ new_type = super (_MetaParserBuilder , metacls ).__new__ (metacls , name , bases , clsdict )
27+
28+ mutating_methods = clsdict ['_mutating_methods_' ]
29+ for base in bases :
30+ methods = ( t for t in inspect .getmembers (base , inspect .ismethod ) if not t [0 ].startswith ("_" ) )
31+ for name , method in methods :
32+ if name in clsdict :
33+ continue
34+ method_with_values = _needs_values (method )
35+ if name in mutating_methods :
36+ method_with_values = _set_dirty_and_flush_changes (method_with_values )
37+ # END mutating methods handling
38+ clsdict [name ] = method_with_values
39+ # END for each base
40+
41+ return new_type
42+
43+
1844
1945def _needs_values (func ):
20- """Returns method assuring we read values (on demand) before we try to access them"""
21- return func
46+ """
47+ Returns method assuring we read values (on demand) before we try to access them
48+ """
49+ def assure_data_present (self , * args , ** kwargs ):
50+ self .read ()
51+ return func (self , * args , ** kwargs )
52+ # END wrapper method
53+ assure_data_present .__name__ = func .__name__
54+ return assure_data_present
2255
23- def _ensure_writable (non_const_func ):
24- """Return method that checks whether given non constant function may be called.
25- If so, the instance will be set dirty"""
56+ def _set_dirty_and_flush_changes (non_const_func ):
57+ """
58+ Return method that checks whether given non constant function may be called.
59+ If so, the instance will be set dirty.
60+ Additionally, we flush the changes right to disk
61+ """
62+ def flush_changes (self , * args , ** kwargs ):
63+ rval = non_const_func (self , * args , ** kwargs )
64+ self .write ()
65+ return rval
66+ # END wrapper method
67+ flush_changes .__name__ = non_const_func .__name__
68+ return flush_changes
2669
2770
2871
29- class GitConfigParser (RawConfigParser , object ):
72+ class GitConfigParser (cp . RawConfigParser , object ):
3073 """
3174 Implements specifics required to read git style configuration files.
3275
@@ -38,6 +81,10 @@ class GitConfigParser(RawConfigParser, object):
3881
3982 The configuration file will be locked if you intend to change values preventing other
4083 instances to write concurrently.
84+
85+ NOTE
86+ The config is case-sensitive even when queried, hence section and option names
87+ must match perfectly.
4188 """
4289 __metaclass__ = _MetaParserBuilder
4390
@@ -51,45 +98,214 @@ class GitConfigParser(RawConfigParser, object):
5198 )
5299
53100 # list of RawConfigParser methods able to change the instance
54- _mutating_methods_ = tuple ()
55-
101+ _mutating_methods_ = ("remove_section" , "remove_option" , "set" )
56102
57103 def __init__ (self , file_or_files , read_only = True ):
58104 """
59105 Initialize a configuration reader to read the given file_or_files and to
60- possibly allow changes to it by setting read_only False
106+ possibly allow changes to it by setting read_only False
107+
108+ ``file_or_files``
109+ A single file path or file objects or multiple of these
110+
111+ ``read_only``
112+ If True, the ConfigParser may only read the data , but not change it.
113+ If False, only a single file path or file object may be given.
61114 """
115+ # initialize base with ordered dictionaries to be sure we write the same
116+ # file back
117+ self ._sections = OrderedDict ()
118+ self ._defaults = OrderedDict ()
119+
62120 self ._file_or_files = file_or_files
63121 self ._read_only = read_only
122+ self ._owns_lock = False
64123 self ._is_initialized = False
65- self ._is_dirty = False
124+
125+
126+ if not read_only :
127+ if isinstance (file_or_files , (tuple , list )):
128+ raise ValueError ("Write-ConfigParsers can operate on a single file only, multiple files have been passed" )
129+ # END single file check
130+
131+ self ._file_name = file_or_files
132+ if not isinstance (self ._file_name , basestring ):
133+ self ._file_name = file_or_files .name
134+ # END get filename
135+
136+ self ._obtain_lock_or_raise ()
137+ # END read-only check
138+
66139
67140 def __del__ (self ):
68141 """
69142 Write pending changes if required and release locks
70143 """
144+ if self .read_only :
145+ return
146+
147+ try :
148+ try :
149+ self .write ()
150+ except IOError ,e :
151+ print "Exception during destruction of GitConfigParser: %s" % str (e )
152+ finally :
153+ self ._release_lock ()
154+
155+ def _lock_file_path (self ):
156+ """
157+ Return
158+ Path to lockfile
159+ """
160+ return "%s.lock" % (self ._file_name )
161+
162+ def _has_lock (self ):
163+ """
164+ Return
165+ True if we have a lock and if the lockfile still exists
166+
167+ Raise
168+ AssertionError if our lock-file does not exist
169+ """
170+ if not self ._owns_lock :
171+ return False
172+
173+ lock_file = self ._lock_file_path ()
174+ try :
175+ fp = open (lock_file , "r" )
176+ pid = int (fp .read ())
177+ fp .close ()
178+ except IOError :
179+ raise AssertionError ("GitConfigParser has a lock but the lock file at %s could not be read" % lock_file )
180+
181+ if pid != os .getpid ():
182+ raise AssertionError ("We claim to own the lock at %s, but it was not owned by our process: %i" % (lock_file , os .getpid ()))
183+
184+ return True
185+
186+ def _obtain_lock_or_raise (self ):
187+ """
188+ Create a lock file as flag for other instances, mark our instance as lock-holder
189+
190+ Raise
191+ IOError if a lock was already present or a lock file could not be written
192+ """
193+ if self ._has_lock ():
194+ return
195+
196+ lock_file = self ._lock_file_path ()
197+ if os .path .exists (lock_file ):
198+ raise IOError ("Lock for file %r did already exist, delete %r in case the lock is illegal" % (self ._file_name , lock_file ))
199+
200+ fp = open (lock_file , "w" )
201+ fp .write (str (os .getpid ()))
202+ fp .close ()
203+
204+ self ._owns_lock = True
205+
206+ def _release_lock (self ):
207+ """
208+ Release our lock if we have one
209+ """
210+ if not self ._has_lock ():
211+ return
212+
213+ os .remove (self ._lock_file_path ())
214+ self ._owns_lock = False
215+
216+ def optionxform (self , optionstr ):
217+ """
218+ Do not transform options in any way when writing
219+ """
220+ return optionstr
71221
72222 def read (self ):
73223 """
74- Read configuration information from our file or files
224+ Reads the data stored in the files we have been initialized with
225+
226+ Returns
227+ Nothing
228+
229+ Raises
230+ IOError if not all files could be read
75231 """
76232 if self ._is_initialized :
77- return
233+ return
234+
235+
236+ files_to_read = self ._file_or_files
237+ if not isinstance (files_to_read , (tuple , list )):
238+ files_to_read = [ files_to_read ]
78239
240+ for file_object in files_to_read :
241+ fp = file_object
242+ close_fp = False
243+ # assume a path if it is not a file-object
244+ if not hasattr (file_object , "seek" ):
245+ fp = open (file_object , "w" )
246+ close_fp = True
247+ # END fp handling
248+
249+ try :
250+ self ._read (fp , fp .name )
251+ finally :
252+ if close_fp :
253+ fp .close ()
254+ # END read-handling
255+ # END for each file object to read
79256 self ._is_initialized = True
80257
81- @_ensure_writable
258+ def _write (self , fp ):
259+ """Write an .ini-format representation of the configuration state in
260+ git compatible format"""
261+ def write_section (name , section_dict ):
262+ fp .write ("[%s]\n " % name )
263+ for (key , value ) in section_dict .items ():
264+ if key != "__name__" :
265+ fp .write ("\t %s = %s\n " % (key , str (value ).replace ('\n ' , '\n \t ' )))
266+ # END if key is not __name__
267+ # END section writing
268+
269+ if self ._defaults :
270+ write_section (cp .DEFAULTSECT , self ._defaults )
271+ map (lambda t : write_section (t [0 ],t [1 ]), self ._sections .items ())
272+
273+
82274 def write (self ):
83275 """
84- Write our changes to our file
276+ Write changes to our file, if there are changes at all
85277
86278 Raise
87- AssertionError if this is a read-only writer instance
279+ IOError if this is a read-only writer instance or if we could not obtain
280+ a file lock
88281 """
89- if not self ._is_dirty :
90- return
282+ self ._assure_writable ("write" )
283+ self ._obtain_lock_or_raise ()
284+
285+
286+ fp = self ._file_or_files
287+ close_fp = False
288+
289+ if not hasattr (fp , "seek" ):
290+ fp = open (self ._file_or_files , "w" )
291+ close_fp = True
292+ else :
293+ fp .seek (0 )
294+
295+ # WRITE DATA
296+ try :
297+ self ._write (fp )
298+ finally :
299+ if close_fp :
300+ fp .close ()
301+ # END data writing
302+
303+ # we do not release the lock - it will be done automatically once the
304+ # instance vanishes
91305
92- self ._is_dirty = False
306+ def _assure_writable (self , method_name ):
307+ if self .read_only :
308+ raise IOError ("Cannot execute non-constant method %s.%s" % (self , method_name ))
93309
94310 @property
95311 def read_only (self ):
0 commit comments