logo

qmk_firmware

custom branch of QMK firmware git clone https://anongit.hacktivis.me/git/qmk_firmware.git

search.py (12114B)


  1. """Functions for searching through QMK keyboards and keymaps.
  2. """
  3. from dataclasses import dataclass
  4. import contextlib
  5. import functools
  6. import fnmatch
  7. import json
  8. import logging
  9. import re
  10. from typing import Callable, Dict, List, Optional, Tuple, Union
  11. from dotty_dict import dotty, Dotty
  12. from milc import cli
  13. from qmk.util import parallel_map
  14. from qmk.info import keymap_json
  15. from qmk.keyboard import list_keyboards, keyboard_folder
  16. from qmk.keymap import list_keymaps, locate_keymap
  17. from qmk.build_targets import KeyboardKeymapBuildTarget, BuildTarget
  18. @dataclass
  19. class KeyboardKeymapDesc:
  20. keyboard: str
  21. keymap: str
  22. data: dict = None
  23. extra_args: dict = None
  24. def __hash__(self) -> int:
  25. return self.keyboard.__hash__() ^ self.keymap.__hash__() ^ json.dumps(self.extra_args, sort_keys=True).__hash__()
  26. def __lt__(self, other) -> bool:
  27. return (self.keyboard, self.keymap, json.dumps(self.extra_args, sort_keys=True)) < (other.keyboard, other.keymap, json.dumps(other.extra_args, sort_keys=True))
  28. def load_data(self):
  29. data = keymap_json(self.keyboard, self.keymap)
  30. self.data = data.to_dict() if isinstance(data, Dotty) else data
  31. @property
  32. def dotty(self) -> Dotty:
  33. return dotty(self.data) if self.data is not None else None
  34. def to_build_target(self) -> KeyboardKeymapBuildTarget:
  35. target = KeyboardKeymapBuildTarget(keyboard=self.keyboard, keymap=self.keymap, json=self.data)
  36. target.extra_args = self.extra_args
  37. return target
  38. # by using a class for filters, we dont need to worry about capturing values
  39. # see details <https://github.com/qmk/qmk_firmware/pull/21090>
  40. class FilterFunction:
  41. """Base class for filters.
  42. It provides:
  43. - __init__: capture key and value
  44. Each subclass should provide:
  45. - func_name: how it will be specified on CLI
  46. >>> qmk find -f <func_name>...
  47. - apply: function that actually applies the filter
  48. ie: return whether the input kb/km satisfies the condition
  49. """
  50. key: str
  51. value: Optional[str]
  52. func_name: str
  53. apply: Callable[[KeyboardKeymapDesc], bool]
  54. def __init__(self, key, value):
  55. self.key = key
  56. self.value = value
  57. class Exists(FilterFunction):
  58. func_name = "exists"
  59. def apply(self, target_info: KeyboardKeymapDesc) -> bool:
  60. return self.key in target_info.dotty
  61. class Absent(FilterFunction):
  62. func_name = "absent"
  63. def apply(self, target_info: KeyboardKeymapDesc) -> bool:
  64. return self.key not in target_info.dotty
  65. class Length(FilterFunction):
  66. func_name = "length"
  67. def apply(self, target_info: KeyboardKeymapDesc) -> bool:
  68. info_dotty = target_info.dotty
  69. return (self.key in info_dotty and len(info_dotty[self.key]) == int(self.value))
  70. class Contains(FilterFunction):
  71. func_name = "contains"
  72. def apply(self, target_info: KeyboardKeymapDesc) -> bool:
  73. info_dotty = target_info.dotty
  74. return (self.key in info_dotty and self.value in info_dotty[self.key])
  75. def _get_filter_class(func_name: str, key: str, value: str) -> Optional[FilterFunction]:
  76. """Initialize a filter subclass based on regex findings and return it.
  77. None if no there's no filter with the name queried.
  78. """
  79. for subclass in FilterFunction.__subclasses__():
  80. if func_name == subclass.func_name:
  81. return subclass(key, value)
  82. return None
  83. def filter_help() -> str:
  84. names = [f"'{f.func_name}'" for f in FilterFunction.__subclasses__()]
  85. return ", ".join(names[:-1]) + f" and {names[-1]}"
  86. def _set_log_level(level):
  87. cli.acquire_lock()
  88. try:
  89. old = cli.log_level
  90. cli.log_level = level
  91. except AttributeError:
  92. old = cli.log.level
  93. cli.log.setLevel(level)
  94. logging.root.setLevel(level)
  95. cli.release_lock()
  96. return old
  97. @contextlib.contextmanager
  98. def ignore_logging():
  99. old = _set_log_level(logging.CRITICAL)
  100. yield
  101. _set_log_level(old)
  102. def _all_keymaps(keyboard) -> List[KeyboardKeymapDesc]:
  103. """Returns a list of KeyboardKeymapDesc for all keymaps for the given keyboard.
  104. """
  105. with ignore_logging():
  106. keyboard = keyboard_folder(keyboard)
  107. return [KeyboardKeymapDesc(keyboard, keymap) for keymap in list_keymaps(keyboard)]
  108. def _keymap_exists(keyboard, keymap):
  109. """Returns the keyboard name if the keyboard+keymap combination exists, otherwise None.
  110. """
  111. with ignore_logging():
  112. return keyboard if locate_keymap(keyboard, keymap) is not None else None
  113. def _load_keymap_info(target: KeyboardKeymapDesc) -> KeyboardKeymapDesc:
  114. """Ensures a KeyboardKeymapDesc has its data loaded.
  115. """
  116. with ignore_logging():
  117. target.load_data() # Ensure we load the data first
  118. return target
  119. def expand_make_targets(targets: List[Union[str, Tuple[str, Dict[str, str]]]]) -> List[KeyboardKeymapDesc]:
  120. """Expand a list of make targets into a list of KeyboardKeymapDesc.
  121. Caters for 'all' in either keyboard or keymap, or both.
  122. """
  123. split_targets = []
  124. for target in targets:
  125. extra_args = None
  126. if isinstance(target, tuple):
  127. split_target = target[0].split(':')
  128. extra_args = target[1]
  129. else:
  130. split_target = target.split(':')
  131. if len(split_target) != 2:
  132. cli.log.error(f"Invalid build target: {target}")
  133. return []
  134. split_targets.append(KeyboardKeymapDesc(split_target[0], split_target[1], extra_args=extra_args))
  135. return expand_keymap_targets(split_targets)
  136. def _expand_keymap_target(target: KeyboardKeymapDesc, all_keyboards: List[str] = None) -> List[KeyboardKeymapDesc]:
  137. """Expand a keyboard input and keymap input into a list of KeyboardKeymapDesc.
  138. Caters for 'all' in either keyboard or keymap, or both.
  139. """
  140. if all_keyboards is None:
  141. all_keyboards = list_keyboards()
  142. if target.keyboard == 'all':
  143. if target.keymap == 'all':
  144. cli.log.info('Retrieving list of all keyboards and keymaps...')
  145. targets = []
  146. for kb in parallel_map(_all_keymaps, all_keyboards):
  147. targets.extend(kb)
  148. for t in targets:
  149. t.extra_args = target.extra_args
  150. return targets
  151. else:
  152. cli.log.info(f'Retrieving list of keyboards with keymap "{target.keymap}"...')
  153. keyboard_filter = functools.partial(_keymap_exists, keymap=target.keymap)
  154. return [KeyboardKeymapDesc(kb, target.keymap, extra_args=target.extra_args) for kb in filter(lambda e: e is not None, parallel_map(keyboard_filter, all_keyboards))]
  155. else:
  156. if target.keymap == 'all':
  157. cli.log.info(f'Retrieving list of keymaps for keyboard "{target.keyboard}"...')
  158. targets = _all_keymaps(target.keyboard)
  159. for t in targets:
  160. t.extra_args = target.extra_args
  161. return targets
  162. else:
  163. return [target]
  164. def expand_keymap_targets(targets: List[KeyboardKeymapDesc]) -> List[KeyboardKeymapDesc]:
  165. """Expand a list of KeyboardKeymapDesc inclusive of 'all', into a list of explicit KeyboardKeymapDesc.
  166. """
  167. overall_targets = []
  168. all_keyboards = list_keyboards()
  169. for target in targets:
  170. overall_targets.extend(_expand_keymap_target(target, all_keyboards))
  171. return list(sorted(set(overall_targets)))
  172. def _construct_build_target(e: KeyboardKeymapDesc):
  173. return e.to_build_target()
  174. def _filter_keymap_targets(target_list: List[KeyboardKeymapDesc], filters: List[str] = []) -> List[KeyboardKeymapDesc]:
  175. """Filter a list of KeyboardKeymapDesc based on the supplied filters.
  176. Optionally includes the values of the queried info.json keys.
  177. """
  178. if len(filters) == 0:
  179. cli.log.info('Preparing target list...')
  180. targets = target_list
  181. else:
  182. cli.log.info('Parsing data for all matching keyboard/keymap combinations...')
  183. valid_targets = parallel_map(_load_keymap_info, target_list)
  184. function_re = re.compile(r'^(?P<function>[a-zA-Z]+)\((?P<key>[a-zA-Z0-9_\.]+)(,\s*(?P<value>[^#]+))?\)$')
  185. comparison_re = re.compile(r'^(?P<key>[a-zA-Z0-9_\.]+)\s*(?P<op>[\<\>\!=]=|\<|\>)\s*(?P<value>[^#]+)$')
  186. for filter_expr in filters:
  187. function_match = function_re.match(filter_expr)
  188. comparison_match = comparison_re.match(filter_expr)
  189. if function_match is not None:
  190. func_name = function_match.group('function').lower()
  191. key = function_match.group('key')
  192. value = function_match.group('value')
  193. filter_class = _get_filter_class(func_name, key, value)
  194. if filter_class is None:
  195. cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}')
  196. continue
  197. valid_targets = filter(filter_class.apply, valid_targets)
  198. value_str = f", {{fg_cyan}}{value}{{fg_reset}}" if value is not None else ""
  199. cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}}{value_str})...')
  200. elif comparison_match is not None:
  201. key = comparison_match.group('key')
  202. op = comparison_match.group('op')
  203. value = comparison_match.group('value')
  204. cli.log.info(f'Filtering on condition: {{fg_cyan}}{key}{{fg_reset}} {op} {{fg_cyan}}{value}{{fg_reset}}...')
  205. def _make_filter(k, o, v):
  206. expr = fnmatch.translate(v)
  207. rule = re.compile(f'^{expr}$', re.IGNORECASE)
  208. def f(e: KeyboardKeymapDesc):
  209. lhs = e.dotty.get(k)
  210. rhs = v
  211. if o in ['<', '>', '<=', '>=']:
  212. lhs = int(False if lhs is None else lhs)
  213. rhs = int(rhs)
  214. if o == '<':
  215. return lhs < rhs
  216. elif o == '>':
  217. return lhs > rhs
  218. elif o == '<=':
  219. return lhs <= rhs
  220. elif o == '>=':
  221. return lhs >= rhs
  222. else:
  223. lhs = str(False if lhs is None else lhs)
  224. if o == '!=':
  225. return rule.search(lhs) is None
  226. elif o == '==':
  227. return rule.search(lhs) is not None
  228. return f
  229. valid_targets = filter(_make_filter(key, op, value), valid_targets)
  230. else:
  231. cli.log.warning(f'Unrecognized filter expression: {filter_expr}')
  232. continue
  233. cli.log.info('Preparing target list...')
  234. targets = list(sorted(set(valid_targets)))
  235. return targets
  236. def search_keymap_targets(targets: List[Union[Tuple[str, str], Tuple[str, str, Dict[str, str]]]] = [('all', 'default')], filters: List[str] = []) -> List[BuildTarget]:
  237. """Search for build targets matching the supplied criteria.
  238. """
  239. def _make_desc(e):
  240. if len(e) == 3:
  241. return KeyboardKeymapDesc(keyboard=e[0], keymap=e[1], extra_args=e[2])
  242. else:
  243. return KeyboardKeymapDesc(keyboard=e[0], keymap=e[1])
  244. targets = map(_make_desc, targets)
  245. targets = _filter_keymap_targets(expand_keymap_targets(targets), filters)
  246. targets = list(set(parallel_map(_construct_build_target, list(targets))))
  247. return sorted(targets)
  248. def search_make_targets(targets: List[Union[str, Tuple[str, Dict[str, str]]]], filters: List[str] = []) -> List[BuildTarget]:
  249. """Search for build targets matching the supplied criteria.
  250. """
  251. targets = _filter_keymap_targets(expand_make_targets(targets), filters)
  252. targets = list(set(parallel_map(_construct_build_target, list(targets))))
  253. return sorted(targets)