Multiple configuration files
Andrew P.
7 years ago
0 | #!/usr/bin/env python3 | |
1 | # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- | |
2 | # LightDM GTK Greeter Settings | |
3 | # Copyright (C) 2015 Andrew P. <pan.pav.7c5@gmail.com> | |
4 | # | |
5 | # This program is free software: you can redistribute it and/or modify it | |
6 | # under the terms of the GNU General Public License version 3, as published | |
7 | # by the Free Software Foundation. | |
8 | # | |
9 | # This program is distributed in the hope that it will be useful, but | |
10 | # WITHOUT ANY WARRANTY; without even the implied warranties of | |
11 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | |
12 | # PURPOSE. See the GNU General Public License for more details. | |
13 | # | |
14 | # You should have received a copy of the GNU General Public License along | |
15 | # with this program. If not, see <http://www.gnu.org/licenses/>. | |
16 | ||
17 | import configparser | |
18 | import os | |
19 | from collections import OrderedDict | |
20 | from glob import iglob | |
21 | ||
22 | ||
23 | class Config: | |
24 | ||
25 | class ConfigGroup: | |
26 | ||
27 | def __init__(self, config): | |
28 | self._config = config | |
29 | self._items = OrderedDict() | |
30 | ||
31 | def __iter__(self): | |
32 | return iter(self._items) | |
33 | ||
34 | def __contains__(self, item): | |
35 | return item in self._items | |
36 | ||
37 | def __getitem__(self, item): | |
38 | values = self._items.get(item) | |
39 | return values[-1][1] if values else None | |
40 | ||
41 | def __setitem__(self, item, value): | |
42 | if isinstance(value, tuple): | |
43 | value, default = value | |
44 | else: | |
45 | default = None | |
46 | ||
47 | values = self._items.get(item) | |
48 | if values and values[-1][0] == self._config._output_path: | |
49 | if default is not None and value == default and len(values) == 1: | |
50 | values.clear() | |
51 | else: | |
52 | values[-1] = (self._config._output_path, value) | |
53 | elif values is not None: | |
54 | if default is None or value != default or (values and values[-1][1] != default): | |
55 | values.append((self._config._output_path, value)) | |
56 | else: | |
57 | if default is None or value != default: | |
58 | self._items[item] = [(self._config._output_path, value)] | |
59 | ||
60 | def __delitem__(self, item): | |
61 | values = self._items.get(item) | |
62 | if values is not None: | |
63 | if values and values[-1][0] == self._config._output_path: | |
64 | del values[-1] | |
65 | if not values: | |
66 | del self._items[item] | |
67 | ||
68 | def get_key_file(self, key): | |
69 | values = self._items.get(key) | |
70 | return values[-1][0] if values else None | |
71 | ||
72 | def __init__(self, input_pathes, output_path): | |
73 | self._input_pathes = tuple(input_pathes) | |
74 | self._output_path = output_path | |
75 | self._groups = OrderedDict() | |
76 | ||
77 | def read(self): | |
78 | files = [] | |
79 | for path in self._input_pathes: | |
80 | if os.path.isdir(path): | |
81 | files.extend(sorted(iglob(os.path.join(path, '*.conf')))) | |
82 | elif os.path.exists(path): | |
83 | files.append(path) | |
84 | if self._output_path not in files: | |
85 | files.append(self._output_path) | |
86 | ||
87 | self._groups.clear() | |
88 | for path in files: | |
89 | config_file = configparser.RawConfigParser(strict=False, allow_no_value=True) | |
90 | config_file.read(path) | |
91 | ||
92 | for groupname, values in config_file.items(): | |
93 | if groupname == 'DEFAULT': | |
94 | continue | |
95 | ||
96 | if groupname not in self._groups: | |
97 | self._groups[groupname] = Config.ConfigGroup(self) | |
98 | group = self._groups[groupname] | |
99 | ||
100 | for key, value in values.items(): | |
101 | if key in group._items: | |
102 | values = group._items[key] | |
103 | if value is not None or values: | |
104 | values.append((path, value)) | |
105 | elif value is not None: | |
106 | group._items[key] = [(path, value)] | |
107 | ||
108 | def write(self): | |
109 | config_file = configparser.RawConfigParser(strict=False, allow_no_value=True) | |
110 | ||
111 | for groupname, group in self._groups.items(): | |
112 | config_file.add_section(groupname) | |
113 | config_section = config_file[groupname] | |
114 | ||
115 | for key, values in group._items.items(): | |
116 | if not values or values[-1][0] != self._output_path: | |
117 | continue | |
118 | if values[-1][1] is not None or len(values) > 1: | |
119 | config_section[key] = values[-1][1] | |
120 | ||
121 | with open(self._output_path, 'w') as file: | |
122 | config_file.write(file) | |
123 | ||
124 | def items(self): | |
125 | return self._groups.items() | |
126 | ||
127 | def allitems(self): | |
128 | return ((g, k, items[k]) for (g, items) in self._groups.items() for k in items._items) | |
129 | ||
130 | def add_group(self, name): | |
131 | if name in self._groups: | |
132 | return self._groups[name] | |
133 | else: | |
134 | return self._groups.setdefault(name, Config.ConfigGroup(self)) | |
135 | ||
136 | def get_key_file(self, groupname, key): | |
137 | group = self._groups.get(groupname) | |
138 | return group.get_key_file(key) if group is not None else None | |
139 | ||
140 | def __iter__(self): | |
141 | return iter(self._groups) | |
142 | ||
143 | def __getitem__(self, item): | |
144 | if isinstance(item, tuple): | |
145 | group = self._groups.get(item[0]) | |
146 | return group[item[1]] if group else None | |
147 | return self._groups.get(item) | |
148 | ||
149 | def __setitem__(self, item, value): | |
150 | if isinstance(item, tuple): | |
151 | if not item[0] in self._groups: | |
152 | self._groups = Config.ConfigGroup(self) | |
153 | self._groups[item[0]][item[1]] = value | |
154 | ||
155 | def __delitem__(self, item): | |
156 | if isinstance(item, tuple): | |
157 | group = self._groups.get(item[0]) | |
158 | if group is not None: | |
159 | del group[item[1]] | |
160 | return | |
161 | ||
162 | group = self._groups.get(item) | |
163 | if group is not None: | |
164 | if not group: | |
165 | del self._groups[item] | |
166 | return | |
167 | ||
168 | keys_to_remove = [] | |
169 | for key, values in group._items.items(): | |
170 | if values[-1][0] == self._output_path: | |
171 | if len(values) == 1: | |
172 | keys_to_remove.append(key) | |
173 | else: | |
174 | values[-1] = (self._output_path, None) | |
175 | elif values: | |
176 | values.append((self._output_path, None)) | |
177 | ||
178 | if len(keys_to_remove) < len(group._items): | |
179 | for key in keys_to_remove: | |
180 | del group._items[key] | |
181 | else: | |
182 | del self._groups[item] |
16 | 16 | |
17 | 17 | |
18 | 18 | import collections |
19 | import configparser | |
20 | 19 | import os |
21 | 20 | import shlex |
22 | 21 | import sys |
22 | from functools import partialmethod | |
23 | 23 | from glob import iglob |
24 | from functools import partialmethod | |
25 | 24 | from itertools import chain |
26 | 25 | from locale import gettext as _ |
27 | 26 | |
28 | 27 | from gi.repository import ( |
29 | 28 | Gdk, |
29 | GLib, | |
30 | 30 | Gtk) |
31 | 31 | from gi.repository import Pango |
32 | 32 | from gi.repository.GObject import markup_escape_text as escape_markup |
33 | 33 | |
34 | 34 | from lightdm_gtk_greeter_settings import ( |
35 | Config, | |
35 | 36 | helpers, |
36 | 37 | IconEntry, |
37 | 38 | IndicatorsEntry, |
137 | 138 | group.entry_added.connect(self.on_entry_added) |
138 | 139 | group.entry_removed.connect(self.on_entry_removed) |
139 | 140 | |
140 | self._config_path = helpers.get_config_path() | |
141 | self._allow_edit = self._has_access_to_write(self._config_path) | |
141 | config_pathes = [] | |
142 | config_pathes.extend(os.path.join(p, 'lightdm-gtk-greeter.conf.d') | |
143 | for p in GLib.get_system_data_dirs()) | |
144 | config_pathes.extend(os.path.join(p, 'lightdm-gtk-greeter.conf.d') | |
145 | for p in GLib.get_system_config_dirs()) | |
146 | config_pathes.append(os.path.join(os.path.dirname(helpers.get_config_path()), | |
147 | 'lightdm-gtk-greeter.conf.d')) | |
148 | ||
149 | self._config = Config.Config(config_pathes, helpers.get_config_path()) | |
150 | ||
151 | self._allow_edit = self._has_access_to_write(helpers.get_config_path()) | |
142 | 152 | self._widgets.apply.props.visible = self._allow_edit |
143 | 153 | |
144 | 154 | if not self._allow_edit: |
150 | 160 | secondary_text=_( |
151 | 161 | 'It seems that you don\'t have permissions to write to ' |
152 | 162 | 'file:\n{path}\n\nTry to run this program using "sudo" ' |
153 | 'or "pkexec"').format(path=self._config_path), | |
163 | 'or "pkexec"').format(path=helpers.get_config_path()), | |
154 | 164 | message_type=Gtk.MessageType.WARNING) |
155 | 165 | |
156 | 166 | if self.mode == WindowMode.Embedded: |
175 | 185 | |
176 | 186 | self.set_titlebar(header) |
177 | 187 | |
178 | self._config = configparser.RawConfigParser(strict=False) | |
179 | 188 | self._read() |
180 | 189 | |
181 | 190 | def _has_access_to_write(self, path): |
182 | if os.path.exists(path) and os.access(self._config_path, os.W_OK): | |
191 | if os.path.exists(path) and os.access(helpers.get_config_path(), os.W_OK): | |
183 | 192 | return True |
184 | return os.access(os.path.dirname(self._config_path), os.W_OK | os.X_OK) | |
193 | return os.access(os.path.dirname(helpers.get_config_path()), os.W_OK | os.X_OK) | |
185 | 194 | |
186 | 195 | def _set_message(self, message, type_=Gtk.MessageType.INFO): |
187 | 196 | if not message: |
192 | 201 | self._widgets.infobar.show() |
193 | 202 | |
194 | 203 | def _read(self): |
195 | self._config.clear() | |
196 | try: | |
197 | if not self._config.read(self._config_path) and \ | |
198 | self.mode != WindowMode.Embedded: | |
199 | helpers.show_message(text=_('Failed to read configuration file: {path}') | |
200 | .format(path=self._config_path), | |
201 | message_type=Gtk.MessageType.ERROR) | |
202 | except (configparser.DuplicateSectionError, | |
203 | configparser.MissingSectionHeaderError): | |
204 | pass | |
205 | ||
204 | self._config.read() | |
206 | 205 | self._changed_entries = None |
207 | 206 | |
208 | 207 | for group in self._groups: |
225 | 224 | self._widgets.apply.props.sensitive = False |
226 | 225 | |
227 | 226 | try: |
228 | with open(self._config_path, 'w') as file: | |
229 | self._config.write(file) | |
227 | self._config.write() | |
230 | 228 | except OSError as e: |
231 | 229 | helpers.show_message(e, Gtk.MessageType.ERROR) |
232 | 230 | |
320 | 318 | class EntryMenu: |
321 | 319 | menu = Gtk.Menu() |
322 | 320 | value = new_item() |
321 | file = new_item() | |
323 | 322 | error_separator = Gtk.SeparatorMenuItem() |
324 | 323 | error = new_item() |
325 | 324 | error_action = new_item(self.on_entry_fix_clicked) |
328 | 327 | default = new_item(self.on_entry_reset_clicked) |
329 | 328 | |
330 | 329 | menu.append(value) |
330 | menu.append(file) | |
331 | 331 | menu.append(error_separator) |
332 | 332 | menu.append(error) |
333 | 333 | menu.append(error_action) |
354 | 354 | group=group.name, |
355 | 355 | key=key, |
356 | 356 | value=format_value(value=entry.value, enabled=entry.enabled)) |
357 | ||
358 | key_file = None | |
359 | if entry not in self._changed_entries: | |
360 | key_file = self._config.get_key_file(group.name, key) | |
361 | if key_file and key_file == helpers.get_config_path(): | |
362 | key_file = None | |
363 | elif key_file: | |
364 | menu.file.props.label = _('Value defined in file: {path}')\ | |
365 | .format(path=escape_markup(key_file)) | |
366 | menu.file.set_tooltip_text(key_file) | |
367 | menu.file.props.visible = key_file is not None | |
357 | 368 | |
358 | 369 | error = entry.error |
359 | 370 | error_action = None |
366 | 377 | menu.error_action.props.label = label or '' |
367 | 378 | if error_action: |
368 | 379 | menu.error_action._fix_entry_data = entry, error_action |
369 | menu.error.set_label(error) | |
380 | menu.error.set_label(escape_markup(error)) | |
370 | 381 | |
371 | 382 | menu.error.props.visible = error is not None |
372 | 383 | menu.error_action.props.visible = error_action is not None |
43 | 43 | |
44 | 44 | def read(self, config): |
45 | 45 | self._entries.clear() |
46 | for name, section in config.items(): | |
46 | for name, group in config.items(): | |
47 | 47 | if not name.startswith(self.GROUP_PREFIX): |
48 | 48 | continue |
49 | 49 | name = name[len(self.GROUP_PREFIX):].strip() |
50 | 50 | entry = MonitorEntry(self._widgets) |
51 | entry['background'] = section.get('background', None) | |
52 | entry['user-background'] = bool2string(section.getboolean('user-background', None), 1) | |
53 | entry['laptop'] = bool2string(section.getboolean('laptop', None), True) | |
51 | entry['background'] = group['background'] | |
52 | entry['user-background'] = bool2string(group['user-background'], True) | |
53 | entry['laptop'] = bool2string(group['laptop'], True) | |
54 | 54 | self._entries[name] = entry |
55 | 55 | self.entry_added.emit(entry, name) |
56 | 56 | |
57 | 57 | def write(self, config): |
58 | for name in config.sections(): | |
59 | if name.startswith(self.GROUP_PREFIX): | |
60 | config.remove_section(name) | |
58 | groups = set(name for name, __ in self._entries.items()) | |
59 | groups_to_remove = tuple(name for name in config | |
60 | if (name.startswith(self.GROUP_PREFIX) and | |
61 | name[len(self.GROUP_PREFIX):].strip() not in groups)) | |
61 | 62 | |
62 | 63 | for name, entry in self._entries.items(): |
63 | section = '{prefix} {name}'.format(prefix=self.GROUP_PREFIX, name=name.strip()) | |
64 | config.add_section(section) | |
64 | groupname = '{prefix} {name}'.format(prefix=self.GROUP_PREFIX, name=name.strip()) | |
65 | group = config.add_group(groupname) | |
65 | 66 | for key, value in entry: |
66 | if value is not None: | |
67 | config.set(section, key, value) | |
67 | group[key] = value | |
68 | ||
69 | for name in groups_to_remove: | |
70 | del config[name] | |
68 | 71 | |
69 | 72 | def _on_label_link_activate(self, label, uri): |
70 | 73 | if not self._dialog: |
43 | 43 | self.__defaults_wrapper = None |
44 | 44 | |
45 | 45 | def read(self, config): |
46 | '''Read group content from specified GreeterConfig object''' | |
46 | 47 | raise NotImplementedError(self.__class__) |
47 | 48 | |
48 | 49 | def write(self, config): |
50 | '''Writes content of this group to specified GreeterConfig object''' | |
49 | 51 | raise NotImplementedError(self.__class__) |
50 | 52 | |
51 | 53 | @property |
52 | 54 | def entries(self): |
55 | '''entries["key"] - key => Entry mapping. Read only.''' | |
53 | 56 | if not self.__entries_wrapper: |
54 | 57 | self.__entries_wrapper = BaseGroup.__DictWrapper(self._get_entry) |
55 | 58 | return self.__entries_wrapper |
56 | 59 | |
57 | 60 | @property |
58 | 61 | def defaults(self): |
62 | '''defaults["key"] - default value for "key" entry. Read only.''' | |
59 | 63 | if not self.__defaults_wrapper: |
60 | 64 | self.__defaults_wrapper = BaseGroup.__DictWrapper(self._get_default) |
61 | 65 | return self.__defaults_wrapper |
62 | 66 | |
63 | def _get_default(self, key): | |
67 | def _get_entry(self, key): | |
64 | 68 | raise NotImplementedError(self.__class__) |
65 | 69 | |
66 | def _get_entry(self, key): | |
70 | def _get_default(self, key): | |
67 | 71 | raise NotImplementedError(self.__class__) |
68 | 72 | |
69 | 73 | @GObject.Signal |
92 | 96 | return self._name |
93 | 97 | |
94 | 98 | def read(self, config): |
95 | ||
96 | 99 | if not self._entries: |
97 | 100 | for key, (klass, default) in self._options.items(): |
98 | 101 | entry = klass(WidgetsWrapper(self._widgets, key)) |
101 | 104 | self.entry_added.emit(entry, key) |
102 | 105 | |
103 | 106 | for key, entry in self._entries.items(): |
104 | if config.has_option(self._name, key): | |
105 | entry.value = config.get(self._name, key) | |
106 | entry.enabled = True | |
107 | else: | |
108 | entry.value = self._defaults[key] | |
109 | entry.enabled = False | |
107 | value = config[self._name, key] | |
108 | entry.value = value if value is not None else self._defaults[key] | |
109 | entry.enabled = value is not None | |
110 | 110 | |
111 | 111 | def write(self, config): |
112 | ||
113 | if not config.has_section(self._name): | |
114 | config.add_section(self._name) | |
115 | ||
116 | 112 | for key, entry in self._entries.items(): |
117 | value = entry.value | |
118 | if entry.enabled and value != self._get_default(key): | |
119 | config.set(self._name, key, entry.value) | |
120 | else: | |
121 | config.remove_option(self._name, key) | |
113 | del config[self._name, key] | |
114 | if entry.enabled: | |
115 | config[self._name, key] = entry.value, self._get_default(key) | |
122 | 116 | |
123 | 117 | def _get_entry(self, key): |
124 | 118 | return self._entries.get(key) |