| # This Source Code Form is subject to the terms of the Mozilla Public |
| # License, v. 2.0. If a copy of the MPL was not distributed with this file, |
| # You can obtain one at http://mozilla.org/MPL/2.0/. |
| |
| """ |
| user preferences |
| """ |
| |
| __all__ = ('PreferencesReadError', 'Preferences') |
| |
| import json |
| import mozfile |
| import os |
| import re |
| import tokenize |
| from ConfigParser import SafeConfigParser as ConfigParser |
| from StringIO import StringIO |
| |
| class PreferencesReadError(Exception): |
| """read error for prefrences files""" |
| |
| |
| class Preferences(object): |
| """assembly of preferences from various sources""" |
| |
| def __init__(self, prefs=None): |
| self._prefs = [] |
| if prefs: |
| self.add(prefs) |
| |
| def add(self, prefs, cast=False): |
| """ |
| :param prefs: |
| :param cast: whether to cast strings to value, e.g. '1' -> 1 |
| """ |
| # wants a list of 2-tuples |
| if isinstance(prefs, dict): |
| prefs = prefs.items() |
| if cast: |
| prefs = [(i, self.cast(j)) for i, j in prefs] |
| self._prefs += prefs |
| |
| def add_file(self, path): |
| """a preferences from a file |
| |
| :param path: |
| """ |
| self.add(self.read(path)) |
| |
| def __call__(self): |
| return self._prefs |
| |
| @classmethod |
| def cast(cls, value): |
| """ |
| interpolate a preference from a string |
| from the command line or from e.g. an .ini file, there is no good way to denote |
| what type the preference value is, as natively it is a string |
| |
| - integers will get cast to integers |
| - true/false will get cast to True/False |
| - anything enclosed in single quotes will be treated as a string with the ''s removed from both sides |
| """ |
| |
| if not isinstance(value, basestring): |
| return value # no op |
| quote = "'" |
| if value == 'true': |
| return True |
| if value == 'false': |
| return False |
| try: |
| return int(value) |
| except ValueError: |
| pass |
| if value.startswith(quote) and value.endswith(quote): |
| value = value[1:-1] |
| return value |
| |
| |
| @classmethod |
| def read(cls, path): |
| """read preferences from a file""" |
| |
| section = None # for .ini files |
| basename = os.path.basename(path) |
| if ':' in basename: |
| # section of INI file |
| path, section = path.rsplit(':', 1) |
| |
| if not os.path.exists(path) and not mozfile.is_url(path): |
| raise PreferencesReadError("'%s' does not exist" % path) |
| |
| if section: |
| try: |
| return cls.read_ini(path, section) |
| except PreferencesReadError: |
| raise |
| except Exception, e: |
| raise PreferencesReadError(str(e)) |
| |
| # try both JSON and .ini format |
| try: |
| return cls.read_json(path) |
| except Exception, e: |
| try: |
| return cls.read_ini(path) |
| except Exception, f: |
| for exception in e, f: |
| if isinstance(exception, PreferencesReadError): |
| raise exception |
| raise PreferencesReadError("Could not recognize format of %s" % path) |
| |
| |
| @classmethod |
| def read_ini(cls, path, section=None): |
| """read preferences from an .ini file""" |
| |
| parser = ConfigParser() |
| parser.optionxform = str |
| parser.readfp(mozfile.load(path)) |
| |
| if section: |
| if section not in parser.sections(): |
| raise PreferencesReadError("No section '%s' in %s" % (section, path)) |
| retval = parser.items(section, raw=True) |
| else: |
| retval = parser.defaults().items() |
| |
| # cast the preferences since .ini is just strings |
| return [(i, cls.cast(j)) for i, j in retval] |
| |
| @classmethod |
| def read_json(cls, path): |
| """read preferences from a JSON blob""" |
| |
| prefs = json.loads(mozfile.load(path).read()) |
| |
| if type(prefs) not in [list, dict]: |
| raise PreferencesReadError("Malformed preferences: %s" % path) |
| if isinstance(prefs, list): |
| if [i for i in prefs if type(i) != list or len(i) != 2]: |
| raise PreferencesReadError("Malformed preferences: %s" % path) |
| values = [i[1] for i in prefs] |
| elif isinstance(prefs, dict): |
| values = prefs.values() |
| else: |
| raise PreferencesReadError("Malformed preferences: %s" % path) |
| types = (bool, basestring, int) |
| if [i for i in values |
| if not [isinstance(i, j) for j in types]]: |
| raise PreferencesReadError("Only bool, string, and int values allowed") |
| return prefs |
| |
| @classmethod |
| def read_prefs(cls, path, pref_setter='user_pref', interpolation=None): |
| """ |
| Read preferences from (e.g.) prefs.js |
| |
| :param path: The path to the preference file to read. |
| :param pref_setter: The name of the function used to set preferences |
| in the preference file. |
| :param interpolation: If provided, a dict that will be passed |
| to str.format to interpolate preference values. |
| """ |
| |
| marker = '##//' # magical marker |
| lines = [i.strip() for i in mozfile.load(path).readlines() if i.strip()] |
| _lines = [] |
| for line in lines: |
| if line.startswith(('#', '//')): |
| continue |
| if '//' in line: |
| line = line.replace('//', marker) |
| _lines.append(line) |
| string = '\n'.join(_lines) |
| |
| # skip trailing comments |
| processed_tokens = [] |
| f_obj = StringIO(string) |
| for token in tokenize.generate_tokens(f_obj.readline): |
| if token[0] == tokenize.COMMENT: |
| continue |
| processed_tokens.append(token[:2]) # [:2] gets around http://bugs.python.org/issue9974 |
| string = tokenize.untokenize(processed_tokens) |
| |
| retval = [] |
| def pref(a, b): |
| if interpolation and isinstance(b, basestring): |
| b = b.format(**interpolation) |
| retval.append((a, b)) |
| lines = [i.strip().rstrip(';') for i in string.split('\n') if i.strip()] |
| |
| _globals = {'retval': retval, 'true': True, 'false': False} |
| _globals[pref_setter] = pref |
| for line in lines: |
| try: |
| eval(line, _globals, {}) |
| except SyntaxError: |
| print line |
| raise |
| |
| # de-magic the marker |
| for index, (key, value) in enumerate(retval): |
| if isinstance(value, basestring) and marker in value: |
| retval[index] = (key, value.replace(marker, '//')) |
| |
| return retval |
| |
| @classmethod |
| def write(cls, _file, prefs, pref_string='user_pref(%s, %s);'): |
| """write preferences to a file""" |
| |
| if isinstance(_file, basestring): |
| f = file(_file, 'a') |
| else: |
| f = _file |
| |
| if isinstance(prefs, dict): |
| # order doesn't matter |
| prefs = prefs.items() |
| |
| # serialize -> JSON |
| _prefs = [(json.dumps(k), json.dumps(v) ) |
| for k, v in prefs] |
| |
| # write the preferences |
| for _pref in _prefs: |
| print >> f, pref_string % _pref |
| |
| # close the file if opened internally |
| if isinstance(_file, basestring): |
| f.close() |