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
parent
ab3dc678af
commit
13d9c4e80b
|
@ -41,3 +41,4 @@ debian/*debhelper
|
|||
.mypy_cache/
|
||||
.tags
|
||||
venv
|
||||
.coverage.*
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1005,4 +1005,3 @@ def translate_masks(modifiers: typing.List[str]) -> int:
|
|||
return functools.reduce(operator.or_, masks)
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -9,4 +9,3 @@ def test_keys(qtile):
|
|||
xc = xcore.XCore()
|
||||
assert "a" in xc.get_keys()
|
||||
assert "shift" in xc.get_modifiers()
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -65,4 +65,3 @@ def test_masks():
|
|||
def test_translate_masks():
|
||||
assert xcbq.translate_masks(["shift", "control"])
|
||||
assert xcbq.translate_masks([]) == 0
|
||||
|
||||
|
|
Loading…
Reference in New Issue