Start working on different per-URL storage

This commit is contained in:
Florian Bruhin 2018-02-16 11:18:43 +01:00
parent 5e50824042
commit 8551288efb
2 changed files with 57 additions and 131 deletions

View File

@ -26,7 +26,7 @@ import functools
import attr import attr
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
from qutebrowser.config import configdata, configexc from qutebrowser.config import configdata, configexc, configutils
from qutebrowser.utils import utils, log, jinja from qutebrowser.utils import utils, log, jinja
from qutebrowser.misc import objects from qutebrowser.misc import objects
@ -229,25 +229,12 @@ class KeyConfig:
self._config.update_mutables(save_yaml=save_yaml) self._config.update_mutables(save_yaml=save_yaml)
@attr.s
class PerUrlSettings:
"""A simple container with an URL pattern and settings for it."""
pattern = attr.ib()
values = attr.ib(default=attr.Factory(dict))
class Config(QObject): class Config(QObject):
"""Main config object. """Main config object.
Attributes: Attributes:
_values: A dict mapping setting names to their values. _values: A dict mapping setting names to configutils.Values objects.
_per_url_values: A mapping from UrlPattern objects to PerUrlSetting
instances. Note that dict lookup is currently only
useful for finding the pattern when adding values, not
for getting values.
_mutables: A dictionary of mutable objects to be checked for changes. _mutables: A dictionary of mutable objects to be checked for changes.
_yaml: A YamlConfig object or None. _yaml: A YamlConfig object or None.
@ -261,18 +248,14 @@ class Config(QObject):
super().__init__(parent) super().__init__(parent)
self.changed.connect(_render_stylesheet.cache_clear) self.changed.connect(_render_stylesheet.cache_clear)
self._values = {} self._values = {}
self._per_url_values = {}
self._mutables = {} self._mutables = {}
self._yaml = yaml_config self._yaml = yaml_config
def __iter__(self): def __iter__(self):
"""Iterate over UrlPattern, Option, value tuples.""" """Iterate over Option, ScopedValue tuples."""
for name, value in sorted(self._values.items()): for name, values in sorted(self._values.items()):
yield (None, self.get_opt(name), value) for scoped in values:
yield self.get_opt(name), scoped
for pattern, options in sorted(self._per_url_values.items()):
for name, value in sorted(options.values.items()):
yield (pattern, self.get_opt(name), value)
def init_save_manager(self, save_manager): def init_save_manager(self, save_manager):
"""Make sure the config gets saved properly. """Make sure the config gets saved properly.
@ -282,40 +265,6 @@ class Config(QObject):
""" """
self._yaml.init_save_manager(save_manager) self._yaml.init_save_manager(save_manager)
def _get_values(self, pattern=None, *, create=False):
"""Get the appropriate _values instance for the given pattern.
With create=True, create a new one instead of returning an empty dict.
"""
if pattern is None:
return self._values
elif pattern in self._per_url_values:
return self._per_url_values[pattern].values
elif create:
settings = PerUrlSettings(pattern)
self._per_url_values[pattern] = settings
return settings.values
else:
return {}
def _get_values_for_url(self, url):
"""Get a temporary values container which merges all matching values.
Note that this does *not* include global values.
Currently, this iterates linearly over all patterns. This could probably
be optimized by storing patterns based on their scheme/host/port and
then searching all possible matches in a dict before doing a full match.
"""
# FIXME We could avoid the copy if there's no per-url match.
# values = self._values.copy()
values = {}
# FIXME:conf what order?
for options in self._per_url_values.values():
if options.pattern.matches(url):
values.update(options.values)
return values
def _set_value(self, opt, value, pattern=None): def _set_value(self, opt, value, pattern=None):
"""Set the given option to the given value.""" """Set the given option to the given value."""
if not isinstance(objects.backend, objects.NoBackend): if not isinstance(objects.backend, objects.NoBackend):
@ -323,8 +272,8 @@ class Config(QObject):
raise configexc.BackendError(opt.name, objects.backend) raise configexc.BackendError(opt.name, objects.backend)
opt.typ.to_py(value) # for validation opt.typ.to_py(value) # for validation
values = self._get_values(pattern, create=True) scoped = configutils.ScopedValue(opt.typ.from_obj(value), pattern)
values[opt.name] = opt.typ.from_obj(value) self._values[opt.name].add(scoped)
self.changed.emit(opt.name) self.changed.emit(opt.name)
log.config.debug("Config option changed: {} = {}".format( log.config.debug("Config option changed: {} = {}".format(
@ -348,20 +297,18 @@ class Config(QObject):
name, deleted=deleted, renamed=renamed) name, deleted=deleted, renamed=renamed)
raise exception from None raise exception from None
def get(self, name): def get(self, name, url=None):
"""Get the given setting converted for Python code.""" """Get the given setting converted for Python code."""
opt = self.get_opt(name) opt = self.get_opt(name)
obj = self.get_obj(name, mutable=False) obj = self.get_obj(name, mutable=False, url=url)
return opt.typ.to_py(obj) return opt.typ.to_py(obj)
def get_obj(self, name, *, mutable=True, pattern=None): def get_obj(self, name, *, mutable=True, url=None):
"""Get the given setting as object (for YAML/config.py). """Get the given setting as object (for YAML/config.py).
If mutable=True is set, watch the returned object for mutations. If mutable=True is set, watch the returned object for mutations.
If a pattern is given, get the per-domain setting for that pattern (if If a URL is given, return the value which should be used for that URL.
any).
""" """
opt = self.get_opt(name)
obj = None obj = None
# If we allow mutation, there is a chance that prior mutations already # If we allow mutation, there is a chance that prior mutations already
# entered the mutable dictionary and thus further copies are unneeded # entered the mutable dictionary and thus further copies are unneeded
@ -371,9 +318,11 @@ class Config(QObject):
# Otherwise, we return a copy of the value stored internally, so the # Otherwise, we return a copy of the value stored internally, so the
# internal value can never be changed by mutating the object returned. # internal value can never be changed by mutating the object returned.
else: else:
values = self._get_values(pattern) if name in self._values:
value = self._values[name].get_any(url)
obj = copy.deepcopy(values.get(name, opt.default)) else:
value = self.get_opt(name).default
obj = copy.deepcopy(value)
# Then we watch the returned object for changes. # Then we watch the returned object for changes.
if isinstance(obj, (dict, list)): if isinstance(obj, (dict, list)):
if mutable: if mutable:
@ -383,39 +332,17 @@ class Config(QObject):
assert obj.__hash__ is not None, obj assert obj.__hash__ is not None, obj
return obj return obj
def get_for_url(self, name, url, *, maybe_unset=True):
"""Get the given per-url setting converted for Python code.
With maybe_unset=True, if the value isn't overridden for a given domain,
return UNSET.
With maybe_unset=False, return the global/default value instead.
"""
opt = self.get_opt(name)
obj = self._get_obj_for_url(opt, url=url, maybe_unset=maybe_unset)
return opt.typ.to_py(obj)
def _get_obj_for_url(self, opt, url, *, maybe_unset=True):
"""Get the given setting as object (for YAML/config.py).
With maybe_unset=True, if the value isn't overridden for a given domain,
return UNSET.
With maybe_unset=False, return the global/default value instead.
"""
values = self._get_values_for_url(url)
if opt in values:
return values[opt]
elif maybe_unset:
return UNSET
else:
return self.get_obj(opt.name, mutable=False)
def get_str(self, name, *, pattern=None): def get_str(self, name, *, pattern=None):
"""Get the given setting as string.""" """Get the given setting as string.
If a pattern is given, get the setting for the given pattern or
configutils.UNSET.
"""
opt = self.get_opt(name) opt = self.get_opt(name)
values = self._get_values(pattern) values = self._values[name]
value = values.get(name, opt.default) value = values.get_for_pattern(pattern)
if value is configutils.UNSET:
return value
return opt.typ.to_str(value) return opt.typ.to_str(value)
def set_obj(self, name, value, *, pattern=None, save_yaml=False): def set_obj(self, name, value, *, pattern=None, save_yaml=False):

View File

@ -32,7 +32,7 @@ import yaml
from PyQt5.QtCore import pyqtSignal, QObject, QSettings from PyQt5.QtCore import pyqtSignal, QObject, QSettings
import qutebrowser import qutebrowser
from qutebrowser.config import configexc, config, configdata from qutebrowser.config import configexc, config, configdata, configutils
from qutebrowser.utils import standarddir, utils, qtutils, log from qutebrowser.utils import standarddir, utils, qtutils, log
@ -88,7 +88,6 @@ class YamlConfig(QObject):
self._filename = os.path.join(standarddir.config(auto=True), self._filename = os.path.join(standarddir.config(auto=True),
'autoconfig.yml') 'autoconfig.yml')
self._values = {} self._values = {}
self._per_url_values = {}
self._dirty = None self._dirty = None
def init_save_manager(self, save_manager): def init_save_manager(self, save_manager):
@ -100,23 +99,8 @@ class YamlConfig(QObject):
save_manager.add_saveable('yaml-config', self._save, self.changed) save_manager.add_saveable('yaml-config', self._save, self.changed)
def __iter__(self): def __iter__(self):
for name, value in sorted(self._values.items()): for name, values in sorted(self._values.items()):
yield (None, name, value) yield from values
for pattern, options in sorted(self._per_url_values.items()):
for name, value in sorted(options.values.items()):
yield (pattern, name, value)
def _get_values(self, pattern=None):
"""Get the appropriate _values instance for the given pattern."""
if pattern is None:
return self._values
elif pattern in self._per_url_values:
return self._per_url_values[pattern]
else:
values = {}
self._per_url_values[pattern] = values
return values
def _mark_changed(self): def _mark_changed(self):
"""Mark the YAML config as changed.""" """Mark the YAML config as changed."""
@ -129,7 +113,7 @@ class YamlConfig(QObject):
return return
data = {'config_version': self.VERSION, 'global': self._values} data = {'config_version': self.VERSION, 'global': self._values}
for pattern, values in sorted(self._per_url_values.items()): for pattern, values in sorted(self._values.items()):
data[str(pattern)] = values data[str(pattern)] = values
with qtutils.savefile_open(self._filename) as f: with qtutils.savefile_open(self._filename) as f:
@ -155,18 +139,17 @@ class YamlConfig(QObject):
raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
try: try:
global_obj = yaml_data.pop('global') settings_obj = yaml_data.pop('settings')
except KeyError: except KeyError:
desc = configexc.ConfigErrorDesc( desc = configexc.ConfigErrorDesc(
"While loading data", "While loading data",
"Toplevel object does not contain 'global' key") "Toplevel object does not contain 'settings' key")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
except TypeError: except TypeError:
desc = configexc.ConfigErrorDesc("While loading data", desc = configexc.ConfigErrorDesc("While loading data",
"Toplevel object is not a dict") "Toplevel object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
# FIXME:conf test this
try: try:
yaml_data.pop('config_version') yaml_data.pop('config_version')
except KeyError: except KeyError:
@ -175,22 +158,38 @@ class YamlConfig(QObject):
"Toplevel object does not contain 'config_version' key") "Toplevel object does not contain 'config_version' key")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc]) raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
if not isinstance(global_obj, dict): self._load_settings_object(settings_obj)
desc = configexc.ConfigErrorDesc(
"While loading data",
"'global' object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
self._values = global_obj
self._per_url_values = yaml_data
self._dirty = False self._dirty = False
self._handle_migrations() self._handle_migrations()
self._validate() self._validate()
def _load_settings_obj(self, settings_obj):
"""Load the settings from the settings: key."""
if not isinstance(settings_obj, dict):
desc = configexc.ConfigErrorDesc(
"While loading data",
"'settings' object is not a dict")
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
# FIXME:conf test this
self._values = {}
for name, yaml_values in settings_obj.items():
values = configutils.Values(self._config.get_opt(name))
if 'global' in yaml_values:
scoped = configutils.ScopedValue(yaml_values.pop('global'))
values.add(scoped)
for pattern, value in yaml_values.items():
scoped = configutils.ScopedValue(value, pattern)
values.add(scoped)
self._values[name] = values
def _handle_migrations(self): def _handle_migrations(self):
"""Migrate older configs to the newest format.""" """Migrate older configs to the newest format."""
# FIXME:conf handle per-URL settings # FIXME:conf handle per-URL settings
# FIXME:conf migrate from older format with global: key
# Simple renamed/deleted options # Simple renamed/deleted options
for name in list(self._values): for name in list(self._values):
if name in configdata.MIGRATIONS.renamed: if name in configdata.MIGRATIONS.renamed: