Add config validation using the base.Core interface

- Config objects now have a validate() method that validates a config
against a core
- Use this validation method on file read
- Pass the Core object to the window manager on startup
- Begin to progressively add "just enough" type annotations as we touch
code.
master
Aldo Cortesi 2019-01-19 10:40:29 +13:00
parent ab3dc678af
commit 13d9c4e80b
12 changed files with 88 additions and 34 deletions

1
.gitignore vendored
View File

@ -41,3 +41,4 @@ debian/*debhelper
.mypy_cache/
.tags
venv
.coverage.*

View File

@ -29,7 +29,6 @@ from . import command
from . import configurable
from . import hook
from . import utils
from .core import xcbq
import sys
import warnings
@ -61,7 +60,16 @@ class Key:
return "<Key (%s, %s)>" % (self.modifiers, self.key)
class Drag:
class Mouse:
def __init__(self, modifiers, button, *commands, **kwargs):
self.focus = kwargs.get("focus", "before")
self.modifiers = modifiers
self.button = button
self.commands = commands
self.button_code = int(self.button.replace('Button', ''))
class Drag(Mouse):
"""Defines binding of a mouse to some dragging action
On each motion event command is executed with two extra parameters added x
@ -70,30 +78,18 @@ class Drag:
It focuses clicked window by default. If you want to prevent it pass,
`focus=None` as an argument
"""
def __init__(self, modifiers, button, *commands, **kwargs):
self.start = kwargs.get("start")
self.focus = kwargs.get("focus", "before")
self.modifiers = modifiers
self.button = button
self.commands = commands
self.button_code = int(self.button.replace('Button', ''))
def __repr__(self):
return "<Drag (%s, %s)>" % (self.modifiers, self.button)
class Click:
class Click(Mouse):
"""Defines binding of a mouse click
It focuses clicked window by default. If you want to prevent it, pass
`focus=None` as an argument
"""
def __init__(self, modifiers, button, *commands, **kwargs):
self.focus = kwargs.get("focus", "before")
self.modifiers = modifiers
self.button = button
self.commands = commands
self.button_code = int(self.button.replace('Button', ''))
super().__init__(modifiers, button, *commands, **kwargs)
def __repr__(self):
return "<Click (%s, %s)>" % (self.modifiers, self.button)

View File

@ -24,6 +24,10 @@
# SOFTWARE.
import os
import sys
import typing
from .core import base
from . import config
class ConfigError(Exception):
@ -50,6 +54,8 @@ class Config:
"bring_front_click",
"wmname",
]
keys: typing.List[config.Key]
mouse: typing.List[config.Mouse]
def __init__(self, **settings):
"""Create a Config() object from settings
@ -85,7 +91,7 @@ class Config:
pass
@classmethod
def from_file(cls, path):
def from_file(cls, kore: base.Core, path: str):
"Create a Config() object from the python file located at path."
try:
sys.path.insert(0, os.path.dirname(path))
@ -96,4 +102,23 @@ class Config:
logger.exception('Could not import config file %r', path)
tb = traceback.format_exc()
raise ConfigError(tb)
return cls(**vars(config))
cnf = cls(**vars(config))
cnf.validate(kore)
return cnf
def validate(self, kore: base.Core) -> None:
"""
Validate the configuration against the core.
"""
valid_keys = kore.get_keys()
valid_mods = kore.get_modifiers()
for k in self.keys:
if k.key not in valid_keys:
raise ConfigError("No such key: %s" % k.key)
for m in k.modifiers:
if m not in valid_mods:
raise ConfigError("No such modifier: %s" % m)
for ms in self.mouse:
for m in ms.modifiers:
if m not in valid_mods:
raise ConfigError("No such modifier: %s" % m)

View File

@ -67,8 +67,15 @@ def _import_module(module_name, dir_path):
class Qtile(command.CommandObject):
"""This object is the `root` of the command graph"""
def __init__(self, config, display_name=None, fname=None, no_spawn=False, state=None):
def __init__(
self,
kore,
config,
display_name=None,
fname=None,
no_spawn=False,
state=None
):
self._restart = False
self.no_spawn = no_spawn
@ -1408,8 +1415,10 @@ class Qtile(command.CommandObject):
keysym = xcbq.keysyms.get(key)
except xcbq.XCBQError as e:
raise command.CommandError(str(e))
class DummyEv:
pass
d = DummyEv()
d.detail = self.conn.first_sym_to_code[keysym]
d.state = modmasks

View File

@ -1005,4 +1005,3 @@ def translate_masks(modifiers: typing.List[str]) -> int:
return functools.reduce(operator.or_, masks)
else:
return 0

View File

@ -1,13 +1,13 @@
import typing
from . import types
from . import base
from . import xcbq
class XCore(types.Core):
class XCore(base.Core):
def get_keys(self) -> typing.List[str]:
return xcbq.keysyms.keys()
return list(xcbq.keysyms.keys())
def get_modifiers(self) -> typing.List[str]:
return xcbq.ModMasks.keys()
return list(xcbq.ModMasks.keys())

View File

@ -27,6 +27,7 @@ from os import path, getenv
from libqtile.log_utils import init_log, logger
from libqtile import confreader
from libqtile.core import xcore
locale.setlocale(locale.LC_ALL, locale.getdefaultlocale()) # type: ignore
@ -103,8 +104,9 @@ def make_qtile():
log_level = getattr(logging, options.log_level)
init_log(log_level=log_level)
kore = xcore.XCore()
try:
config = confreader.Config.from_file(options.configfile)
config = confreader.Config.from_file(kore, options.configfile)
except Exception as e:
logger.exception('Error while reading config file (%s)', e)
config = confreader.Config()
@ -115,6 +117,7 @@ def make_qtile():
# before start importing stuff
from libqtile.core import manager
return manager.Qtile(
kore,
config,
fname=options.socket,
no_spawn=options.no_spawn,

View File

@ -23,6 +23,7 @@
import libqtile
import libqtile.ipc
from libqtile.core.manager import Qtile as QtileManager
from libqtile.core import xcore
from libqtile.log_utils import init_log
import functools
@ -257,9 +258,10 @@ class Qtile:
def run_qtile():
llvl = logging.DEBUG if pytest.config.getoption("--debuglog") else logging.INFO
kore = xcore.XCore()
try:
init_log(llvl, log_path=None, log_color=False)
q = QtileManager(config_class(), self.display, self.sockfile)
q = QtileManager(kore, config_class(), self.display, self.sockfile)
q.loop()
except Exception:
wpipe.send(traceback.format_exc())
@ -285,7 +287,8 @@ class Qtile:
"""
llvl = logging.DEBUG if pytest.config.getoption("--debuglog") else logging.INFO
init_log(llvl, log_path=None, log_color=False)
return QtileManager(config_class(), self.display, self.sockfile)
kore = xcore.XCore()
return QtileManager(kore, config_class(), self.display, self.sockfile)
def terminate(self):
if self.proc is None:

View File

@ -9,4 +9,3 @@ def test_keys(qtile):
xc = xcore.XCore()
assert "a" in xc.get_keys()
assert "shift" in xc.get_modifiers()

View File

@ -25,22 +25,42 @@ import pytest
from libqtile import confreader
from libqtile import config, utils
from libqtile.core import xcore
tests_dir = os.path.dirname(os.path.realpath(__file__))
def test_syntaxerr():
def test_validate():
xc = xcore.XCore()
f = confreader.Config.from_file(xc, os.path.join(tests_dir, "configs", "basic.py"))
f.validate(xc)
f.keys[0].key = "nonexistent"
with pytest.raises(confreader.ConfigError):
confreader.Config.from_file(os.path.join(tests_dir, "configs", "syntaxerr.py"))
f.validate(xc)
f.keys[0].key = "x"
f = confreader.Config.from_file(xc, os.path.join(tests_dir, "configs", "basic.py"))
f.keys[0].modifiers = ["nonexistent"]
with pytest.raises(confreader.ConfigError):
f.validate(xc)
f.keys[0].modifiers = ["shift"]
def test_syntaxerr():
xc = xcore.XCore()
with pytest.raises(confreader.ConfigError):
confreader.Config.from_file(xc, os.path.join(tests_dir, "configs", "syntaxerr.py"))
def test_basic():
f = confreader.Config.from_file(os.path.join(tests_dir, "configs", "basic.py"))
xc = xcore.XCore()
f = confreader.Config.from_file(xc, os.path.join(tests_dir, "configs", "basic.py"))
assert f.keys
def test_falls_back():
f = confreader.Config.from_file(os.path.join(tests_dir, "configs", "basic.py"))
xc = xcore.XCore()
f = confreader.Config.from_file(xc, os.path.join(tests_dir, "configs", "basic.py"))
# We just care that it has a default, we don't actually care what the
# default is; don't assert anything at all about the default in case

View File

@ -65,4 +65,3 @@ def test_masks():
def test_translate_masks():
assert xcbq.translate_masks(["shift", "control"])
assert xcbq.translate_masks([]) == 0