Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: chainedconfigparser.py

Issue 29319007: Issue 2711 - Refactored ChainedConfigParser, allowing manipulation of list items (Closed)
Patch Set: Created June 22, 2015, 9:58 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | packager.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # coding: utf-8 1 # coding: utf-8
2 2
3 # This Source Code Form is subject to the terms of the Mozilla Public 3 # This Source Code Form is subject to the terms of the Mozilla Public
4 # License, v. 2.0. If a copy of the MPL was not distributed with this 4 # License, v. 2.0. If a copy of the MPL was not distributed with this
5 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 6
7 import os, codecs, ConfigParser 7 import os
8 import re
9 import ConfigParser
8 10
9 class Item(tuple): 11 class Item(tuple):
10 def __new__(cls, name, value, source): 12 def __new__(cls, name, value, source):
11 result = super(Item, cls).__new__(cls, (name, value)) 13 result = super(Item, cls).__new__(cls, (name, value))
12 result.source = source 14 result.source = source
13 return result 15 return result
14 16
15 class ChainedConfigParser: 17 class ChainedConfigParser(ConfigParser.SafeConfigParser):
16 """ 18 '''
17 This class provides essentially the same interfaces as SafeConfigParser but 19 This class provides essentially the same interfaces as SafeConfigParser but
18 allows chaining configuration files so that one config file provides the 20 allows chaining configuration files so that one config file provides the
19 default values for the other. To specify the config file to inherit from 21 default values for the other. To specify the config file to inherit from
20 a config file needs to contain the following option: 22 a config file needs to contain the following option:
21 23
22 [default] 24 [default]
23 inherit = foo/bar.config 25 inherit = foo/bar.config
24 26
25 The value of the inherit option has to be a relative path with forward 27 The value of the inherit option has to be a relative path with forward
26 slashes as delimiters. Up to 5 configuration files can be chained this way, 28 slashes as delimiters. Up to 5 configuration files can be chained this way,
27 longer chains are disallowed to deal with circular references. 29 longer chains are disallowed to deal with circular references.
28 30
29 A main API difference to SafeConfigParser is the way a class instance is 31 A main API difference to SafeConfigParser is the way a class instance is
30 constructed: a file path has to be passed, this file is assumed to be 32 constructed: a file path has to be passed, this file is assumed to be
31 encoded as UTF-8. Also, ChainedConfigParser data is read-only and the 33 encoded as UTF-8. Also, ChainedConfigParser data is read-only and the
32 options are case-sensitive. An additional option_source(section, option) 34 options are case-sensitive. An additional option_source(section, option)
33 method is provided to get the path of the configuration file defining this 35 method is provided to get the path of the configuration file defining this
34 option (for relative paths). Items returned by the items() function also 36 option (for relative paths). Items returned by the items() function also
35 have a source attribute serving the same purpose. 37 have a source attribute serving the same purpose.
Wladimir Palant 2015/06/23 09:43:36 Extend documentation to mention the new += and -=
Sebastian Noack 2015/06/24 08:51:09 Done.
36 """ 38 '''
37 39
38 def __init__(self, path): 40 def __init__(self):
39 self.chain = [] 41 ConfigParser.SafeConfigParser.__init__(self)
40 self.read_path(path) 42 self._origin = {}
41 43
42 def read_path(self, path): 44 def _get_parser_chain(self, file, filename):
43 if len(self.chain) >= 5: 45 parsers = []
44 raise Exception('Too much inheritance in config files')
45 46
46 config = ConfigParser.SafeConfigParser() 47 parser = ConfigParser.SafeConfigParser()
47 config.optionxform = str 48 parser._read(file, filename)
Wladimir Palant 2015/06/23 09:43:35 Please use public API here: parser.readfp(file,
Sebastian Noack 2015/06/24 08:51:10 This code has been gone while addressing the comme
48 config.source_path = path
49 handle = codecs.open(path, 'rb', encoding='utf-8')
50 config.readfp(handle)
51 handle.close()
52 self.chain.append(config)
53 49
54 if config.has_section('default') and config.has_option('default', 'inherit') : 50 while True:
55 parts = config.get('default', 'inherit').split('/') 51 parsers.insert(0, (parser, filename))
56 defaults_path = os.path.join(os.path.dirname(path), *parts)
57 self.read_path(defaults_path)
58 52
59 def defaults(self): 53 try:
60 result = {} 54 inherit = parser.get('default', 'inherit')
61 for config in reverse(self.chain): 55 except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
62 for key, value in config.defaults().iteritems(): 56 return parsers
63 result[key] = value
64 return result
65 57
66 def sections(self): 58 filename = os.path.join(os.path.dirname(filename), *inherit.split('/'))
67 result = set() 59 parser = ConfigParser.SafeConfigParser()
68 for config in self.chain: 60 parser.read(filename)
69 for section in config.sections():
70 result.add(section)
71 return list(result)
72 61
73 def has_section(self, section): 62 def _apply_diff(self, section, option, value):
74 for config in self.chain: 63 addition = option.endswith('+')
75 if config.has_section(section): 64 removal = option.endswith('-')
76 return True
77 return False
78 65
79 def options(self, section): 66 if addition or removal:
80 result = set() 67 option = option[:-1].rstrip()
81 for config in self.chain: 68 old_value = self.get(section, option)
Wladimir Palant 2015/06/23 09:43:36 This will throw when trying to change an option th
Sebastian Noack 2015/06/24 08:51:09 Done.
82 if config.has_section(section):
83 for option in config.options(section):
84 result.add(option)
85 return list(result)
86 69
87 def has_option(self, section, option): 70 if addition:
88 for config in self.chain: 71 value = '%s %s' % (old_value, value)
Wladimir Palant 2015/06/23 09:43:35 I don't think that we want duplicate values - we a
Sebastian Noack 2015/06/24 08:51:10 Done.
89 if config.has_section(section) and config.has_option(section, option): 72 elif removal:
Wladimir Palant 2015/06/23 09:43:35 Nit: no need for elif, can be simply else. But I d
Sebastian Noack 2015/06/24 08:51:09 Done.
90 return True 73 value = re.sub(r'\b(?:%s)\b\s*' % '|'.join(map(re.escape, value.split()) ), '', old_value).rstrip()
Wladimir Palant 2015/06/23 09:43:35 I don't think that regular expressions are the rig
Sebastian Noack 2015/06/24 08:51:09 Done.
91 return False
92 74
93 def get(self, section, option): 75 return option, value
94 for config in self.chain:
95 if config.has_section(section) and config.has_option(section, option):
96 return config.get(section, option)
97 raise ConfigParser.NoOptionError(option, section)
98 76
99 def items(self, section): 77 def _read(self, file, filename):
Wladimir Palant 2015/06/23 09:43:35 Is it really a good idea to override private metho
Sebastian Noack 2015/06/24 08:51:10 Overriding read() and readfp() would require quite
100 seen = set() 78 parsers = self._get_parser_chain(file, filename)
101 result = [] 79
102 for config in self.chain: 80 for parser, filename in parsers:
103 if config.has_section(section): 81 for section in parser.sections():
104 for name, value in config.items(section): 82 for option, value in parser.items(section):
105 if name not in seen: 83 option, value = self._apply_diff(section, option, value)
106 seen.add(name) 84 try:
107 result.append(Item(name, value, config.source_path)) 85 self.set(section, option, value)
108 return result 86 except ConfigParser.NoSectionError:
87 try:
88 self.add_section(section)
89 except ValueError:
90 # add_section() hardcodes 'default' and raises a ValueError if
91 # you try to add a section called like that (case insensitive).
92 # This bug has been fixed in Python 3.
93 self._sections[section] = self._dict()
Wladimir Palant 2015/06/23 09:43:36 I cannot say that I like this hack. How about simp
Sebastian Noack 2015/06/24 08:51:09 "default" != "DEFAULT". The latter is handled spec
Wladimir Palant 2015/06/25 14:11:40 I see. Still, should we access private variables i
Sebastian Noack 2015/06/25 16:06:28 The except block will only be reached in Python ve
Wladimir Palant 2015/06/25 16:12:28 That is: every Python version we support. And I'm
Sebastian Noack 2015/06/25 23:05:31 In case I didn't stress this enough, this is a bug
Wladimir Palant 2015/06/26 13:17:21 It doesn't matter what you call it - it's document
Sebastian Noack 2015/06/26 13:37:53 So your concerns here are only about using interna
94 self.set(section, option, value)
95 self._origin[(section, option)] = filename
Wladimir Palant 2015/06/23 09:43:35 Ok, we have a problem right here... Consider the f
Sebastian Noack 2015/06/24 08:51:09 I first thought that we can simply rely on the sou
96
97 def items(self, section, *args, **kwargs):
98 items = []
99 for option, value in ConfigParser.SafeConfigParser.items(self, section, *arg s, **kwargs):
100 items.append(Item(option, value, self._origin[(section, option)]))
101 return items
109 102
110 def option_source(self, section, option): 103 def option_source(self, section, option):
111 for config in self.chain: 104 try:
112 if config.has_section(section) and config.has_option(section, option): 105 return self._origin[(section, option)]
113 return config.source_path 106 except KeyError:
114 raise ConfigParser.NoOptionError(option, section) 107 raise ConfigParser.NoOptionError(option, section)
OLDNEW
« no previous file with comments | « no previous file | packager.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld