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