minikconf: add mypy annotations
Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com> Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
Paolo Bonzini committed
Apr 24, 2026 at 11:37 UTC
7b4680b03f874357132b2dceae08041316eaa8b7
2 files changed
+122
-101
python/tests/linters.py
+3
@@ -65,6 +65,9 @@ class TestLinters:
65
]
66
)
67
68
+ def test_mypy_minikconf(self):
69
+ check_call([sys.executable, "-m", "mypy", "../scripts/minikconf.py"])
70
+
71
def test_mypy_pkg(self):
72
check_call([sys.executable, "-m", "mypy", "-p", "qemu"])
73
scripts/minikconf.py
+119
-101
@@ -17,31 +17,34 @@ import os
17
import random
18
import re
19
import sys
20
+import typing as T
21
from dataclasses import dataclass
22
23
__all__ = [ 'KconfigDataError', 'KconfigParserError',
24
'KconfigData', 'KconfigParser' ,
25
'defconfig', 'allyesconfig', 'allnoconfig', 'randconfig' ]
26
27
+Mangler = T.Callable[[bool], bool]
28
+
29
@dataclass
30
class IncludeInfo:
31
file: str
32
line: int
33
parent: IncludeInfo | None
34
32
- def __iter__(self):
33
- inf = self
35
+ def __iter__(self) -> T.Iterator[str]:
36
+ inf: IncludeInfo | None = self
37
while inf is not None:
38
yield "%s:%d" % (inf.file, inf.line)
39
inf = inf.parent
40
38
- def error_path(self):
41
+ def error_path(self) -> str:
42
res = ""
43
for loc in self:
44
res = "In file included from %s:\n" % loc + res
45
return res
46
44
-def debug_print(*args):
47
+def debug_print(*args: object) -> None:
48
#print('# ' + (' '.join(str(x) for x in args)))
49
pass
50
@@ -56,81 +59,81 @@ def debug_print(*args):
59
# -------------------------------------------
60
61
class KconfigDataError(Exception):
59
- def __init__(self, msg):
62
+ def __init__(self, msg: str) -> None:
63
self.msg = msg
64
62
- def __str__(self):
65
+ def __str__(self) -> str:
66
return self.msg
67
65
-allyesconfig = lambda x: True
66
-allnoconfig = lambda x: False
67
-defconfig = lambda x: x
68
-randconfig = lambda x: random.randint(0, 1) == 1
68
+allyesconfig: Mangler = lambda x: True
69
+allnoconfig: Mangler = lambda x: False
70
+defconfig: Mangler = lambda x: x
71
+randconfig: Mangler = lambda x: random.randint(0, 1) == 1
72
73
class KconfigData:
74
class Expr:
72
- def __and__(self, rhs):
75
+ def __and__(self, rhs: KconfigData.Expr) -> KconfigData.Expr:
76
return KconfigData.AND(self, rhs)
74
- def __or__(self, rhs):
77
+ def __or__(self, rhs: KconfigData.Expr) -> KconfigData.Expr:
78
return KconfigData.OR(self, rhs)
76
- def __invert__(self):
79
+ def __invert__(self) -> KconfigData.Expr:
80
return KconfigData.NOT(self)
81
82
# Abstract methods
80
- def add_edges_to(self, var):
83
+ def add_edges_to(self, var: KconfigData.Var) -> None:
84
pass
82
- def evaluate(self):
85
+ def evaluate(self) -> bool:
86
assert False
87
88
class AND(Expr):
86
- def __init__(self, lhs, rhs):
89
+ def __init__(self, lhs: KconfigData.Expr, rhs: KconfigData.Expr) -> None:
90
self.lhs = lhs
91
self.rhs = rhs
89
- def __str__(self):
92
+ def __str__(self) -> str:
93
return "(%s && %s)" % (self.lhs, self.rhs)
94
92
- def add_edges_to(self, var):
95
+ def add_edges_to(self, var: KconfigData.Var) -> None:
96
self.lhs.add_edges_to(var)
97
self.rhs.add_edges_to(var)
95
- def evaluate(self):
98
+ def evaluate(self) -> bool:
99
return self.lhs.evaluate() and self.rhs.evaluate()
100
101
class OR(Expr):
99
- def __init__(self, lhs, rhs):
102
+ def __init__(self, lhs: KconfigData.Expr, rhs: KconfigData.Expr) -> None:
103
self.lhs = lhs
104
self.rhs = rhs
102
- def __str__(self):
105
+ def __str__(self) -> str:
106
return "(%s || %s)" % (self.lhs, self.rhs)
107
105
- def add_edges_to(self, var):
108
+ def add_edges_to(self, var: KconfigData.Var) -> None:
109
self.lhs.add_edges_to(var)
110
self.rhs.add_edges_to(var)
108
- def evaluate(self):
111
+ def evaluate(self) -> bool:
112
return self.lhs.evaluate() or self.rhs.evaluate()
113
114
class NOT(Expr):
112
- def __init__(self, lhs):
115
+ def __init__(self, lhs: KconfigData.Expr) -> None:
116
self.lhs = lhs
114
- def __str__(self):
117
+ def __str__(self) -> str:
118
return "!%s" % (self.lhs)
119
117
- def add_edges_to(self, var):
120
+ def add_edges_to(self, var: KconfigData.Var) -> None:
121
self.lhs.add_edges_to(var)
119
- def evaluate(self):
122
+ def evaluate(self) -> bool:
123
return not self.lhs.evaluate()
124
125
class Var(Expr):
123
- def __init__(self, name):
126
+ def __init__(self, name: str) -> None:
127
self.name = name
125
- self.value = None
126
- self.outgoing = set()
127
- self.clauses_for_var = list()
128
- def __str__(self):
128
+ self.value: bool | None = None
129
+ self.outgoing: set[KconfigData.Var] = set()
130
+ self.clauses_for_var: list[KconfigData.Clause] = []
131
+ def __str__(self) -> str:
132
return self.name
133
131
- def has_value(self):
134
+ def has_value(self) -> bool:
135
return self.value is not None
133
- def set_value(self, val, clause):
136
+ def set_value(self, val: bool, clause: KconfigData.Clause) -> None:
137
self.clauses_for_var.append(clause)
138
if self.has_value() and self.value != val:
139
print("The following clauses were found for " + self.name, file=sys.stderr)
@@ -141,7 +144,8 @@ class KconfigData:
144
self.value = val
145
146
# depth first search of the dependency graph
144
- def dfs(self, visited, f):
147
+ def dfs(self, visited: set[KconfigData.Var],
148
+ f: T.Callable[[KconfigData.Var], None]) -> None:
149
if self in visited:
150
return
151
visited.add(self)
@@ -149,87 +153,89 @@ class KconfigData:
153
v.dfs(visited, f)
154
f(self)
155
152
- def add_edges_to(self, var):
156
+ def add_edges_to(self, var: KconfigData.Var) -> None:
157
self.outgoing.add(var)
154
- def evaluate(self):
158
+ def evaluate(self) -> bool:
159
if not self.has_value():
160
raise KconfigDataError('cycle found including %s' % self)
161
+ assert self.value is not None
162
return self.value
163
164
class Clause:
160
- def __init__(self, dest):
165
+ def __init__(self, dest: KconfigData.Var) -> None:
166
self.dest = dest
162
- def priority(self):
167
+ def priority(self) -> int:
168
return 0
164
- def process(self):
169
+ def process(self) -> None:
170
pass
171
172
class AssignmentClause(Clause):
168
- def __init__(self, dest, value):
173
+ def __init__(self, dest: KconfigData.Var, value: bool) -> None:
174
KconfigData.Clause.__init__(self, dest)
175
self.value = value
171
- def __str__(self):
176
+ def __str__(self) -> str:
177
return "CONFIG_%s=%s" % (self.dest, 'y' if self.value else 'n')
178
174
- def process(self):
179
+ def process(self) -> None:
180
self.dest.set_value(self.value, self)
181
182
class DefaultClause(Clause):
178
- def __init__(self, dest, value, cond=None):
183
+ def __init__(self, dest: KconfigData.Var, value: bool,
184
+ cond: KconfigData.Expr | None = None) -> None:
185
KconfigData.Clause.__init__(self, dest)
186
self.value = value
187
self.cond = cond
188
if self.cond is not None:
189
self.cond.add_edges_to(self.dest)
184
- def __str__(self):
190
+ def __str__(self) -> str:
191
value = 'y' if self.value else 'n'
192
if self.cond is None:
193
return "config %s default %s" % (self.dest, value)
194
else:
195
return "config %s default %s if %s" % (self.dest, value, self.cond)
196
191
- def priority(self):
197
+ def priority(self) -> int:
198
# Defaults are processed just before leaving the variable
199
return -1
194
- def process(self):
200
+ def process(self) -> None:
201
if not self.dest.has_value() and \
202
(self.cond is None or self.cond.evaluate()):
203
self.dest.set_value(self.value, self)
204
205
class DependsOnClause(Clause):
200
- def __init__(self, dest, expr):
206
+ def __init__(self, dest: KconfigData.Var, expr: KconfigData.Expr) -> None:
207
KconfigData.Clause.__init__(self, dest)
208
self.expr = expr
209
self.expr.add_edges_to(self.dest)
204
- def __str__(self):
210
+ def __str__(self) -> str:
211
return "config %s depends on %s" % (self.dest, self.expr)
212
207
- def process(self):
213
+ def process(self) -> None:
214
if not self.expr.evaluate():
215
self.dest.set_value(False, self)
216
217
class SelectClause(Clause):
212
- def __init__(self, dest, cond):
218
+ def __init__(self, dest: KconfigData.Var, cond: KconfigData.Expr) -> None:
219
KconfigData.Clause.__init__(self, dest)
220
self.cond = cond
221
self.cond.add_edges_to(self.dest)
216
- def __str__(self):
222
+ def __str__(self) -> str:
223
return "select %s if %s" % (self.dest, self.cond)
224
219
- def process(self):
225
+ def process(self) -> None:
226
if self.cond.evaluate():
227
self.dest.set_value(True, self)
228
223
- def __init__(self, value_mangler=defconfig):
229
+ def __init__(self, value_mangler: Mangler = defconfig) -> None:
230
self.value_mangler = value_mangler
225
- self.previously_included = []
226
- self.defined_vars = set()
227
- self.referenced_vars = dict()
228
- self.clauses = list()
231
+ self.previously_included: list[str] = []
232
+ self.defined_vars: set[str] = set()
233
+ self.referenced_vars: dict[str, KconfigData.Var] = {}
234
+ self.clauses: list[KconfigData.Clause] = []
235
236
# semantic analysis -------------
237
232
- def check_undefined(self):
238
+ def check_undefined(self) -> bool:
239
undef = False
240
for i in self.referenced_vars:
241
if i not in self.defined_vars:
@@ -237,7 +243,7 @@ class KconfigData:
243
undef = True
244
return undef
245
240
- def compute_config(self):
246
+ def compute_config(self) -> dict[str, bool]:
247
if self.check_undefined():
248
raise KconfigDataError("there were undefined symbols")
249
@@ -250,10 +256,10 @@ class KconfigData:
256
debug_print(source, "->", [str(x) for x in edges.outgoing])
257
258
# The reverse of the depth-first order is the topological sort
253
- dfo = dict()
254
- visited = set()
259
+ dfo: dict[KconfigData.Var, int] = {}
260
+ visited: set[KconfigData.Var] = set()
261
debug_print("\n")
256
- def visit_fn(var):
262
+ def visit_fn(var: KconfigData.Var) -> None:
263
debug_print(var, "has DFS number", len(dfo))
264
dfo[var] = len(dfo)
265
@@ -272,7 +278,7 @@ class KconfigData:
278
clause.process()
279
280
debug_print("")
275
- values = dict()
281
+ values: dict[str, bool] = {}
282
for name, v in self.referenced_vars.items():
283
debug_print("Evaluating", name)
284
values[name] = v.evaluate()
@@ -281,39 +287,42 @@ class KconfigData:
287
288
# semantic actions -------------
289
284
- def do_declaration(self, var):
290
+ def do_declaration(self, var: KconfigData.Var) -> None:
291
if var.name in self.defined_vars:
292
raise KconfigDataError('variable "%s" defined twice' % var.name)
287
-
293
self.defined_vars.add(var.name)
294
295
# var is a string with the variable's name.
291
- def do_var(self, var):
296
+ def do_var(self, var: str) -> KconfigData.Var:
297
if var in self.referenced_vars:
298
return self.referenced_vars[var]
299
300
var_obj = self.referenced_vars[var] = KconfigData.Var(var)
301
return var_obj
302
298
- def do_assignment(self, var, val):
303
+ def do_assignment(self, var: KconfigData.Var, val: bool) -> None:
304
self.clauses.append(KconfigData.AssignmentClause(var, val))
305
301
- def do_cmdline_assignment(self, var, val):
306
+ def do_cmdline_assignment(self, var: str, val: bool) -> None:
307
assert var.startswith("CONFIG_")
308
self.do_assignment(self.do_var(var[7:]), val)
309
305
- def do_default(self, var, val, cond=None):
310
+ def do_default(self, var: KconfigData.Var, val: bool,
311
+ cond: KconfigData.Expr | None = None) -> None:
312
val = self.value_mangler(val)
313
self.clauses.append(KconfigData.DefaultClause(var, val, cond))
314
309
- def do_depends_on(self, var, expr):
315
+ def do_depends_on(self, var: KconfigData.Var,
316
+ expr: KconfigData.Expr) -> None:
317
self.clauses.append(KconfigData.DependsOnClause(var, expr))
318
312
- def do_select(self, var, symbol, cond=None):
319
+ def do_select(self, var: KconfigData.Var, symbol: KconfigData.Var,
320
+ cond: KconfigData.Expr | None = None) -> None:
321
cond = (cond & var) if cond is not None else var
322
self.clauses.append(KconfigData.SelectClause(symbol, cond))
323
316
- def do_imply(self, var, symbol, cond=None):
324
+ def do_imply(self, var: KconfigData.Var, symbol: KconfigData.Var,
325
+ cond: KconfigData.Expr | None = None) -> None:
326
# "config X imply Y [if COND]" is the same as
327
# "config Y default y if X [&& COND]"
328
cond = (cond & var) if cond is not None else var
@@ -325,7 +334,7 @@ class KconfigData:
334
# -------------------------------------------
335
336
# tokens table
328
-TOKENS = {}
337
+TOKENS: dict[int, str] = {}
338
TOK_NONE = -1
339
TOK_LPAREN = 0; TOKENS[TOK_LPAREN] = '"("'
340
TOK_RPAREN = 1; TOKENS[TOK_RPAREN] = '")"'
@@ -348,7 +357,8 @@ TOK_ID = 17; TOKENS[TOK_ID] = 'identifier'
357
TOK_EOF = 18; TOKENS[TOK_EOF] = 'end of file'
358
359
class KconfigParserError(Exception):
351
- def __init__(self, parser, msg, tok=None):
360
+ def __init__(self, parser: KconfigParser, msg: str,
361
+ tok: int | str | None = None) -> None:
362
self.loc = parser.location()
363
tok = tok if tok is not None else parser.tok
364
if tok != TOK_NONE:
@@ -356,33 +366,37 @@ class KconfigParserError(Exception):
366
msg = '%s before %s' % (msg, location)
367
self.msg = msg
368
359
- def __str__(self):
369
+ def __str__(self) -> str:
370
return "%s: %s" % (self.loc, self.msg)
371
372
class KconfigParser:
373
374
@classmethod
365
- def parse(cls, fp, data, incl_info=None):
375
+ def parse(cls, fp: T.TextIO, data: KconfigData, incl_info: IncludeInfo | None = None) -> None:
376
cls(fp, data, incl_info).parse_config()
377
368
- def __init__(self, fp, data, incl_info):
378
+ def __init__(self, fp: T.TextIO, data: KconfigData, incl_info: IncludeInfo | None = None):
379
self.data = data
380
self.incl_info = incl_info
381
self.abs_fname = os.path.abspath(fp.name)
382
self.fname = fp.name
383
self.data.previously_included.append(self.abs_fname)
384
+
385
src = fp.read()
386
if src == '' or src[-1] != '\n':
387
src += '\n'
388
self.src = src
378
- self.cursor = 0
379
- self.line = 1
380
- self.line_pos = 0
389
+ self.cursor: int = 0
390
+ self.line: int = 1
391
+ self.line_pos: int = 0
392
+ self.pos: int = 0
393
+ self.tok: int = TOK_NONE
394
+ self.val: str | None = None
395
self.get_token()
396
397
# file management -----
398
385
- def location(self):
399
+ def location(self) -> str:
400
col = 1
401
for ch in self.src[self.line_pos:self.pos]:
402
if ch == '\t':
@@ -393,7 +407,7 @@ class KconfigParser:
407
incl_chain = inf.error_path() if inf is not None else ""
408
return '%s%s:%d:%d' % (incl_chain, self.fname, self.line, col)
409
396
- def do_include(self, include):
410
+ def do_include(self, include: str) -> None:
411
incl_abs_fname = os.path.join(os.path.dirname(self.abs_fname),
412
include)
413
# catch inclusion cycle
@@ -421,7 +435,7 @@ class KconfigParser:
435
# recursive descent parser -----
436
437
# y_or_n: Y | N
424
- def parse_y_or_n(self):
438
+ def parse_y_or_n(self) -> bool:
439
if self.tok == TOK_Y:
440
self.get_token()
441
return True
@@ -431,7 +445,7 @@ class KconfigParser:
445
raise KconfigParserError(self, 'Expected "y" or "n"')
446
447
# var: ID
434
- def parse_var(self):
448
+ def parse_var(self) -> KconfigData.Var:
449
if self.tok != TOK_ID:
450
raise KconfigParserError(self, 'Expected identifier')
451
val = self.val
@@ -440,7 +454,7 @@ class KconfigParser:
454
return self.data.do_var(val)
455
456
# assignment_var: ID (starting with "CONFIG_")
443
- def parse_assignment_var(self):
457
+ def parse_assignment_var(self) -> KconfigData.Var:
458
if self.tok != TOK_ID:
459
raise KconfigParserError(self, 'Expected identifier')
460
val = self.val
@@ -452,7 +466,7 @@ class KconfigParser:
466
return self.data.do_var(val[7:])
467
468
# assignment: var EQUAL y_or_n
455
- def parse_assignment(self):
469
+ def parse_assignment(self) -> None:
470
var = self.parse_assignment_var()
471
if self.tok != TOK_EQUAL:
472
raise KconfigParserError(self, 'Expected "="')
@@ -462,7 +476,7 @@ class KconfigParser:
476
# primary: NOT primary
477
# | LPAREN expr RPAREN
478
# | var
465
- def parse_primary(self):
479
+ def parse_primary(self) -> KconfigData.Expr:
480
if self.tok == TOK_NOT:
481
self.get_token()
482
val = ~self.parse_primary()
@@ -479,7 +493,7 @@ class KconfigParser:
493
return val
494
495
# disj: primary (OR primary)*
482
- def parse_disj(self):
496
+ def parse_disj(self) -> KconfigData.Expr:
497
lhs = self.parse_primary()
498
while self.tok == TOK_OR:
499
self.get_token()
@@ -487,7 +501,7 @@ class KconfigParser:
501
return lhs
502
503
# expr: disj (AND disj)*
490
- def parse_expr(self):
504
+ def parse_expr(self) -> KconfigData.Expr:
505
lhs = self.parse_disj()
506
while self.tok == TOK_AND:
507
self.get_token()
@@ -496,7 +510,7 @@ class KconfigParser:
510
511
# condition: IF expr
512
# | empty
499
- def parse_condition(self):
513
+ def parse_condition(self) -> KconfigData.Expr | None:
514
if self.tok != TOK_IF:
515
return None
516
self.get_token()
@@ -506,7 +520,7 @@ class KconfigParser:
520
# | DEPENDS ON expr
521
# | SELECT var condition
522
# | BOOL
509
- def parse_property(self, var):
523
+ def parse_property(self, var: KconfigData.Var) -> None:
524
if self.tok == TOK_DEFAULT:
525
self.get_token()
526
val = self.parse_y_or_n()
@@ -535,7 +549,7 @@ class KconfigParser:
549
550
# properties: properties property
551
# | /* empty */
538
- def parse_properties(self, var):
552
+ def parse_properties(self, var: KconfigData.Var) -> None:
553
while self.tok == TOK_DEFAULT or self.tok == TOK_DEPENDS or \
554
self.tok == TOK_SELECT or self.tok == TOK_BOOL or \
555
self.tok == TOK_IMPLY:
@@ -548,7 +562,7 @@ class KconfigParser:
562
+ '"default", "depends on", "imply" or "select"')
563
564
# declaration: config var properties
551
- def parse_declaration(self):
565
+ def parse_declaration(self) -> None:
566
if self.tok == TOK_CONFIG:
567
self.get_token()
568
var = self.parse_var()
@@ -560,9 +574,10 @@ class KconfigParser:
574
# clause: SOURCE
575
# | declaration
576
# | assignment
563
- def parse_clause(self):
577
+ def parse_clause(self) -> None:
578
if self.tok == TOK_SOURCE:
579
val = self.val
580
+ assert val is not None
581
self.get_token()
582
self.do_include(val)
583
elif self.tok == TOK_CONFIG:
@@ -573,14 +588,15 @@ class KconfigParser:
588
raise KconfigParserError(self, 'expected "source", "config" or identifier')
589
590
# config: clause+ EOF
576
- def parse_config(self):
591
+ def parse_config(self) -> KconfigData:
592
while self.tok != TOK_EOF:
593
self.parse_clause()
594
return self.data
595
596
# scanner -----
597
583
- def get_token(self):
598
+ def get_token(self) -> None:
599
+ assert self.src is not None
600
while True:
601
ch = self.src[self.cursor]
602
self.pos = self.cursor
@@ -592,7 +608,8 @@ class KconfigParser:
608
self.tok = tok
609
return
610
595
- def check_keyword(self, rest):
611
+ def check_keyword(self, rest: str) -> bool:
612
+ assert self.src is not None
613
if not self.src.startswith(rest, self.cursor):
614
return False
615
length = len(rest)
@@ -601,7 +618,8 @@ class KconfigParser:
618
self.cursor += length
619
return True
620
604
- def scan_token(self, ch):
621
+ def scan_token(self, ch: str) -> int | None:
622
+ assert self.src is not None
623
if ch == '#':
624
self.cursor = self.src.find('\n', self.cursor)
625
return None
@@ -669,7 +687,7 @@ class KconfigParser:
687
688
def main() -> None:
689
argv = sys.argv
672
- mode = defconfig
690
+ mode: Mangler = defconfig
691
if len(sys.argv) > 1:
692
if argv[1] == '--defconfig':
693
del argv[1]
@@ -693,7 +711,7 @@ def main() -> None:
711
sys.exit(1)
712
713
data = KconfigData(mode)
696
- external_vars = set()
714
+ external_vars: set[str] = set()
715
for arg in argv[3:]:
716
m = re.match(r'^(CONFIG_[A-Z0-9_]+)=([yn]?)$', arg)
717
if m is not None: