Merge 890adf3a65 into 7e3df43463
This commit is contained in:
commit
5ccf31fe3a
|
|
@ -20,7 +20,7 @@ from qutebrowser.config import config
|
|||
#: This also supports setting configuration values::
|
||||
#:
|
||||
#: config.val.content.javascript.enabled = False
|
||||
val = cast('config.ConfigContainer', None)
|
||||
val = cast('config.ConfigContainerInternal', None)
|
||||
|
||||
|
||||
def get(name: str, url: QUrl = None) -> Any:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
from typing import cast
|
||||
|
||||
from qutebrowser.config.configcontainer_types import ConfigContainer # pylint: disable=unused-import
|
||||
from qutebrowser.config import configfiles # pylint: disable=unused-import
|
||||
|
||||
|
||||
c = cast('ConfigContainer', None)
|
||||
config = cast('configfiles.ConfigAPI', None)
|
||||
|
|
@ -18,12 +18,16 @@ from qutebrowser.utils import utils, log, urlmatch
|
|||
from qutebrowser.misc import objects
|
||||
from qutebrowser.keyinput import keyutils
|
||||
|
||||
# alias to the generated type for back-compat
|
||||
from qutebrowser.config.configcontainer_types import ConfigContainer # pylint: disable=unused-import
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from qutebrowser.config import configcache, configfiles
|
||||
from qutebrowser.misc import savemanager
|
||||
|
||||
|
||||
# An easy way to access the config from other code via config.val.foo
|
||||
val = cast('ConfigContainer', None)
|
||||
val = cast('ConfigContainerInternal', None)
|
||||
instance = cast('Config', None)
|
||||
key_instance = cast('KeyConfig', None)
|
||||
cache = cast('configcache.ConfigCache', None)
|
||||
|
|
@ -569,7 +573,7 @@ class Config(QObject):
|
|||
return '\n'.join(lines)
|
||||
|
||||
|
||||
class ConfigContainer:
|
||||
class ConfigContainerInternal:
|
||||
|
||||
"""An object implementing config access via __getattr__.
|
||||
|
||||
|
|
@ -607,9 +611,9 @@ class ConfigContainer:
|
|||
text = f"While {action}"
|
||||
self._configapi.errors.append(configexc.ConfigErrorDesc(text, e))
|
||||
|
||||
def _with_prefix(self, prefix: str) -> 'ConfigContainer':
|
||||
def _with_prefix(self, prefix: str) -> 'ConfigContainerInternal':
|
||||
"""Get a new ConfigContainer for the given prefix."""
|
||||
return ConfigContainer(
|
||||
return ConfigContainerInternal(
|
||||
config=self._config,
|
||||
configapi=self._configapi,
|
||||
pattern=self._pattern,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -21,12 +21,14 @@ import yaml
|
|||
from qutebrowser.qt.core import pyqtSignal, pyqtSlot, QObject, QSettings, qVersion
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.api import configpy
|
||||
from qutebrowser.config import (configexc, config, configdata, configutils,
|
||||
configtypes)
|
||||
from qutebrowser.keyinput import keyutils
|
||||
from qutebrowser.utils import standarddir, utils, qtutils, log, urlmatch, version
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from qutebrowser.config.configcontainer_types import ConfigContainer
|
||||
from qutebrowser.misc import savemanager
|
||||
|
||||
|
||||
|
|
@ -787,12 +789,12 @@ class ConfigAPI:
|
|||
self.errors += e.errors
|
||||
|
||||
@contextlib.contextmanager
|
||||
def pattern(self, pattern: str) -> Iterator[config.ConfigContainer]:
|
||||
def pattern(self, pattern: str) -> Iterator[config.ConfigContainerInternal]:
|
||||
"""Get a ConfigContainer for the given pattern."""
|
||||
# We need to propagate the exception so we don't need to return
|
||||
# something.
|
||||
urlpattern = urlmatch.UrlPattern(pattern)
|
||||
container = config.ConfigContainer(config=self._config, configapi=self,
|
||||
container = config.ConfigContainerInternal(config=self._config, configapi=self,
|
||||
pattern=urlpattern)
|
||||
yield container
|
||||
|
||||
|
|
@ -860,6 +862,8 @@ class ConfigPyWriter:
|
|||
yield self._line("# qute://help/configuring.html")
|
||||
yield self._line("# qute://help/settings.html")
|
||||
yield ''
|
||||
yield 'from qutebrowser.api.configpy import c, config'
|
||||
yield ''
|
||||
if self._commented:
|
||||
# When generated from an autoconfig.yml with commented=False,
|
||||
# we don't want to load that autoconfig.yml anymore.
|
||||
|
|
@ -950,9 +954,14 @@ def read_config_py(
|
|||
config.key_instance,
|
||||
warn_autoconfig=warn_autoconfig,
|
||||
)
|
||||
container = config.ConfigContainer(config.instance, configapi=api)
|
||||
|
||||
container = config.ConfigContainerInternal(config.instance, configapi=api)
|
||||
basename = os.path.basename(filename)
|
||||
|
||||
# used for `from qutebrowser.api.configpy import c, config`
|
||||
configpy.config = api
|
||||
configpy.c = cast('ConfigContainer', container)
|
||||
|
||||
module = types.ModuleType('config')
|
||||
module.config = api # type: ignore[attr-defined]
|
||||
module.c = container # type: ignore[attr-defined]
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ def early_init(args: argparse.Namespace) -> None:
|
|||
yaml_config = configfiles.YamlConfig()
|
||||
|
||||
config.instance = config.Config(yaml_config=yaml_config)
|
||||
config.val = config.ConfigContainer(config.instance)
|
||||
configapi.val = config.ConfigContainer(config.instance)
|
||||
config.val = config.ConfigContainerInternal(config.instance)
|
||||
configapi.val = config.ConfigContainerInternal(config.instance)
|
||||
config.key_instance = config.KeyConfig(config.instance)
|
||||
config.cache = configcache.ConfigCache()
|
||||
yaml_config.setParent(config.instance)
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import functools
|
|||
import operator
|
||||
import json
|
||||
import dataclasses
|
||||
from abc import abstractmethod
|
||||
from typing import Any, Optional, Union
|
||||
from re import Pattern
|
||||
from collections.abc import Iterable, Iterator, Sequence, Callable
|
||||
|
|
@ -131,6 +132,13 @@ class ValidValues:
|
|||
if desc is not None:
|
||||
self.descriptions[val] = desc
|
||||
|
||||
def py_type(self) -> str:
|
||||
"""Generate a `Literal` Python type annotation for the valid values."""
|
||||
typ = f'Literal[{", ".join(repr(k) for k in self.values)}]'
|
||||
if self.others_permitted:
|
||||
return f'Union[{typ}, str]'
|
||||
return typ
|
||||
|
||||
def __contains__(self, val: str) -> bool:
|
||||
return val in self.values
|
||||
|
||||
|
|
@ -305,6 +313,21 @@ class BaseType:
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def py_type(self) -> str:
|
||||
"""Generate a Python type annotation for this type."""
|
||||
typ = self.valid_values.py_type() if self.valid_values else self._py_type()
|
||||
if self.none_ok:
|
||||
return f'Optional[{typ}]'
|
||||
return typ
|
||||
|
||||
@abstractmethod
|
||||
def _py_type(self) -> str:
|
||||
"""An internal version of `.py_type()` so that we don't have to handle `None` everywhere.
|
||||
|
||||
This must be implemented by all subclasses of `BaseType`.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def to_str(self, value: Any) -> str:
|
||||
"""Get a string from the setting value.
|
||||
|
||||
|
|
@ -367,6 +390,9 @@ class MappingType(BaseType):
|
|||
self.valid_values = ValidValues(
|
||||
*[(key, doc) for (key, (_val, doc)) in self.MAPPING.items()])
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return f'Literal[{", ".join(repr(k) for k in self.MAPPING)}]'
|
||||
|
||||
def to_py(self, value: Any) -> Any:
|
||||
self._basic_py_validation(value, str)
|
||||
if isinstance(value, usertypes.Unset):
|
||||
|
|
@ -424,6 +450,9 @@ class String(BaseType):
|
|||
self.encoding = encoding
|
||||
self.regex = regex
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
|
||||
self._basic_py_validation(value, str)
|
||||
if isinstance(value, usertypes.Unset):
|
||||
|
|
@ -499,6 +528,9 @@ class List(BaseType):
|
|||
self.valtype = valtype
|
||||
self.length = length
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return f'list[{self.valtype.py_type()}]'
|
||||
|
||||
def get_name(self) -> str:
|
||||
name = super().get_name()
|
||||
if self._show_valtype:
|
||||
|
|
@ -597,6 +629,9 @@ class ListOrValue(BaseType):
|
|||
self.listtype = List(valtype=valtype, none_ok=none_ok, **kwargs)
|
||||
self.valtype = valtype
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return f'Union[{self.valtype.py_type()}, {self.listtype.py_type()}]'
|
||||
|
||||
def _val_and_type(self, value: Any) -> tuple[Any, BaseType]:
|
||||
"""Get the value and type to use for to_str/to_doc/from_str."""
|
||||
if isinstance(value, list):
|
||||
|
|
@ -737,6 +772,15 @@ class Bool(BaseType):
|
|||
super().__init__(none_ok=none_ok, completions=completions)
|
||||
self.valid_values = ValidValues('true', 'false', generate_docs=False)
|
||||
|
||||
def py_type(self) -> str:
|
||||
typ = self._py_type()
|
||||
if self.none_ok:
|
||||
return f'Optional[{typ}]'
|
||||
return typ
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'bool'
|
||||
|
||||
def to_py(self, value: Union[bool, str, None]) -> Optional[bool]:
|
||||
self._basic_py_validation(value, bool)
|
||||
assert not isinstance(value, str)
|
||||
|
|
@ -773,6 +817,9 @@ class BoolAsk(Bool):
|
|||
super().__init__(none_ok=none_ok, completions=completions)
|
||||
self.valid_values = ValidValues('true', 'false', 'ask')
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return "Union[bool, Literal['ask']]"
|
||||
|
||||
def to_py(self, # type: ignore[override]
|
||||
value: Union[bool, str]) -> Union[bool, str, None]:
|
||||
# basic validation unneeded if it's == 'ask' and done by Bool if we
|
||||
|
|
@ -869,6 +916,9 @@ class Int(_Numeric):
|
|||
|
||||
"""Base class for an integer setting."""
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'int'
|
||||
|
||||
def from_str(self, value: str) -> Optional[int]:
|
||||
self._basic_str_validation(value)
|
||||
if not value:
|
||||
|
|
@ -891,6 +941,9 @@ class Float(_Numeric):
|
|||
|
||||
"""Base class for a float setting."""
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'float'
|
||||
|
||||
def from_str(self, value: str) -> Optional[float]:
|
||||
self._basic_str_validation(value)
|
||||
if not value:
|
||||
|
|
@ -916,6 +969,9 @@ class Perc(_Numeric):
|
|||
|
||||
"""A percentage."""
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'Union[float, int, str]'
|
||||
|
||||
def to_py(
|
||||
self,
|
||||
value: Union[float, int, str, _UnsetNone]
|
||||
|
|
@ -978,6 +1034,9 @@ class PercOrInt(_Numeric):
|
|||
raise ValueError("minperc ({}) needs to be <= maxperc "
|
||||
"({})!".format(self.minperc, self.maxperc))
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'Union[int, str]'
|
||||
|
||||
def from_str(self, value: str) -> Union[None, str, int]:
|
||||
self._basic_str_validation(value)
|
||||
if not value:
|
||||
|
|
@ -1040,6 +1099,9 @@ class Command(BaseType):
|
|||
invalid commands (in bindings/aliases) fail when used.
|
||||
"""
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def complete(self) -> _Completions:
|
||||
if self._completions is not None:
|
||||
return self._completions
|
||||
|
|
@ -1094,6 +1156,9 @@ class QtColor(BaseType):
|
|||
* `hsv(h, s, v)` / `hsva(h, s, v, a)` (values 0-255, hue 0-359)
|
||||
"""
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def _parse_value(self, kind: str, val: str) -> int:
|
||||
try:
|
||||
return int(val)
|
||||
|
|
@ -1168,6 +1233,9 @@ class QssColor(BaseType):
|
|||
under ``Gradient''
|
||||
"""
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
|
||||
self._basic_py_validation(value, str)
|
||||
if isinstance(value, usertypes.Unset):
|
||||
|
|
@ -1211,6 +1279,9 @@ class FontBase(BaseType):
|
|||
)* # 0-inf size/weight/style tags
|
||||
(?P<family>.+) # mandatory font family""", re.VERBOSE)
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
@classmethod
|
||||
def set_defaults(cls, default_family: list[str], default_size: str) -> None:
|
||||
"""Make sure default_family/default_size are available.
|
||||
|
|
@ -1313,6 +1384,9 @@ class Regex(BaseType):
|
|||
operator.or_,
|
||||
(getattr(re, flag.strip()) for flag in flags.split(' | ')))
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'Union[str, re.Pattern[str]]'
|
||||
|
||||
def _compile_regex(self, pattern: str) -> Pattern[str]:
|
||||
"""Check if the given regex is valid.
|
||||
|
||||
|
|
@ -1385,6 +1459,9 @@ class Dict(BaseType):
|
|||
self.fixed_keys = fixed_keys
|
||||
self.required_keys = required_keys
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return f"Mapping[{self.keytype.py_type()}, {self.valtype.py_type()}]"
|
||||
|
||||
def _validate_keys(self, value: dict) -> None:
|
||||
if (self.fixed_keys is not None and not
|
||||
set(value.keys()).issubset(self.fixed_keys)):
|
||||
|
|
@ -1484,6 +1561,9 @@ class File(BaseType):
|
|||
super().__init__(none_ok=none_ok, completions=completions)
|
||||
self.required = required
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
|
||||
self._basic_py_validation(value, str)
|
||||
if isinstance(value, usertypes.Unset):
|
||||
|
|
@ -1516,6 +1596,9 @@ class Directory(BaseType):
|
|||
|
||||
"""A directory on the local filesystem."""
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
|
||||
self._basic_py_validation(value, str)
|
||||
if isinstance(value, usertypes.Unset):
|
||||
|
|
@ -1560,6 +1643,9 @@ class FormatString(BaseType):
|
|||
self.encoding = encoding
|
||||
self._completions = completions
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
|
||||
self._basic_py_validation(value, str)
|
||||
if isinstance(value, usertypes.Unset):
|
||||
|
|
@ -1643,6 +1729,11 @@ class Proxy(BaseType):
|
|||
others_permitted=True,
|
||||
)
|
||||
|
||||
def _py_type(self) -> str:
|
||||
# this should always be set as it is assigned in __init__
|
||||
assert self.valid_values is not None
|
||||
return f'Union[{self.valid_values.py_type()}, str]'
|
||||
|
||||
def to_py(
|
||||
self,
|
||||
value: _StrUnset
|
||||
|
|
@ -1689,6 +1780,9 @@ class SearchEngineUrl(BaseType):
|
|||
|
||||
"""A search engine URL."""
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
|
||||
self._basic_py_validation(value, str)
|
||||
if isinstance(value, usertypes.Unset):
|
||||
|
|
@ -1719,6 +1813,9 @@ class FuzzyUrl(BaseType):
|
|||
|
||||
"""A URL which gets interpreted as search if needed."""
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def to_py(self, value: _StrUnset) -> Union[QUrl, _UnsetNone]:
|
||||
self._basic_py_validation(value, str)
|
||||
if isinstance(value, usertypes.Unset):
|
||||
|
|
@ -1777,6 +1874,9 @@ class Encoding(BaseType):
|
|||
|
||||
"""Setting for a python encoding."""
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
|
||||
self._basic_py_validation(value, str)
|
||||
if isinstance(value, usertypes.Unset):
|
||||
|
|
@ -1842,6 +1942,9 @@ class Url(BaseType):
|
|||
|
||||
"""A URL as a string."""
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def to_py(self, value: _StrUnset) -> Union[_UnsetNone, QUrl]:
|
||||
self._basic_py_validation(value, str)
|
||||
if isinstance(value, usertypes.Unset):
|
||||
|
|
@ -1860,6 +1963,9 @@ class SessionName(BaseType):
|
|||
|
||||
"""The name of a session."""
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def to_py(self, value: _StrUnset) -> _StrUnsetNone:
|
||||
self._basic_py_validation(value, str)
|
||||
if isinstance(value, usertypes.Unset):
|
||||
|
|
@ -1976,6 +2082,9 @@ class Key(BaseType):
|
|||
"""Make sure key sequences are always normalized."""
|
||||
return str(keyutils.KeySequence.parse(value))
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def to_py(
|
||||
self,
|
||||
value: _StrUnset
|
||||
|
|
@ -2001,6 +2110,9 @@ class UrlPattern(BaseType):
|
|||
for the allowed syntax.
|
||||
"""
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'str'
|
||||
|
||||
def to_py(
|
||||
self,
|
||||
value: _StrUnset
|
||||
|
|
|
|||
|
|
@ -363,7 +363,7 @@ def change_console_formatter(level: int) -> None:
|
|||
assert isinstance(old_formatter, JSONFormatter), old_formatter
|
||||
|
||||
|
||||
def init_from_config(conf: 'configmodule.ConfigContainer') -> None:
|
||||
def init_from_config(conf: 'configmodule.ConfigContainerInternal') -> None:
|
||||
"""Initialize logging settings from the config.
|
||||
|
||||
init_log is called before the config module is initialized, so config-based
|
||||
|
|
|
|||
|
|
@ -0,0 +1,152 @@
|
|||
"""This script auto-generates the `qutebrowser/config/configcontainer.py` file."""
|
||||
|
||||
import pathlib
|
||||
import textwrap
|
||||
from typing import TYPE_CHECKING, NoReturn, Union
|
||||
from collections.abc import Mapping, Iterator
|
||||
|
||||
from qutebrowser.config import configdata
|
||||
from qutebrowser.config.configdata import Option
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import assert_never
|
||||
else:
|
||||
|
||||
def assert_never(value) -> NoReturn:
|
||||
raise AssertionError(f"Expected code to be unreachable, but got: {repr(value)}")
|
||||
|
||||
|
||||
NestedConfig = dict[str, Union[Option, "NestedConfig"]]
|
||||
|
||||
|
||||
def make_nested_config(config: Mapping[str, Option]) -> NestedConfig:
|
||||
"""The original configdata.yml defines nested keys using `.`s in a flat tree.
|
||||
|
||||
This function returns a new dict where options are grouped and nested, e.g.
|
||||
`'auto_save.session': Option(...)` -> `{'auto_save': {'session': Option(...)}}`
|
||||
"""
|
||||
result = {}
|
||||
for key, value in config.items():
|
||||
parts = key.split(".")
|
||||
current = result
|
||||
for part in parts[:-1]:
|
||||
current = current.setdefault(part, {})
|
||||
current[parts[-1]] = value
|
||||
return result
|
||||
|
||||
|
||||
def generate_config_types(config_data: Mapping[str, Option]) -> Iterator[str]:
|
||||
"""Generate the `ConfigContainer` dataclass and all nested types."""
|
||||
yield "# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>"
|
||||
yield "#"
|
||||
yield "# SPDX-License-Identifier: GPL-3.0-or-later"
|
||||
yield ""
|
||||
yield "# DO NOT EDIT THIS FILE DIRECTLY!"
|
||||
yield "# It is autogenerated by running:"
|
||||
yield "# $ python3 scripts/dev/src2asciidoc.py"
|
||||
yield "# vim: readonly:"
|
||||
yield ""
|
||||
yield triple_quote(
|
||||
textwrap.dedent("""
|
||||
This file defines static types for the `c` variable in `config.py`.
|
||||
|
||||
This is auto-generated from the `scripts/dev/generate_config_types.py` file.
|
||||
|
||||
It is not intended to be used at runtime.
|
||||
|
||||
Example usage:
|
||||
```py
|
||||
from qutebrowser.api.configpy import c, config
|
||||
```
|
||||
""").lstrip()
|
||||
)
|
||||
yield ""
|
||||
yield "# pylint: disable=line-too-long, invalid-name"
|
||||
yield ""
|
||||
yield "import re"
|
||||
yield "from dataclasses import dataclass"
|
||||
yield "from collections.abc import Mapping"
|
||||
yield "from typing import Optional, Union, Literal"
|
||||
yield ""
|
||||
yield ""
|
||||
|
||||
def generate_class(
|
||||
class_name: str,
|
||||
config: NestedConfig,
|
||||
*,
|
||||
indent: str = "",
|
||||
description: 'str | None' = None,
|
||||
) -> Iterator[str]:
|
||||
yield f"{indent}@dataclass"
|
||||
yield f"{indent}class {class_name}:"
|
||||
|
||||
if description is not None:
|
||||
yield f"{indent} {triple_quote(description)}"
|
||||
|
||||
for key, value in config.items():
|
||||
if isinstance(value, Option):
|
||||
type_hint = value.typ.py_type()
|
||||
if value.default is None:
|
||||
yield f"{indent} {key}: Optional[{type_hint}]"
|
||||
else:
|
||||
yield f"{indent} {key}: {type_hint}"
|
||||
|
||||
if value.description:
|
||||
lines = value.description.split("\n")
|
||||
description = "\n\n".join(
|
||||
[
|
||||
line if i == 0 else f"{indent} {line}"
|
||||
for i, line in enumerate(lines)
|
||||
]
|
||||
)
|
||||
if not description.endswith("\n") and len(lines) > 1:
|
||||
description += f"\n{indent} "
|
||||
|
||||
yield f"{indent} {triple_quote(description)}\n"
|
||||
elif isinstance(value, dict):
|
||||
nested_class_name = "_" + snake_to_camel(key)
|
||||
yield f"{indent} {key}: '{nested_class_name}'"
|
||||
yield from generate_class(
|
||||
nested_class_name, value, indent=indent + " "
|
||||
)
|
||||
else:
|
||||
assert_never(value)
|
||||
|
||||
nested_config = make_nested_config(config_data)
|
||||
yield from generate_class(
|
||||
"ConfigContainer",
|
||||
nested_config,
|
||||
description="Type for the `c` variable in `config.py`.",
|
||||
)
|
||||
|
||||
|
||||
def snake_to_camel(name: str) -> str:
|
||||
return "".join(word.capitalize() for word in name.split("_"))
|
||||
|
||||
|
||||
def triple_quote(v: str) -> str:
|
||||
"""surround the given string with trible double quotes."""
|
||||
# some option descriptions use `\+` which isn't a valid escape sequence
|
||||
# in python docstrings, so just escape it.
|
||||
return '"""' + v.replace(r"\+", r"\\+") + '"""'
|
||||
|
||||
|
||||
def main():
|
||||
configdata.init()
|
||||
|
||||
generated_code = "\n".join(generate_config_types(configdata.DATA))
|
||||
|
||||
output_file = (
|
||||
pathlib.Path(__file__).parent.parent.parent
|
||||
/ "qutebrowser"
|
||||
/ "config"
|
||||
/ "configcontainer_types.py"
|
||||
)
|
||||
output_file.write_text(generated_code)
|
||||
|
||||
print(f"Config types have been written to {output_file.resolve()}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -178,7 +178,8 @@ def run(files):
|
|||
vult = vulture.Vulture(verbose=False)
|
||||
vult.scavenge(
|
||||
files + [whitelist_file.name],
|
||||
exclude=["qutebrowser/qt/_core_pyqtproperty.py"],
|
||||
exclude=["qutebrowser/qt/_core_pyqtproperty.py",
|
||||
"qutebrowser/config/configcontainer_types.py"],
|
||||
)
|
||||
|
||||
os.remove(whitelist_file.name)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from qutebrowser.config import configdata, configtypes
|
|||
from qutebrowser.utils import docutils, usertypes
|
||||
from qutebrowser.misc import objects
|
||||
from scripts import asciidoc2html, utils
|
||||
from scripts.dev import generate_config_types
|
||||
|
||||
FILE_HEADER = """
|
||||
// DO NOT EDIT THIS FILE DIRECTLY!
|
||||
|
|
@ -572,6 +573,8 @@ def main():
|
|||
regenerate_cheatsheet()
|
||||
if '--html' in sys.argv:
|
||||
asciidoc2html.main()
|
||||
print("Generating config.py types...")
|
||||
generate_config_types.main()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ def config_stub(stubs, monkeypatch, configdata_init, yaml_config_stub, qapp):
|
|||
conf = config.Config(yaml_config=yaml_config_stub)
|
||||
monkeypatch.setattr(config, 'instance', conf)
|
||||
|
||||
container = config.ConfigContainer(conf)
|
||||
container = config.ConfigContainerInternal(conf)
|
||||
monkeypatch.setattr(config, 'val', container)
|
||||
monkeypatch.setattr(configapi, 'val', container)
|
||||
|
||||
|
|
|
|||
|
|
@ -751,7 +751,7 @@ class TestContainer:
|
|||
|
||||
@pytest.fixture
|
||||
def container(self, config_stub):
|
||||
return config.ConfigContainer(config_stub)
|
||||
return config.ConfigContainerInternal(config_stub)
|
||||
|
||||
def test_getattr_invalid_private(self, container):
|
||||
"""Make sure an invalid _attribute doesn't try getting a container."""
|
||||
|
|
@ -813,4 +813,4 @@ class TestContainer:
|
|||
pattern = urlmatch.UrlPattern('https://example.com/')
|
||||
with pytest.raises(TypeError,
|
||||
match="Can't use pattern without configapi!"):
|
||||
config.ConfigContainer(config_stub, pattern=pattern)
|
||||
config.ConfigContainerInternal(config_stub, pattern=pattern)
|
||||
|
|
|
|||
|
|
@ -1007,6 +1007,28 @@ class TestConfigPy:
|
|||
'assert directory.exists()')
|
||||
confpy.read()
|
||||
|
||||
def test_c_import(self, confpy):
|
||||
confpy.write('from qutebrowser.api.configpy import c',
|
||||
'c.colors.hints.bg = "red"')
|
||||
confpy.read()
|
||||
assert config.instance.get_obj('colors.hints.bg') == 'red'
|
||||
|
||||
def test_config_import(self, confpy):
|
||||
confpy.write('from qutebrowser.api.configpy import config',
|
||||
'config.set("colors.hints.bg", "red")')
|
||||
confpy.read()
|
||||
assert config.instance.get_obj('colors.hints.bg') == 'red'
|
||||
|
||||
def test_c_import_id(self, confpy):
|
||||
confpy.write('from qutebrowser.api import configpy',
|
||||
'assert configpy.c is c')
|
||||
confpy.read()
|
||||
|
||||
def test_config_import_id(self, confpy):
|
||||
confpy.write('from qutebrowser.api import configpy',
|
||||
'assert configpy.config is config')
|
||||
confpy.read()
|
||||
|
||||
@pytest.mark.parametrize('line', [
|
||||
'c.colors.hints.bg = "red"',
|
||||
'config.set("colors.hints.bg", "red")',
|
||||
|
|
@ -1426,6 +1448,8 @@ class TestConfigPyWriter:
|
|||
# qute://help/configuring.html
|
||||
# qute://help/settings.html
|
||||
|
||||
from qutebrowser.api.configpy import c, config
|
||||
|
||||
# Change the argument to True to still load settings configured via autoconfig.yml
|
||||
config.load_autoconfig(False)
|
||||
|
||||
|
|
|
|||
|
|
@ -248,6 +248,9 @@ class TestAll:
|
|||
|
||||
assert converted == s
|
||||
|
||||
def test_py_type_is_defined(self, klass):
|
||||
assert isinstance(klass().py_type(), str)
|
||||
|
||||
def test_none_ok_true(self, klass):
|
||||
"""Test None and empty string values with none_ok=True."""
|
||||
typ = klass(none_ok=True)
|
||||
|
|
@ -262,6 +265,7 @@ class TestAll:
|
|||
assert typ.from_str('') is None
|
||||
assert typ.to_py(None) == to_py_expected
|
||||
assert typ.to_str(None) == ''
|
||||
assert 'Optional' in typ.py_type()
|
||||
|
||||
@pytest.mark.parametrize('method, value', [
|
||||
('from_str', ''),
|
||||
|
|
@ -558,6 +562,9 @@ class FromObjType(configtypes.BaseType):
|
|||
def from_obj(self, value):
|
||||
return int(value)
|
||||
|
||||
def _py_type(self) -> str:
|
||||
return 'Any'
|
||||
|
||||
def to_py(self, value):
|
||||
return value
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue