1# Copyright (c) 2020, Tycho Andersen. All rights reserved. 2# 3# Permission is hereby granted, free of charge, to any person obtaining a copy 4# of this software and associated documentation files (the "Software"), to deal 5# in the Software without restriction, including without limitation the rights 6# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7# copies of the Software, and to permit persons to whom the Software is 8# furnished to do so, subject to the following conditions: 9# 10# The above copyright notice and this permission notice shall be included in 11# all copies or substantial portions of the Software. 12# 13# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19# SOFTWARE. 20 21# Set the locale before any widgets or anything are imported, so any widget 22# whose defaults depend on a reasonable locale sees something reasonable. 23import shutil 24import subprocess 25import sys 26import tempfile 27from os import environ, getenv, path 28 29from libqtile import confreader 30 31 32def type_check_config_vars(tempdir, config_name): 33 if shutil.which("stubtest") is None: 34 print("stubtest not found, can't type check config file\n" 35 "install it and try again") 36 return 37 38 # write a .pyi file to tempdir: 39 f = open(path.join(tempdir, config_name+".pyi"), "w") 40 f.write(confreader.config_pyi_header) 41 for name, type_ in confreader.Config.__annotations__.items(): 42 f.write(name) 43 f.write(": ") 44 f.write(type_) 45 f.write("\n") 46 f.close() 47 48 # need to tell python to look in pwd for modules 49 newenv = environ.copy() 50 newenv["PYTHONPATH"] = newenv.get("PYTHONPATH", "") + ":" 51 52 p = subprocess.Popen( 53 ["stubtest", "--concise", config_name], 54 stdout=subprocess.PIPE, 55 stderr=subprocess.PIPE, 56 cwd=tempdir, 57 text=True, 58 env=newenv, 59 ) 60 stdout, stderr = p.communicate() 61 missing_vars = [] 62 for line in (stdout+stderr).split("\n"): 63 # filter out stuff that users didn't specify; they'll be imported from 64 # the default config 65 if "is not present at runtime" in line: 66 missing_vars.append(line.split()[0]) 67 68 # write missing vars to a tempfile 69 whitelist = open(path.join(tempdir, "stubtest_whitelist"), "w") 70 for var in missing_vars: 71 whitelist.write(var) 72 whitelist.write("\n") 73 whitelist.close() 74 75 p = subprocess.Popen([ 76 "stubtest", 77 # ignore variables that the user creates in their config that 78 # aren't in our default config list 79 "--ignore-missing-stub", 80 # use our whitelist to ignore stuff users didn't specify 81 "--whitelist", whitelist.name, 82 config_name, 83 ], 84 cwd=tempdir, 85 text=True, 86 env=newenv, 87 ) 88 p.wait() 89 if p.returncode != 0: 90 sys.exit(1) 91 92 93def type_check_config_args(config_file): 94 if shutil.which("mypy") is None: 95 print("mypy not found, can't type check config file" 96 "install it and try again") 97 return 98 try: 99 # we want to use Literal, which is in 3.8. If people have a mypy that 100 # is too old, they can upgrade; this is an optional check anyways. 101 subprocess.check_call(["mypy", "--python-version=3.8", config_file]) 102 print("config file type checking succeeded") 103 except subprocess.CalledProcessError as e: 104 print("config file type checking failed: {}".format(e)) 105 sys.exit(1) 106 107 108def check_config(args): 109 print("checking qtile config file {}".format(args.configfile)) 110 111 # need to do all the checking in a tempdir because we need to write stuff 112 # for stubtest 113 with tempfile.TemporaryDirectory() as tempdir: 114 shutil.copytree(path.dirname(args.configfile), tempdir, dirs_exist_ok=True) 115 tmp_path = path.join(tempdir, path.basename(args.configfile)) 116 117 # are the top level config variables the right type? 118 module_name = path.splitext(path.basename(args.configfile))[0] 119 type_check_config_vars(tempdir, module_name) 120 121 # are arguments passed to qtile APIs correct? 122 type_check_config_args(tmp_path) 123 124 # can we load the config? 125 config = confreader.Config(args.configfile) 126 config.load() 127 config.validate() 128 print("config file can be loaded by qtile") 129 130 131def add_subcommand(subparsers, parents): 132 parser = subparsers.add_parser( 133 "check", 134 parents=parents, 135 help="Check a configuration file for errors" 136 ) 137 parser.add_argument( 138 "-c", "--config", 139 action="store", 140 default=path.expanduser(path.join( 141 getenv('XDG_CONFIG_HOME', '~/.config'), 'qtile', 'config.py')), 142 dest="configfile", 143 help='Use the specified configuration file', 144 ) 145 parser.set_defaults(func=check_config) 146