| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 9513aa0 commit 3fd3723
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -9,24 +9,67 @@ | |||
| 9 | 9 | """ | |
| 10 | 10 | ||
| 11 | 11 | import re | |
| 12 | - from ConfigParser import RawConfigParser | ||
| 12 | + import os | ||
| 13 | + import ConfigParser as cp | ||
| 14 | + from git.odict import OrderedDict | ||
| 15 | + import inspect | ||
| 13 | 16 | ||
| 14 | 17 | class _MetaParserBuilder(type): | |
| 15 | 18 | """ | |
| 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 | ||
| 17 | 20 | """ | |
| 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 | + | ||
| 18 | 44 | ||
| 19 | 45 | def _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 | ||
| 22 | 55 | ||
| 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 | ||
| 26 | 69 | ||
| 27 | 70 | ||
| 28 | 71 | ||
| 29 | - class GitConfigParser(RawConfigParser, object): | ||
| 72 | + class GitConfigParser(cp.RawConfigParser, object): | ||
| 30 | 73 | """ | |
| 31 | 74 | Implements specifics required to read git style configuration files. | |
| 32 | 75 | ||
@@ -38,6 +81,10 @@ class GitConfigParser(RawConfigParser, object): | |||
| 38 | 81 | ||
| 39 | 82 | The configuration file will be locked if you intend to change values preventing other | |
| 40 | 83 | 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. | ||
| 41 | 88 | """ | |
| 42 | 89 | __metaclass__ = _MetaParserBuilder | |
| 43 | 90 | ||
@@ -51,45 +98,214 @@ class GitConfigParser(RawConfigParser, object): | |||
| 51 | 98 | ) | |
| 52 | 99 | ||
| 53 | 100 | # list of RawConfigParser methods able to change the instance | |
| 54 | - _mutating_methods_ = tuple() | ||
| 55 | - | ||
| 101 | + _mutating_methods_ = ("remove_section", "remove_option", "set") | ||
| 56 | 102 | ||
| 57 | 103 | def __init__(self, file_or_files, read_only=True): | |
| 58 | 104 | """ | |
| 59 | 105 | 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. | ||
| 61 | 114 | """ | |
| 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 | + | ||
| 62 | 120 | self._file_or_files = file_or_files | |
| 63 | 121 | self._read_only = read_only | |
| 122 | + self._owns_lock = False | ||
| 64 | 123 | 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 | + | ||
| 66 | 139 | ||
| 67 | 140 | def __del__(self): | |
| 68 | 141 | """ | |
| 69 | 142 | Write pending changes if required and release locks | |
| 70 | 143 | """ | |
| 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 | ||
| 71 | 221 | ||
| 72 | 222 | def read(self): | |
| 73 | 223 | """ | |
| 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 | ||
| 75 | 231 | """ | |
| 76 | 232 | 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 ] | ||
| 78 | 239 | ||
| 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 | ||
| 79 | 256 | self._is_initialized = True | |
| 80 | 257 | ||
| 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 | + | ||
| 82 | 274 | def write(self): | |
| 83 | 275 | """ | |
| 84 | - Write our changes to our file | ||
| 276 | + Write changes to our file, if there are changes at all | ||
| 85 | 277 | ||
| 86 | 278 | 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 | ||
| 88 | 281 | """ | |
| 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 | ||
| 91 | 305 | ||
| 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)) | ||
| 93 | 309 | ||
| 94 | 310 | @property | |
| 95 | 311 | def read_only(self): | |
| Back | FazBrowse Home | New Git URL |
0 commit comments