Initial implementation of per-URL setting storage
This commit is contained in:
parent
894da598d6
commit
4ed07d6062
@ -23,6 +23,7 @@ import copy
|
||||
import contextlib
|
||||
import functools
|
||||
|
||||
import attr
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject
|
||||
|
||||
from qutebrowser.config import configdata, configexc
|
||||
@ -225,12 +226,25 @@ class KeyConfig:
|
||||
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):
|
||||
|
||||
"""Main config object.
|
||||
|
||||
Attributes:
|
||||
_values: A dict mapping setting names to their values.
|
||||
_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.
|
||||
_yaml: A YamlConfig object or None.
|
||||
|
||||
@ -244,13 +258,18 @@ class Config(QObject):
|
||||
super().__init__(parent)
|
||||
self.changed.connect(_render_stylesheet.cache_clear)
|
||||
self._values = {}
|
||||
self._per_url_values = {}
|
||||
self._mutables = {}
|
||||
self._yaml = yaml_config
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterate over Option, value tuples."""
|
||||
"""Iterate over UrlPattern, Option, value tuples."""
|
||||
for name, value in sorted(self._values.items()):
|
||||
yield (self.get_opt(name), value)
|
||||
yield (None, self.get_opt(name), value)
|
||||
|
||||
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):
|
||||
"""Make sure the config gets saved properly.
|
||||
@ -260,14 +279,31 @@ class Config(QObject):
|
||||
"""
|
||||
self._yaml.init_save_manager(save_manager)
|
||||
|
||||
def _set_value(self, opt, value):
|
||||
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 _set_value(self, opt, value, pattern=None):
|
||||
"""Set the given option to the given value."""
|
||||
if not isinstance(objects.backend, objects.NoBackend):
|
||||
if objects.backend not in opt.backends:
|
||||
raise configexc.BackendError(opt.name, objects.backend)
|
||||
|
||||
opt.typ.to_py(value) # for validation
|
||||
self._values[opt.name] = opt.typ.from_obj(value)
|
||||
values = self._get_values(pattern, create=True)
|
||||
values[opt.name] = opt.typ.from_obj(value)
|
||||
|
||||
self.changed.emit(opt.name)
|
||||
log.config.debug("Config option changed: {} = {}".format(
|
||||
@ -276,8 +312,9 @@ class Config(QObject):
|
||||
def read_yaml(self):
|
||||
"""Read the YAML settings from self._yaml."""
|
||||
self._yaml.load()
|
||||
for name, value in self._yaml:
|
||||
self._set_value(self.get_opt(name), value)
|
||||
# FIXME:conf implement in self._yaml
|
||||
for pattern, name, value in self._yaml:
|
||||
self._set_value(self.get_opt(name), value, pattern=pattern)
|
||||
|
||||
def get_opt(self, name):
|
||||
"""Get a configdata.Option object for the given setting."""
|
||||
@ -290,16 +327,18 @@ class Config(QObject):
|
||||
name, deleted=deleted, renamed=renamed)
|
||||
raise exception from None
|
||||
|
||||
def get(self, name):
|
||||
def get(self, name, pattern=None):
|
||||
"""Get the given setting converted for Python code."""
|
||||
opt = self.get_opt(name)
|
||||
obj = self.get_obj(name, mutable=False)
|
||||
obj = self.get_obj(name, mutable=False, pattern=pattern)
|
||||
return opt.typ.to_py(obj)
|
||||
|
||||
def get_obj(self, name, *, mutable=True):
|
||||
def get_obj(self, name, *, mutable=True, pattern=None):
|
||||
"""Get the given setting as object (for YAML/config.py).
|
||||
|
||||
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
|
||||
any).
|
||||
"""
|
||||
opt = self.get_opt(name)
|
||||
obj = None
|
||||
@ -311,7 +350,8 @@ class Config(QObject):
|
||||
# Otherwise, we return a copy of the value stored internally, so the
|
||||
# internal value can never be changed by mutating the object returned.
|
||||
else:
|
||||
obj = copy.deepcopy(self._values.get(name, opt.default))
|
||||
values = self._get_values(pattern)
|
||||
obj = copy.deepcopy(values.get(name, opt.default))
|
||||
# Then we watch the returned object for changes.
|
||||
if isinstance(obj, (dict, list)):
|
||||
if mutable:
|
||||
@ -321,22 +361,23 @@ class Config(QObject):
|
||||
assert obj.__hash__ is not None, obj
|
||||
return obj
|
||||
|
||||
def get_str(self, name):
|
||||
def get_str(self, name, *, pattern=None):
|
||||
"""Get the given setting as string."""
|
||||
opt = self.get_opt(name)
|
||||
value = self._values.get(name, opt.default)
|
||||
values = self._get_values(pattern)
|
||||
value = values.get(name, opt.default)
|
||||
return opt.typ.to_str(value)
|
||||
|
||||
def set_obj(self, name, value, *, save_yaml=False):
|
||||
def set_obj(self, name, value, *, pattern=None, save_yaml=False):
|
||||
"""Set the given setting from a YAML/config.py object.
|
||||
|
||||
If save_yaml=True is given, store the new value to YAML.
|
||||
"""
|
||||
self._set_value(self.get_opt(name), value)
|
||||
self._set_value(self.get_opt(name), value, pattern=pattern)
|
||||
if save_yaml:
|
||||
self._yaml[name] = value
|
||||
self._yaml.set_obj(name, value, pattern=pattern)
|
||||
|
||||
def set_str(self, name, value, *, save_yaml=False):
|
||||
def set_str(self, name, value, *, pattern=None, save_yaml=False):
|
||||
"""Set the given setting from a string.
|
||||
|
||||
If save_yaml=True is given, store the new value to YAML.
|
||||
@ -346,21 +387,22 @@ class Config(QObject):
|
||||
log.config.debug("Setting {} (type {}) to {!r} (converted from {!r})"
|
||||
.format(name, opt.typ.__class__.__name__, converted,
|
||||
value))
|
||||
self._set_value(opt, converted)
|
||||
self._set_value(opt, converted, pattern=pattern)
|
||||
if save_yaml:
|
||||
self._yaml[name] = converted
|
||||
self._yaml.set_obj(name, converted, pattern=pattern)
|
||||
|
||||
def unset(self, name, *, save_yaml=False):
|
||||
def unset(self, name, *, save_yaml=False, pattern=None):
|
||||
"""Set the given setting back to its default."""
|
||||
self.get_opt(name)
|
||||
values = self._get_values(pattern)
|
||||
try:
|
||||
del self._values[name]
|
||||
del values[name]
|
||||
except KeyError:
|
||||
return
|
||||
self.changed.emit(name)
|
||||
|
||||
if save_yaml:
|
||||
self._yaml.unset(name)
|
||||
self._yaml.unset(name, pattern=pattern)
|
||||
|
||||
def clear(self, *, save_yaml=False):
|
||||
"""Clear all settings in the config.
|
||||
@ -368,6 +410,7 @@ class Config(QObject):
|
||||
If save_yaml=True is given, also remove all customization from the YAML
|
||||
file.
|
||||
"""
|
||||
# FIXME:conf support per-URL settings?
|
||||
old_values = self._values
|
||||
self._values = {}
|
||||
for name in old_values:
|
||||
@ -398,9 +441,13 @@ class Config(QObject):
|
||||
The changed config part as string.
|
||||
"""
|
||||
lines = []
|
||||
for opt, value in self:
|
||||
for pattern, opt, value in self:
|
||||
str_value = opt.typ.to_str(value)
|
||||
lines.append('{} = {}'.format(opt.name, str_value))
|
||||
if pattern is None:
|
||||
lines.append('{} = {}'.format(opt.name, str_value))
|
||||
else:
|
||||
lines.append('{}: {} = {}'.format(pattern, opt.name,
|
||||
str_value))
|
||||
if not lines:
|
||||
lines = ['<Default configuration>']
|
||||
return '\n'.join(lines)
|
||||
|
@ -26,7 +26,7 @@ from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.completion.models import configmodel
|
||||
from qutebrowser.utils import objreg, utils, message, standarddir
|
||||
from qutebrowser.utils import objreg, utils, message, standarddir, urlmatch
|
||||
from qutebrowser.config import configtypes, configexc, configfiles, configdata
|
||||
from qutebrowser.misc import editor
|
||||
|
||||
@ -47,17 +47,29 @@ class ConfigCommands:
|
||||
except configexc.Error as e:
|
||||
raise cmdexc.CommandError(str(e))
|
||||
|
||||
def _print_value(self, option):
|
||||
def _parse_pattern(self, url):
|
||||
"""Parse an URL argument to a pattern."""
|
||||
if url is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return urlmatch.UrlPattern(url)
|
||||
except urlmatch.ParseError as e:
|
||||
raise cmdexc.CommandError("Error while parsing {}: {}"
|
||||
.format(url, str(e)))
|
||||
|
||||
def _print_value(self, option, pattern):
|
||||
"""Print the value of the given option."""
|
||||
with self._handle_config_error():
|
||||
value = self._config.get_str(option)
|
||||
value = self._config.get_str(option, pattern=pattern)
|
||||
message.info("{} = {}".format(option, value))
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.option)
|
||||
@cmdutils.argument('value', completion=configmodel.value)
|
||||
@cmdutils.argument('win_id', win_id=True)
|
||||
def set(self, win_id, option=None, value=None, temp=False, print_=False):
|
||||
def set(self, win_id, option=None, value=None, temp=False, print_=False,
|
||||
*, url=None):
|
||||
"""Set an option.
|
||||
|
||||
If the option name ends with '?', the value of the option is shown
|
||||
@ -69,6 +81,7 @@ class ConfigCommands:
|
||||
Args:
|
||||
option: The name of the option.
|
||||
value: The value to set.
|
||||
url: The URL pattern to use.
|
||||
temp: Set value temporarily until qutebrowser is closed.
|
||||
print_: Print the value after setting.
|
||||
"""
|
||||
@ -82,8 +95,10 @@ class ConfigCommands:
|
||||
raise cmdexc.CommandError("Toggling values was moved to the "
|
||||
":config-cycle command")
|
||||
|
||||
pattern = self._parse_pattern(url)
|
||||
|
||||
if option.endswith('?') and option != '?':
|
||||
self._print_value(option[:-1])
|
||||
self._print_value(option[:-1], pattern=pattern)
|
||||
return
|
||||
|
||||
with self._handle_config_error():
|
||||
@ -91,10 +106,11 @@ class ConfigCommands:
|
||||
raise cmdexc.CommandError("set: The following arguments "
|
||||
"are required: value")
|
||||
else:
|
||||
self._config.set_str(option, value, save_yaml=not temp)
|
||||
self._config.set_str(option, value, pattern=pattern,
|
||||
save_yaml=not temp)
|
||||
|
||||
if print_:
|
||||
self._print_value(option)
|
||||
self._print_value(option, pattern=pattern)
|
||||
|
||||
@cmdutils.register(instance='config-commands', maxsplit=1,
|
||||
no_cmd_split=True, no_replace_variables=True)
|
||||
@ -161,18 +177,22 @@ class ConfigCommands:
|
||||
@cmdutils.register(instance='config-commands', star_args_optional=True)
|
||||
@cmdutils.argument('option', completion=configmodel.option)
|
||||
@cmdutils.argument('values', completion=configmodel.value)
|
||||
def config_cycle(self, option, *values, temp=False, print_=False):
|
||||
def config_cycle(self, option, *values, url=None, temp=False, print_=False):
|
||||
"""Cycle an option between multiple values.
|
||||
|
||||
Args:
|
||||
option: The name of the option.
|
||||
values: The values to cycle through.
|
||||
url: The URL pattern to use.
|
||||
temp: Set value temporarily until qutebrowser is closed.
|
||||
print_: Print the value after setting.
|
||||
"""
|
||||
pattern = self._parse_pattern(url)
|
||||
|
||||
with self._handle_config_error():
|
||||
opt = self._config.get_opt(option)
|
||||
old_value = self._config.get_obj(option, mutable=False)
|
||||
old_value = self._config.get_obj(option, mutable=False,
|
||||
pattern=pattern)
|
||||
|
||||
if not values and isinstance(opt.typ, configtypes.Bool):
|
||||
values = ['true', 'false']
|
||||
@ -194,10 +214,11 @@ class ConfigCommands:
|
||||
value = values[0]
|
||||
|
||||
with self._handle_config_error():
|
||||
self._config.set_obj(option, value, save_yaml=not temp)
|
||||
self._config.set_obj(option, value, pattern=pattern,
|
||||
save_yaml=not temp)
|
||||
|
||||
if print_:
|
||||
self._print_value(option)
|
||||
self._print_value(option, pattern=pattern)
|
||||
|
||||
@cmdutils.register(instance='config-commands')
|
||||
@cmdutils.argument('option', completion=configmodel.customized_option)
|
||||
|
@ -88,6 +88,7 @@ class YamlConfig(QObject):
|
||||
self._filename = os.path.join(standarddir.config(auto=True),
|
||||
'autoconfig.yml')
|
||||
self._values = {}
|
||||
self._per_url_values = {}
|
||||
self._dirty = None
|
||||
|
||||
def init_save_manager(self, save_manager):
|
||||
@ -98,18 +99,24 @@ class YamlConfig(QObject):
|
||||
"""
|
||||
save_manager.add_saveable('yaml-config', self._save, self.changed)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self._values[name]
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
self._values[name] = value
|
||||
self._mark_changed()
|
||||
|
||||
def __contains__(self, name):
|
||||
return name in self._values
|
||||
|
||||
def __iter__(self):
|
||||
return iter(sorted(self._values.items()))
|
||||
for name, value in sorted(self._values.items()):
|
||||
yield (None, name, value)
|
||||
|
||||
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):
|
||||
"""Mark the YAML config as changed."""
|
||||
@ -122,6 +129,9 @@ class YamlConfig(QObject):
|
||||
return
|
||||
|
||||
data = {'config_version': self.VERSION, 'global': self._values}
|
||||
for pattern, values in sorted(self._per_url_values.items()):
|
||||
data[str(pattern)] = values
|
||||
|
||||
with qtutils.savefile_open(self._filename) as f:
|
||||
f.write(textwrap.dedent("""
|
||||
# DO NOT edit this file by hand, qutebrowser will overwrite it.
|
||||
@ -145,7 +155,7 @@ class YamlConfig(QObject):
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
||||
|
||||
try:
|
||||
global_obj = yaml_data['global']
|
||||
global_obj = yaml_data.pop('global')
|
||||
except KeyError:
|
||||
desc = configexc.ConfigErrorDesc(
|
||||
"While loading data",
|
||||
@ -156,6 +166,15 @@ class YamlConfig(QObject):
|
||||
"Toplevel object is not a dict")
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
||||
|
||||
# FIXME:conf test this
|
||||
try:
|
||||
yaml_data.pop('config_version')
|
||||
except KeyError:
|
||||
desc = configexc.ConfigErrorDesc(
|
||||
"While loading data",
|
||||
"Toplevel object does not contain 'config_version' key")
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
||||
|
||||
if not isinstance(global_obj, dict):
|
||||
desc = configexc.ConfigErrorDesc(
|
||||
"While loading data",
|
||||
@ -163,6 +182,7 @@ class YamlConfig(QObject):
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', [desc])
|
||||
|
||||
self._values = global_obj
|
||||
self._per_url_values = yaml_data
|
||||
self._dirty = False
|
||||
|
||||
self._handle_migrations()
|
||||
@ -170,6 +190,7 @@ class YamlConfig(QObject):
|
||||
|
||||
def _handle_migrations(self):
|
||||
"""Migrate older configs to the newest format."""
|
||||
# FIXME:conf handle per-URL settings
|
||||
# Simple renamed/deleted options
|
||||
for name in list(self._values):
|
||||
if name in configdata.MIGRATIONS.renamed:
|
||||
@ -196,23 +217,35 @@ class YamlConfig(QObject):
|
||||
|
||||
def _validate(self):
|
||||
"""Make sure all settings exist."""
|
||||
unknown = set(self._values) - set(configdata.DATA)
|
||||
unknown = []
|
||||
for _pattern, name, value in self:
|
||||
# FIXME:conf show pattern
|
||||
if name not in configdata.DATA:
|
||||
unknown.append(name)
|
||||
|
||||
if unknown:
|
||||
errors = [configexc.ConfigErrorDesc("While loading options",
|
||||
"Unknown option {}".format(e))
|
||||
for e in sorted(unknown)]
|
||||
raise configexc.ConfigFileErrors('autoconfig.yml', errors)
|
||||
|
||||
def unset(self, name):
|
||||
def set_obj(self, name, value, *, pattern=None):
|
||||
"""Set the given setting to the given value."""
|
||||
values = self._get_values(pattern)
|
||||
values[name] = value
|
||||
|
||||
def unset(self, name, *, pattern=None):
|
||||
"""Remove the given option name if it's configured."""
|
||||
values = self._get_values(pattern)
|
||||
try:
|
||||
del self._values[name]
|
||||
del values[name]
|
||||
except KeyError:
|
||||
return
|
||||
self._mark_changed()
|
||||
|
||||
def clear(self):
|
||||
"""Clear all values from the YAML file."""
|
||||
# FIXME:conf per-URL support?
|
||||
self._values = []
|
||||
self._mark_changed()
|
||||
|
||||
|
@ -95,6 +95,17 @@ class UrlPattern:
|
||||
self._init_path(parsed)
|
||||
self._init_port(parsed)
|
||||
|
||||
def _to_tuple(self):
|
||||
"""Get a pattern with information used for __eq__/__hash__."""
|
||||
return (self._match_all, self._match_subdomains, self._scheme,
|
||||
self._host, self._path, self._port)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._to_tuple())
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._to_tuple() == other._to_tuple()
|
||||
|
||||
def _fixup_pattern(self, pattern):
|
||||
"""Make sure the given pattern is parseable by urllib.parse."""
|
||||
if pattern.startswith('*:'): # Any scheme, but *:// is unparseable
|
||||
|
Loading…
Reference in New Issue
Block a user