config.py
347 lines
| 10.9 KiB
| text/x-python
|
PythonLexer
r2007 | # config.py - Reading and writing Git config files | |||
# Copyright (C) 2011 Jelmer Vernooij <jelmer@samba.org> | ||||
# | ||||
# This program is free software; you can redistribute it and/or | ||||
# modify it under the terms of the GNU General Public License | ||||
# as published by the Free Software Foundation; version 2 | ||||
# of the License or (at your option) a later version. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU General Public License | ||||
# along with this program; if not, write to the Free Software | ||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, | ||||
# MA 02110-1301, USA. | ||||
"""Reading and writing Git configuration files. | ||||
TODO: | ||||
* preserve formatting when updating configuration files | ||||
* treat subsection names as case-insensitive for [branch.foo] style | ||||
subsections | ||||
""" | ||||
# Taken from dulwich not yet released 0.8.3 version (until it is actually | ||||
# released) | ||||
import errno | ||||
import os | ||||
import re | ||||
from dulwich.file import GitFile | ||||
class Config(object): | ||||
"""A Git configuration.""" | ||||
def get(self, section, name): | ||||
"""Retrieve the contents of a configuration setting. | ||||
:param section: Tuple with section name and optional subsection namee | ||||
:param subsection: Subsection name | ||||
:return: Contents of the setting | ||||
:raise KeyError: if the value is not set | ||||
""" | ||||
raise NotImplementedError(self.get) | ||||
def get_boolean(self, section, name, default=None): | ||||
"""Retrieve a configuration setting as boolean. | ||||
:param section: Tuple with section name and optional subsection namee | ||||
:param name: Name of the setting, including section and possible | ||||
subsection. | ||||
:return: Contents of the setting | ||||
:raise KeyError: if the value is not set | ||||
""" | ||||
try: | ||||
value = self.get(section, name) | ||||
except KeyError: | ||||
return default | ||||
if value.lower() == "true": | ||||
return True | ||||
elif value.lower() == "false": | ||||
return False | ||||
raise ValueError("not a valid boolean string: %r" % value) | ||||
def set(self, section, name, value): | ||||
"""Set a configuration value. | ||||
:param name: Name of the configuration value, including section | ||||
and optional subsection | ||||
:param: Value of the setting | ||||
""" | ||||
raise NotImplementedError(self.set) | ||||
class ConfigDict(Config): | ||||
"""Git configuration stored in a dictionary.""" | ||||
def __init__(self, values=None): | ||||
"""Create a new ConfigDict.""" | ||||
if values is None: | ||||
values = {} | ||||
self._values = values | ||||
def __repr__(self): | ||||
return "%s(%r)" % (self.__class__.__name__, self._values) | ||||
def __eq__(self, other): | ||||
return ( | ||||
isinstance(other, self.__class__) and | ||||
other._values == self._values) | ||||
@classmethod | ||||
def _parse_setting(cls, name): | ||||
parts = name.split(".") | ||||
if len(parts) == 3: | ||||
return (parts[0], parts[1], parts[2]) | ||||
else: | ||||
return (parts[0], None, parts[1]) | ||||
def get(self, section, name): | ||||
if isinstance(section, basestring): | ||||
section = (section, ) | ||||
if len(section) > 1: | ||||
try: | ||||
return self._values[section][name] | ||||
except KeyError: | ||||
pass | ||||
return self._values[(section[0],)][name] | ||||
def set(self, section, name, value): | ||||
if isinstance(section, basestring): | ||||
section = (section, ) | ||||
self._values.setdefault(section, {})[name] = value | ||||
def _format_string(value): | ||||
if (value.startswith(" ") or | ||||
value.startswith("\t") or | ||||
value.endswith(" ") or | ||||
value.endswith("\t")): | ||||
return '"%s"' % _escape_value(value) | ||||
return _escape_value(value) | ||||
def _parse_string(value): | ||||
value = value.strip() | ||||
ret = [] | ||||
block = [] | ||||
in_quotes = False | ||||
for c in value: | ||||
if c == "\"": | ||||
in_quotes = (not in_quotes) | ||||
ret.append(_unescape_value("".join(block))) | ||||
block = [] | ||||
elif c in ("#", ";") and not in_quotes: | ||||
# the rest of the line is a comment | ||||
break | ||||
else: | ||||
block.append(c) | ||||
if in_quotes: | ||||
raise ValueError("value starts with quote but lacks end quote") | ||||
ret.append(_unescape_value("".join(block)).rstrip()) | ||||
return "".join(ret) | ||||
def _unescape_value(value): | ||||
"""Unescape a value.""" | ||||
def unescape(c): | ||||
return { | ||||
"\\\\": "\\", | ||||
"\\\"": "\"", | ||||
"\\n": "\n", | ||||
"\\t": "\t", | ||||
"\\b": "\b", | ||||
}[c.group(0)] | ||||
return re.sub(r"(\\.)", unescape, value) | ||||
def _escape_value(value): | ||||
"""Escape a value.""" | ||||
return value.replace("\\", "\\\\").replace("\n", "\\n")\ | ||||
.replace("\t", "\\t").replace("\"", "\\\"") | ||||
def _check_variable_name(name): | ||||
for c in name: | ||||
if not c.isalnum() and c != '-': | ||||
return False | ||||
return True | ||||
def _check_section_name(name): | ||||
for c in name: | ||||
if not c.isalnum() and c not in ('-', '.'): | ||||
return False | ||||
return True | ||||
def _strip_comments(line): | ||||
line = line.split("#")[0] | ||||
line = line.split(";")[0] | ||||
return line | ||||
class ConfigFile(ConfigDict): | ||||
"""A Git configuration file, like .git/config or ~/.gitconfig. | ||||
""" | ||||
@classmethod | ||||
def from_file(cls, f): | ||||
"""Read configuration from a file-like object.""" | ||||
ret = cls() | ||||
section = None | ||||
setting = None | ||||
for lineno, line in enumerate(f.readlines()): | ||||
line = line.lstrip() | ||||
if setting is None: | ||||
if _strip_comments(line).strip() == "": | ||||
continue | ||||
if line[0] == "[": | ||||
line = _strip_comments(line).rstrip() | ||||
if line[-1] != "]": | ||||
raise ValueError("expected trailing ]") | ||||
key = line.strip() | ||||
pts = key[1:-1].split(" ", 1) | ||||
pts[0] = pts[0].lower() | ||||
if len(pts) == 2: | ||||
if pts[1][0] != "\"" or pts[1][-1] != "\"": | ||||
raise ValueError( | ||||
"Invalid subsection " + pts[1]) | ||||
else: | ||||
pts[1] = pts[1][1:-1] | ||||
if not _check_section_name(pts[0]): | ||||
raise ValueError("invalid section name %s" % | ||||
pts[0]) | ||||
section = (pts[0], pts[1]) | ||||
else: | ||||
if not _check_section_name(pts[0]): | ||||
raise ValueError("invalid section name %s" % | ||||
pts[0]) | ||||
pts = pts[0].split(".", 1) | ||||
if len(pts) == 2: | ||||
section = (pts[0], pts[1]) | ||||
else: | ||||
section = (pts[0], ) | ||||
ret._values[section] = {} | ||||
else: | ||||
if section is None: | ||||
raise ValueError("setting %r without section" % line) | ||||
try: | ||||
setting, value = line.split("=", 1) | ||||
except ValueError: | ||||
setting = line | ||||
value = "true" | ||||
setting = setting.strip().lower() | ||||
if not _check_variable_name(setting): | ||||
raise ValueError("invalid variable name %s" % setting) | ||||
if value.endswith("\\\n"): | ||||
value = value[:-2] | ||||
continuation = True | ||||
else: | ||||
continuation = False | ||||
value = _parse_string(value) | ||||
ret._values[section][setting] = value | ||||
if not continuation: | ||||
setting = None | ||||
else: # continuation line | ||||
if line.endswith("\\\n"): | ||||
line = line[:-2] | ||||
continuation = True | ||||
else: | ||||
continuation = False | ||||
value = _parse_string(line) | ||||
ret._values[section][setting] += value | ||||
if not continuation: | ||||
setting = None | ||||
return ret | ||||
@classmethod | ||||
def from_path(cls, path): | ||||
"""Read configuration from a file on disk.""" | ||||
f = GitFile(path, 'rb') | ||||
try: | ||||
ret = cls.from_file(f) | ||||
ret.path = path | ||||
return ret | ||||
finally: | ||||
f.close() | ||||
def write_to_path(self, path=None): | ||||
"""Write configuration to a file on disk.""" | ||||
if path is None: | ||||
path = self.path | ||||
f = GitFile(path, 'wb') | ||||
try: | ||||
self.write_to_file(f) | ||||
finally: | ||||
f.close() | ||||
def write_to_file(self, f): | ||||
"""Write configuration to a file-like object.""" | ||||
for section, values in self._values.iteritems(): | ||||
try: | ||||
section_name, subsection_name = section | ||||
except ValueError: | ||||
(section_name, ) = section | ||||
subsection_name = None | ||||
if subsection_name is None: | ||||
f.write("[%s]\n" % section_name) | ||||
else: | ||||
f.write("[%s \"%s\"]\n" % (section_name, subsection_name)) | ||||
for key, value in values.iteritems(): | ||||
f.write("%s = %s\n" % (key, _escape_value(value))) | ||||
class StackedConfig(Config): | ||||
"""Configuration which reads from multiple config files..""" | ||||
def __init__(self, backends, writable=None): | ||||
self.backends = backends | ||||
self.writable = writable | ||||
def __repr__(self): | ||||
return "<%s for %r>" % (self.__class__.__name__, self.backends) | ||||
@classmethod | ||||
def default_backends(cls): | ||||
"""Retrieve the default configuration. | ||||
This will look in the repository configuration (if for_path is | ||||
specified), the users' home directory and the system | ||||
configuration. | ||||
""" | ||||
paths = [] | ||||
paths.append(os.path.expanduser("~/.gitconfig")) | ||||
paths.append("/etc/gitconfig") | ||||
backends = [] | ||||
for path in paths: | ||||
try: | ||||
cf = ConfigFile.from_path(path) | ||||
except (IOError, OSError), e: | ||||
if e.errno != errno.ENOENT: | ||||
raise | ||||
else: | ||||
continue | ||||
backends.append(cf) | ||||
return backends | ||||
def get(self, section, name): | ||||
for backend in self.backends: | ||||
try: | ||||
return backend.get(section, name) | ||||
except KeyError: | ||||
pass | ||||
raise KeyError(name) | ||||
def set(self, section, name, value): | ||||
if self.writable is None: | ||||
raise NotImplementedError(self.set) | ||||
return self.writable.set(section, name, value) | ||||