3 from __future__ import annotations
6 from dataclasses import astuple, dataclass
7 from pathlib import Path
9 from typing import Callable, Generic, Sequence, TypeVar
15 @dataclass(order=True)
17 """A semantic version number: MAJOR.MINOR.PATCH."""
19 UNKNOWN_VERSION = "ALWAYS"
20 DEFAULT_VERSION = "1.0.0"
27 return ".".join(map(str, astuple(self)))
30 def parse(cls, raw_version: str) -> Version:
31 if raw_version == cls.UNKNOWN_VERSION:
32 raw_version = cls.DEFAULT_VERSION
33 version = raw_version.split(".")
34 assert 2 <= len(version) <= 3 and all(
35 n.isdecimal() for n in version
37 return Version(*map(int, version))
42 """An example in a message entry."""
50 def parse(cls, entry) -> Example:
51 name = entry.get("name")
54 description = entry.get("description")
57 before = entry.get("before")
58 after = entry.get("after")
59 # Either none or both of them
60 assert not (bool(before) ^ bool(after))
62 return Example(name=name, description=description, before=before, after=after)
67 """An xkbcommon message entry in the message registry"""
69 VALID_TYPES = ("warning", "error")
72 """A unique strictly positive integer identifier"""
74 """A unique short human-readable string identifier"""
76 """Log level of the message"""
78 """A short description of the meaning of the message"""
80 """A long description of the meaning of the message"""
82 """Version of xkbcommon the message has been added"""
83 removed: Version | None
84 """Version of xkbcommon the message has been removed"""
85 examples: tuple[Example, ...]
87 Optional examples of situations in which the message occurs.
88 If the message is an error or a warning, also provide hints on how to fix it.
92 def parse(cls, entry) -> Entry:
93 code = entry.get("code")
94 assert code is not None and isinstance(code, int) and code > 0, entry
97 assert id is not None, entry
99 type_ = entry.get("type")
100 assert type_ in cls.VALID_TYPES, entry
102 description = entry.get("description")
103 assert description is not None, entry
105 details = entry.get("details", "")
107 raw_added = entry.get("added", "")
108 assert raw_added, entry
110 added = Version.parse(raw_added)
113 if removed := entry.get("removed"):
114 removed = Version.parse(removed)
115 assert added < removed, entry
117 if examples := entry.get("examples", ()):
118 examples = tuple(map(Example.parse, examples))
124 description=description,
132 def message_code(self) -> str:
133 """Format the message code for display"""
134 return f"XKB-{self.code:0>3}"
137 def message_code_constant(self: Entry) -> str:
138 """Returns the C enumeration member denoting the message code"""
139 id = self.id.replace("-", "_").upper()
140 return f"XKB_{self.type.upper()}_{id}"
143 def message_name(self: Entry):
144 """Format the message string identifier for display"""
145 return self.id.replace("-", " ").capitalize()
148 def prepend_todo(text: str) -> str:
149 if text.startswith("TODO"):
150 return f"""<span class="todo">{text[:5]}</span>{text[5:]}"""
155 def load_message_registry(
156 env: jinja2.Environment, constants: dict[str, int], path: Path
157 ) -> Sequence[Entry]:
158 # Load the message registry YAML file as a Jinja2 template
159 registry_template = env.get_template(str(path))
161 # Load message registry
162 message_registry = sorted(
163 map(Entry.parse, yaml.safe_load(registry_template.render(constants))),
164 key=lambda e: e.code,
167 # Check message codes and identifiers are unique
168 codes: set[int] = set()
169 identifiers: set[str] = set()
170 for n, entry in enumerate(message_registry):
171 if entry.code in codes:
172 raise ValueError("Duplicated code in entry #{n}: {entry.code}")
173 if entry.id in identifiers:
174 raise ValueError("Duplicated identifier in entry #{n}: {entry.id}")
175 codes.add(entry.code)
176 identifiers.add(entry.id)
178 return message_registry
182 registry: Sequence[Entry],
183 env: jinja2.Environment,
186 skip_removed: bool = False,
188 """Generate a file from its Jinja2 template and the message registry"""
189 template_path = file.with_suffix(f"{file.suffix}.jinja")
190 template = env.get_template(str(template_path))
192 script = Path(__file__).name
193 with path.open("wt", encoding="utf-8") as fd:
195 tuple(filter(lambda e: e.removed is None, registry))
199 fd.writelines(template.generate(entries=entries, script=script))
206 class Constant(Generic[T]):
209 conversion: Callable[[str], T]
212 def read_constants(path: Path, patterns: Sequence[Constant[T]]) -> dict[str, T]:
213 constants: dict[str, T] = {}
214 patternsʹ = list(patterns)
215 with path.open("rt", encoding="utf-8") as fd:
217 for k, constant in enumerate(patternsʹ):
218 if m := constant.pattern.match(line):
219 constants[constant.name] = constant.conversion(m.group(1))
221 continue # Expect only one match per line
223 # No more pattern to match
225 for constant in patternsʹ:
226 print(f"ERROR: could not find constant: {constant.name}.")
228 raise ValueError("Some constants were not found.")
232 # Root of the project
233 ROOT = Path(__file__).parent.parent
236 parser = argparse.ArgumentParser(description="Generate files from the message registry")
241 help="Path to the root of the project (default: %(default)s)",
244 args = parser.parse_args()
246 # Read some constants from libxkbcommon that we need
247 constants = read_constants(
248 Path(__file__).parent.parent / "src" / "keymap.h",
249 (Constant("XKB_MAX_GROUPS", re.compile("^#define\s+XKB_MAX_GROUPS\s+(\d+)"), int),),
253 template_loader = jinja2.FileSystemLoader(args.root, encoding="utf-8")
254 jinja_env = jinja2.Environment(
255 loader=template_loader,
256 keep_trailing_newline=True,
260 jinja_env.filters["prepend_todo"] = prepend_todo
262 # Load message registry
263 message_registry = load_message_registry(
264 jinja_env, constants, Path("doc/message-registry.yaml")
272 Path("src/messages-codes.h"),
276 message_registry, jinja_env, args.root, Path("tools/messages.c"), skip_removed=True
278 generate(message_registry, jinja_env, args.root, Path("doc/message-registry.md"))