##// END OF EJS Templates
Fix documentation generation with pass-through filters
krassowski -
Show More
@@ -1,221 +1,223 b''
1 1 from dataclasses import dataclass
2 2 from inspect import getsource
3 3 from pathlib import Path
4 4 from typing import cast, List, Union
5 5 from html import escape as html_escape
6 6 import re
7 7
8 8 from prompt_toolkit.keys import KEY_ALIASES
9 9 from prompt_toolkit.key_binding import KeyBindingsBase
10 10 from prompt_toolkit.filters import Filter, Condition
11 11 from prompt_toolkit.shortcuts import PromptSession
12 12
13 13 from IPython.terminal.shortcuts import create_ipython_shortcuts, create_identifier
14 14 from IPython.terminal.shortcuts.filters import KEYBINDING_FILTERS
15 15
16 16
17 17 @dataclass
18 18 class Shortcut:
19 19 #: a sequence of keys (each element on the list corresponds to pressing one or more keys)
20 20 keys_sequence: List[str]
21 21 filter: str
22 22
23 23
24 24 @dataclass
25 25 class Handler:
26 26 description: str
27 27 identifier: str
28 28
29 29
30 30 @dataclass
31 31 class Binding:
32 32 handler: Handler
33 33 shortcut: Shortcut
34 34
35 35
36 36 class _NestedFilter(Filter):
37 37 """Protocol reflecting non-public prompt_toolkit's `_AndList` and `_OrList`."""
38 38
39 39 filters: List[Filter]
40 40
41 41
42 42 class _Invert(Filter):
43 43 """Protocol reflecting non-public prompt_toolkit's `_Invert`."""
44 44
45 45 filter: Filter
46 46
47 47
48 48 conjunctions_labels = {"_AndList": "&", "_OrList": "|"}
49 49
50 50 ATOMIC_CLASSES = {"Never", "Always", "Condition"}
51 51
52 52
53 53 HUMAN_NAMES_FOR_FILTERS = {
54 54 filter_: name for name, filter_ in KEYBINDING_FILTERS.items()
55 55 }
56 56
57 57
58 58 def format_filter(
59 59 filter_: Union[Filter, _NestedFilter, Condition, _Invert],
60 60 is_top_level=True,
61 61 skip=None,
62 62 ) -> str:
63 63 """Create easily readable description of the filter."""
64 64 s = filter_.__class__.__name__
65 65 if s == "Condition":
66 66 func = cast(Condition, filter_).func
67 67 if filter_ in HUMAN_NAMES_FOR_FILTERS:
68 68 return HUMAN_NAMES_FOR_FILTERS[filter_]
69 69 name = func.__name__
70 70 if name == "<lambda>":
71 71 source = getsource(func)
72 72 return source.split("=")[0].strip()
73 73 return func.__name__
74 74 elif s == "_Invert":
75 75 operand = cast(_Invert, filter_).filter
76 76 if operand.__class__.__name__ in ATOMIC_CLASSES:
77 77 return f"~{format_filter(operand, is_top_level=False)}"
78 78 return f"~({format_filter(operand, is_top_level=False)})"
79 79 elif s in conjunctions_labels:
80 80 filters = cast(_NestedFilter, filter_).filters
81 81 if filter_ in HUMAN_NAMES_FOR_FILTERS:
82 82 return HUMAN_NAMES_FOR_FILTERS[filter_]
83 83 conjunction = conjunctions_labels[s]
84 84 glue = f" {conjunction} "
85 85 result = glue.join(format_filter(x, is_top_level=False) for x in filters)
86 86 if len(filters) > 1 and not is_top_level:
87 87 result = f"({result})"
88 88 return result
89 89 elif s in ["Never", "Always"]:
90 90 return s.lower()
91 elif s == "PassThrough":
92 return "pass_through"
91 93 else:
92 94 raise ValueError(f"Unknown filter type: {filter_}")
93 95
94 96
95 97 def sentencize(s) -> str:
96 98 """Extract first sentence"""
97 99 s = re.split(r"\.\W", s.replace("\n", " ").strip())
98 100 s = s[0] if len(s) else ""
99 101 if not s.endswith("."):
100 102 s += "."
101 103 try:
102 104 return " ".join(s.split())
103 105 except AttributeError:
104 106 return s
105 107
106 108
107 109 class _DummyTerminal:
108 110 """Used as a buffer to get prompt_toolkit bindings
109 111 """
110 112 handle_return = None
111 113 input_transformer_manager = None
112 114 display_completions = None
113 115 editing_mode = "emacs"
114 116 auto_suggest = None
115 117
116 118
117 119 def bindings_from_prompt_toolkit(prompt_bindings: KeyBindingsBase) -> List[Binding]:
118 120 """Collect bindings to a simple format that does not depend on prompt-toolkit internals"""
119 121 bindings: List[Binding] = []
120 122
121 123 for kb in prompt_bindings.bindings:
122 124 bindings.append(
123 125 Binding(
124 126 handler=Handler(
125 127 description=kb.handler.__doc__ or "",
126 128 identifier=create_identifier(kb.handler),
127 129 ),
128 130 shortcut=Shortcut(
129 131 keys_sequence=[
130 132 str(k.value) if hasattr(k, "value") else k for k in kb.keys
131 133 ],
132 134 filter=format_filter(kb.filter, skip={"has_focus_filter"}),
133 135 ),
134 136 )
135 137 )
136 138 return bindings
137 139
138 140
139 141 INDISTINGUISHABLE_KEYS = {**KEY_ALIASES, **{v: k for k, v in KEY_ALIASES.items()}}
140 142
141 143
142 144 def format_prompt_keys(keys: str, add_alternatives=True) -> str:
143 145 """Format prompt toolkit key with modifier into an RST representation."""
144 146
145 147 def to_rst(key):
146 148 escaped = key.replace("\\", "\\\\")
147 149 return f":kbd:`{escaped}`"
148 150
149 151 keys_to_press: List[str]
150 152
151 153 prefixes = {
152 154 "c-s-": [to_rst("ctrl"), to_rst("shift")],
153 155 "s-c-": [to_rst("ctrl"), to_rst("shift")],
154 156 "c-": [to_rst("ctrl")],
155 157 "s-": [to_rst("shift")],
156 158 }
157 159
158 160 for prefix, modifiers in prefixes.items():
159 161 if keys.startswith(prefix):
160 162 remainder = keys[len(prefix) :]
161 163 keys_to_press = [*modifiers, to_rst(remainder)]
162 164 break
163 165 else:
164 166 keys_to_press = [to_rst(keys)]
165 167
166 168 result = " + ".join(keys_to_press)
167 169
168 170 if keys in INDISTINGUISHABLE_KEYS and add_alternatives:
169 171 alternative = INDISTINGUISHABLE_KEYS[keys]
170 172
171 173 result = (
172 174 result
173 175 + " (or "
174 176 + format_prompt_keys(alternative, add_alternatives=False)
175 177 + ")"
176 178 )
177 179
178 180 return result
179 181
180 182
181 183 if __name__ == '__main__':
182 184 here = Path(__file__).parent
183 185 dest = here / "source" / "config" / "shortcuts"
184 186
185 187 ipy_bindings = create_ipython_shortcuts(_DummyTerminal())
186 188
187 189 session = PromptSession(key_bindings=ipy_bindings)
188 190 prompt_bindings = session.app.key_bindings
189 191
190 192 assert prompt_bindings
191 193 # Ensure that we collected the default shortcuts
192 194 assert len(prompt_bindings.bindings) > len(ipy_bindings.bindings)
193 195
194 196 bindings = bindings_from_prompt_toolkit(prompt_bindings)
195 197
196 198 def sort_key(binding: Binding):
197 199 return binding.handler.identifier, binding.shortcut.filter
198 200
199 201 filters = []
200 202 with (dest / "table.tsv").open("w", encoding="utf-8") as csv:
201 203 for binding in sorted(bindings, key=sort_key):
202 204 sequence = ", ".join(
203 205 [format_prompt_keys(keys) for keys in binding.shortcut.keys_sequence]
204 206 )
205 207 if binding.shortcut.filter == "always":
206 208 condition_label = "-"
207 209 else:
208 210 # we cannot fit all the columns as the filters got too complex over time
209 211 condition_label = "ⓘ"
210 212
211 213 csv.write(
212 214 "\t".join(
213 215 [
214 216 sequence,
215 217 sentencize(binding.handler.description)
216 218 + f" :raw-html:`<br>` `{binding.handler.identifier}`",
217 219 f':raw-html:`<span title="{html_escape(binding.shortcut.filter)}" style="cursor: help">{condition_label}</span>`',
218 220 ]
219 221 )
220 222 + "\n"
221 223 )
General Comments 0
You need to be logged in to leave comments. Login now