Show More
@@ -0,0 +1,320 b'' | |||
|
1 | """ | |
|
2 | This module contains utility function and classes to inject simple ast | |
|
3 | transformations based on code strings into IPython. While it is already possible | |
|
4 | with ast-transformers it is not easy to directly manipulate ast. | |
|
5 | ||
|
6 | ||
|
7 | IPython has pre-code and post-code hooks, but are ran from within the IPython | |
|
8 | machinery so may be inappropriate, for example for performance mesurement. | |
|
9 | ||
|
10 | This module give you tools to simplify this, and expose 2 classes: | |
|
11 | ||
|
12 | - `ReplaceCodeTransformer` which is a simple ast transformer based on code | |
|
13 | template, | |
|
14 | ||
|
15 | and for advance case: | |
|
16 | ||
|
17 | - `Mangler` which is a simple ast transformer that mangle names in the ast. | |
|
18 | ||
|
19 | ||
|
20 | Example, let's try to make a simple version of the ``timeit`` magic, that run a | |
|
21 | code snippet 10 times and print the average time taken. | |
|
22 | ||
|
23 | Basically we want to run : | |
|
24 | ||
|
25 | .. code-block:: python | |
|
26 | ||
|
27 | from time import perf_counter | |
|
28 | now = perf_counter() | |
|
29 | for i in range(10): | |
|
30 | __code__ # our code | |
|
31 | print(f"Time taken: {(perf_counter() - now)/10}") | |
|
32 | __ret__ # the result of the last statement | |
|
33 | ||
|
34 | Where ``__code__`` is the code snippet we want to run, and ``__ret__`` is the | |
|
35 | result, so that if we for example run `dataframe.head()` IPython still display | |
|
36 | the head of dataframe instead of nothing. | |
|
37 | ||
|
38 | Here is a complete example of a file `timit2.py` that define such a magic: | |
|
39 | ||
|
40 | .. code-block:: python | |
|
41 | ||
|
42 | from IPython.core.magic import ( | |
|
43 | Magics, | |
|
44 | magics_class, | |
|
45 | line_cell_magic, | |
|
46 | ) | |
|
47 | from IPython.core.magics.ast_mod import ReplaceCodeTransformer | |
|
48 | from textwrap import dedent | |
|
49 | import ast | |
|
50 | ||
|
51 | template = template = dedent(''' | |
|
52 | from time import perf_counter | |
|
53 | now = perf_counter() | |
|
54 | for i in range(10): | |
|
55 | __code__ | |
|
56 | print(f"Time taken: {(perf_counter() - now)/10}") | |
|
57 | __ret__ | |
|
58 | ''' | |
|
59 | ) | |
|
60 | ||
|
61 | ||
|
62 | @magics_class | |
|
63 | class AstM(Magics): | |
|
64 | @line_cell_magic | |
|
65 | def t2(self, line, cell): | |
|
66 | transformer = ReplaceCodeTransformer.from_string(template) | |
|
67 | transformer.debug = True | |
|
68 | transformer.mangler.debug = True | |
|
69 | new_code = transformer.visit(ast.parse(cell)) | |
|
70 | return exec(compile(new_code, "<ast>", "exec")) | |
|
71 | ||
|
72 | ||
|
73 | def load_ipython_extension(ip): | |
|
74 | ip.register_magics(AstM) | |
|
75 | ||
|
76 | ||
|
77 | ||
|
78 | .. code-block:: python | |
|
79 | ||
|
80 | In [1]: %load_ext timit2 | |
|
81 | ||
|
82 | In [2]: %%t2 | |
|
83 | ...: import time | |
|
84 | ...: time.sleep(0.05) | |
|
85 | ...: | |
|
86 | ...: | |
|
87 | Time taken: 0.05435649999999441 | |
|
88 | ||
|
89 | ||
|
90 | If you wish to ran all the code enter in IPython in an ast transformer, you can | |
|
91 | do so as well: | |
|
92 | ||
|
93 | .. code-block:: python | |
|
94 | ||
|
95 | In [1]: from IPython.core.magics.ast_mod import ReplaceCodeTransformer | |
|
96 | ...: | |
|
97 | ...: template = ''' | |
|
98 | ...: from time import perf_counter | |
|
99 | ...: now = perf_counter() | |
|
100 | ...: __code__ | |
|
101 | ...: print(f"Code ran in {perf_counter()-now}") | |
|
102 | ...: __ret__''' | |
|
103 | ...: | |
|
104 | ...: get_ipython().ast_transformers.append(ReplaceCodeTransformer.from_string(template)) | |
|
105 | ||
|
106 | In [2]: 1+1 | |
|
107 | Code ran in 3.40410006174352e-05 | |
|
108 | Out[2]: 2 | |
|
109 | ||
|
110 | ||
|
111 | ||
|
112 | Hygiene and Mangling | |
|
113 | -------------------- | |
|
114 | ||
|
115 | The ast transformer above is not hygienic, it may not work if the user code use | |
|
116 | the same variable names as the ones used in the template. For example. | |
|
117 | ||
|
118 | To help with this by default the `ReplaceCodeTransformer` will mangle all names | |
|
119 | staring with 3 underscores. This is a simple heuristic that should work in most | |
|
120 | case, but can be cumbersome in some case. We provide a `Mangler` class that can | |
|
121 | be overridden to change the mangling heuristic, or simply use the `mangle_all` | |
|
122 | utility function. It will _try_ to mangle all names (except `__ret__` and | |
|
123 | `__code__`), but this include builtins (``print``, ``range``, ``type``) and | |
|
124 | replace those by invalid identifiers py prepending ``mangle-``: | |
|
125 | ``mangle-print``, ``mangle-range``, ``mangle-type`` etc. This is not a problem | |
|
126 | as currently Python AST support invalid identifiers, but it may not be the case | |
|
127 | in the future. | |
|
128 | ||
|
129 | You can set `ReplaceCodeTransformer.debug=True` and | |
|
130 | `ReplaceCodeTransformer.mangler.debug=True` to see the code after mangling and | |
|
131 | transforming: | |
|
132 | ||
|
133 | .. code-block:: python | |
|
134 | ||
|
135 | ||
|
136 | In [1]: from IPython.core.magics.ast_mod import ReplaceCodeTransformer, mangle_all | |
|
137 | ...: | |
|
138 | ...: template = ''' | |
|
139 | ...: from builtins import type, print | |
|
140 | ...: from time import perf_counter | |
|
141 | ...: now = perf_counter() | |
|
142 | ...: __code__ | |
|
143 | ...: print(f"Code ran in {perf_counter()-now}") | |
|
144 | ...: __ret__''' | |
|
145 | ...: | |
|
146 | ...: transformer = ReplaceCodeTransformer.from_string(template, mangling_predicate=mangle_all) | |
|
147 | ||
|
148 | ||
|
149 | In [2]: transformer.debug = True | |
|
150 | ...: transformer.mangler.debug = True | |
|
151 | ...: get_ipython().ast_transformers.append(transformer) | |
|
152 | ||
|
153 | In [3]: 1+1 | |
|
154 | Mangling Alias mangle-type | |
|
155 | Mangling Alias mangle-print | |
|
156 | Mangling Alias mangle-perf_counter | |
|
157 | Mangling now | |
|
158 | Mangling perf_counter | |
|
159 | Not mangling __code__ | |
|
160 | Mangling print | |
|
161 | Mangling perf_counter | |
|
162 | Mangling now | |
|
163 | Not mangling __ret__ | |
|
164 | ---- Transformed code ---- | |
|
165 | from builtins import type as mangle-type, print as mangle-print | |
|
166 | from time import perf_counter as mangle-perf_counter | |
|
167 | mangle-now = mangle-perf_counter() | |
|
168 | ret-tmp = 1 + 1 | |
|
169 | mangle-print(f'Code ran in {mangle-perf_counter() - mangle-now}') | |
|
170 | ret-tmp | |
|
171 | ---- ---------------- ---- | |
|
172 | Code ran in 0.00013654199938173406 | |
|
173 | Out[3]: 2 | |
|
174 | ||
|
175 | ||
|
176 | """ | |
|
177 | ||
|
178 | __skip_doctest__ = True | |
|
179 | ||
|
180 | ||
|
181 | from ast import NodeTransformer, Store, Load, Name, Expr, Assign, Module | |
|
182 | import ast | |
|
183 | import copy | |
|
184 | ||
|
185 | from typing import Dict, Optional | |
|
186 | ||
|
187 | ||
|
188 | mangle_all = lambda name: False if name in ("__ret__", "__code__") else True | |
|
189 | ||
|
190 | ||
|
191 | class Mangler(NodeTransformer): | |
|
192 | """ | |
|
193 | Mangle given names in and ast tree to make sure they do not conflict with | |
|
194 | user code. | |
|
195 | """ | |
|
196 | ||
|
197 | enabled: bool = True | |
|
198 | debug: bool = False | |
|
199 | ||
|
200 | def log(self, *args, **kwargs): | |
|
201 | if self.debug: | |
|
202 | print(*args, **kwargs) | |
|
203 | ||
|
204 | def __init__(self, predicate=None): | |
|
205 | if predicate is None: | |
|
206 | predicate = lambda name: name.startswith("___") | |
|
207 | self.predicate = predicate | |
|
208 | ||
|
209 | def visit_Name(self, node): | |
|
210 | if self.predicate(node.id): | |
|
211 | self.log("Mangling", node.id) | |
|
212 | # Once in the ast we do not need | |
|
213 | # names to be valid identifiers. | |
|
214 | node.id = "mangle-" + node.id | |
|
215 | else: | |
|
216 | self.log("Not mangling", node.id) | |
|
217 | return node | |
|
218 | ||
|
219 | def visit_FunctionDef(self, node): | |
|
220 | if self.predicate(node.name): | |
|
221 | self.log("Mangling", node.name) | |
|
222 | node.name = "mangle-" + node.name | |
|
223 | else: | |
|
224 | self.log("Not mangling", node.name) | |
|
225 | ||
|
226 | for arg in node.args.args: | |
|
227 | if self.predicate(arg.arg): | |
|
228 | self.log("Mangling function arg", arg.arg) | |
|
229 | arg.arg = "mangle-" + arg.arg | |
|
230 | else: | |
|
231 | self.log("Not mangling function arg", arg.arg) | |
|
232 | return self.generic_visit(node) | |
|
233 | ||
|
234 | def visit_ImportFrom(self, node): | |
|
235 | return self._visit_Import_and_ImportFrom(node) | |
|
236 | ||
|
237 | def visit_Import(self, node): | |
|
238 | return self._visit_Import_and_ImportFrom(node) | |
|
239 | ||
|
240 | def _visit_Import_and_ImportFrom(self, node): | |
|
241 | for alias in node.names: | |
|
242 | asname = alias.name if alias.asname is None else alias.asname | |
|
243 | if self.predicate(asname): | |
|
244 | new_name: str = "mangle-" + asname | |
|
245 | self.log("Mangling Alias", new_name) | |
|
246 | alias.asname = new_name | |
|
247 | else: | |
|
248 | self.log("Not mangling Alias", alias.asname) | |
|
249 | return node | |
|
250 | ||
|
251 | ||
|
252 | class ReplaceCodeTransformer(NodeTransformer): | |
|
253 | enabled: bool = True | |
|
254 | debug: bool = False | |
|
255 | mangler: Mangler | |
|
256 | ||
|
257 | def __init__( | |
|
258 | self, template: Module, mapping: Optional[Dict] = None, mangling_predicate=None | |
|
259 | ): | |
|
260 | assert isinstance(mapping, (dict, type(None))) | |
|
261 | assert isinstance(mangling_predicate, (type(None), type(lambda: None))) | |
|
262 | assert isinstance(template, ast.Module) | |
|
263 | self.template = template | |
|
264 | self.mangler = Mangler(predicate=mangling_predicate) | |
|
265 | if mapping is None: | |
|
266 | mapping = {} | |
|
267 | self.mapping = mapping | |
|
268 | ||
|
269 | @classmethod | |
|
270 | def from_string( | |
|
271 | cls, template: str, mapping: Optional[Dict] = None, mangling_predicate=None | |
|
272 | ): | |
|
273 | return cls( | |
|
274 | ast.parse(template), mapping=mapping, mangling_predicate=mangling_predicate | |
|
275 | ) | |
|
276 | ||
|
277 | def visit_Module(self, code): | |
|
278 | if not self.enabled: | |
|
279 | return code | |
|
280 | # if not isinstance(code, ast.Module): | |
|
281 | # recursively called... | |
|
282 | # return generic_visit(self, code) | |
|
283 | last = code.body[-1] | |
|
284 | if isinstance(last, Expr): | |
|
285 | code.body.pop() | |
|
286 | code.body.append(Assign([Name("ret-tmp", ctx=Store())], value=last.value)) | |
|
287 | ast.fix_missing_locations(code) | |
|
288 | ret = Expr(value=Name("ret-tmp", ctx=Load())) | |
|
289 | ret = ast.fix_missing_locations(ret) | |
|
290 | self.mapping["__ret__"] = ret | |
|
291 | else: | |
|
292 | self.mapping["__ret__"] = ast.parse("None").body[0] | |
|
293 | self.mapping["__code__"] = code.body | |
|
294 | tpl = ast.fix_missing_locations(self.template) | |
|
295 | ||
|
296 | tx = copy.deepcopy(tpl) | |
|
297 | tx = self.mangler.visit(tx) | |
|
298 | node = self.generic_visit(tx) | |
|
299 | node_2 = ast.fix_missing_locations(node) | |
|
300 | if self.debug: | |
|
301 | print("---- Transformed code ----") | |
|
302 | print(ast.unparse(node_2)) | |
|
303 | print("---- ---------------- ----") | |
|
304 | return node_2 | |
|
305 | ||
|
306 | # this does not work as the name might be in a list and one might want to extend the list. | |
|
307 | # def visit_Name(self, name): | |
|
308 | # if name.id in self.mapping and name.id == "__ret__": | |
|
309 | # print(name, "in mapping") | |
|
310 | # if isinstance(name.ctx, ast.Store): | |
|
311 | # return Name("tmp", ctx=Store()) | |
|
312 | # else: | |
|
313 | # return copy.deepcopy(self.mapping[name.id]) | |
|
314 | # return name | |
|
315 | ||
|
316 | def visit_Expr(self, expr): | |
|
317 | if isinstance(expr.value, Name) and expr.value.id in self.mapping: | |
|
318 | if self.mapping[expr.value.id] is not None: | |
|
319 | return copy.deepcopy(self.mapping[expr.value.id]) | |
|
320 | return self.generic_visit(expr) |
@@ -3338,8 +3338,11 b' class InteractiveShell(SingletonConfigurable):' | |||
|
3338 | 3338 | # an InputRejected. Short-circuit in this case so that we |
|
3339 | 3339 | # don't unregister the transform. |
|
3340 | 3340 | raise |
|
3341 | except Exception: | |
|
3342 | warn("AST transformer %r threw an error. It will be unregistered." % transformer) | |
|
3341 | except Exception as e: | |
|
3342 | warn( | |
|
3343 | "AST transformer %r threw an error. It will be unregistered. %s" | |
|
3344 | % (transformer, e) | |
|
3345 | ) | |
|
3343 | 3346 | self.ast_transformers.remove(transformer) |
|
3344 | 3347 | |
|
3345 | 3348 | if self.ast_transformers: |
@@ -8,6 +8,7 b'' | |||
|
8 | 8 | import ast |
|
9 | 9 | import bdb |
|
10 | 10 | import builtins as builtin_mod |
|
11 | import copy | |
|
11 | 12 | import cProfile as profile |
|
12 | 13 | import gc |
|
13 | 14 | import itertools |
@@ -19,14 +20,28 b' import shlex' | |||
|
19 | 20 | import sys |
|
20 | 21 | import time |
|
21 | 22 | import timeit |
|
22 | from ast import Module | |
|
23 | from typing import Dict, Any | |
|
24 | from ast import ( | |
|
25 | Assign, | |
|
26 | Call, | |
|
27 | Expr, | |
|
28 | Load, | |
|
29 | Module, | |
|
30 | Name, | |
|
31 | NodeTransformer, | |
|
32 | Store, | |
|
33 | parse, | |
|
34 | unparse, | |
|
35 | ) | |
|
23 | 36 | from io import StringIO |
|
24 | 37 | from logging import error |
|
25 | 38 | from pathlib import Path |
|
26 | 39 | from pdb import Restart |
|
40 | from textwrap import dedent, indent | |
|
27 | 41 | from warnings import warn |
|
28 | 42 | |
|
29 | 43 | from IPython.core import magic_arguments, oinspect, page |
|
44 | from IPython.core.displayhook import DisplayHook | |
|
30 | 45 | from IPython.core.error import UsageError |
|
31 | 46 | from IPython.core.macro import Macro |
|
32 | 47 | from IPython.core.magic import ( |
@@ -37,8 +52,8 b' from IPython.core.magic import (' | |||
|
37 | 52 | magics_class, |
|
38 | 53 | needs_local_scope, |
|
39 | 54 | no_var_expand, |
|
40 | output_can_be_silenced, | |
|
41 | 55 | on_off, |
|
56 | output_can_be_silenced, | |
|
42 | 57 | ) |
|
43 | 58 | from IPython.testing.skipdoctest import skip_doctest |
|
44 | 59 | from IPython.utils.capture import capture_output |
@@ -47,7 +62,7 b' from IPython.utils.ipstruct import Struct' | |||
|
47 | 62 | from IPython.utils.module_paths import find_mod |
|
48 | 63 | from IPython.utils.path import get_py_filename, shellglob |
|
49 | 64 | from IPython.utils.timing import clock, clock2 |
|
50 | from IPython.core.displayhook import DisplayHook | |
|
65 | from IPython.core.magics.ast_mod import ReplaceCodeTransformer | |
|
51 | 66 | |
|
52 | 67 | #----------------------------------------------------------------------------- |
|
53 | 68 | # Magic implementation classes |
@@ -164,9 +179,9 b' class Timer(timeit.Timer):' | |||
|
164 | 179 | |
|
165 | 180 | @magics_class |
|
166 | 181 | class ExecutionMagics(Magics): |
|
167 | """Magics related to code execution, debugging, profiling, etc. | |
|
182 | """Magics related to code execution, debugging, profiling, etc.""" | |
|
168 | 183 | |
|
169 | """ | |
|
184 | _transformers: Dict[str, Any] = {} | |
|
170 | 185 | |
|
171 | 186 | def __init__(self, shell): |
|
172 | 187 | super(ExecutionMagics, self).__init__(shell) |
@@ -1474,6 +1489,83 b' class ExecutionMagics(Magics):' | |||
|
1474 | 1489 | elif args.output: |
|
1475 | 1490 | self.shell.user_ns[args.output] = io |
|
1476 | 1491 | |
|
1492 | @skip_doctest | |
|
1493 | @magic_arguments.magic_arguments() | |
|
1494 | @magic_arguments.argument("name", type=str, default="default", nargs="?") | |
|
1495 | @magic_arguments.argument( | |
|
1496 | "--remove", action="store_true", help="remove the current transformer" | |
|
1497 | ) | |
|
1498 | @magic_arguments.argument( | |
|
1499 | "--list", action="store_true", help="list existing transformers name" | |
|
1500 | ) | |
|
1501 | @magic_arguments.argument( | |
|
1502 | "--list-all", | |
|
1503 | action="store_true", | |
|
1504 | help="list existing transformers name and code template", | |
|
1505 | ) | |
|
1506 | @line_cell_magic | |
|
1507 | def code_wrap(self, line, cell=None): | |
|
1508 | """ | |
|
1509 | Simple magic to quickly define a code transformer for all IPython's future imput. | |
|
1510 | ||
|
1511 | ``__code__`` and ``__ret__`` are special variable that represent the code to run | |
|
1512 | and the value of the last expression of ``__code__`` respectively. | |
|
1513 | ||
|
1514 | Examples | |
|
1515 | -------- | |
|
1516 | ||
|
1517 | .. ipython:: | |
|
1518 | ||
|
1519 | In [1]: %%code_wrap before_after | |
|
1520 | ...: print('before') | |
|
1521 | ...: __code__ | |
|
1522 | ...: print('after') | |
|
1523 | ...: __ret__ | |
|
1524 | ||
|
1525 | ||
|
1526 | In [2]: 1 | |
|
1527 | before | |
|
1528 | after | |
|
1529 | Out[2]: 1 | |
|
1530 | ||
|
1531 | In [3]: %code_wrap --list | |
|
1532 | before_after | |
|
1533 | ||
|
1534 | In [4]: %code_wrap --list-all | |
|
1535 | before_after : | |
|
1536 | print('before') | |
|
1537 | __code__ | |
|
1538 | print('after') | |
|
1539 | __ret__ | |
|
1540 | ||
|
1541 | In [5]: %code_wrap --remove before_after | |
|
1542 | ||
|
1543 | """ | |
|
1544 | args = magic_arguments.parse_argstring(self.code_wrap, line) | |
|
1545 | ||
|
1546 | if args.list: | |
|
1547 | for name in self._transformers.keys(): | |
|
1548 | print(name) | |
|
1549 | return | |
|
1550 | if args.list_all: | |
|
1551 | for name, _t in self._transformers.items(): | |
|
1552 | print(name, ":") | |
|
1553 | print(indent(ast.unparse(_t.template), " ")) | |
|
1554 | print() | |
|
1555 | return | |
|
1556 | ||
|
1557 | to_remove = self._transformers.pop(args.name, None) | |
|
1558 | if to_remove in self.shell.ast_transformers: | |
|
1559 | self.shell.ast_transformers.remove(to_remove) | |
|
1560 | if cell is None or args.remove: | |
|
1561 | return | |
|
1562 | ||
|
1563 | _trs = ReplaceCodeTransformer(ast.parse(cell)) | |
|
1564 | ||
|
1565 | self._transformers[args.name] = _trs | |
|
1566 | self.shell.ast_transformers.append(_trs) | |
|
1567 | ||
|
1568 | ||
|
1477 | 1569 | def parse_breakpoint(text, current_file): |
|
1478 | 1570 | '''Returns (file, line) for file:line and (current_file, line) for line''' |
|
1479 | 1571 | colon = text.find(':') |
@@ -1519,4 +1611,4 b' def _format_time(timespan, precision=3):' | |||
|
1519 | 1611 | order = min(-int(math.floor(math.log10(timespan)) // 3), 3) |
|
1520 | 1612 | else: |
|
1521 | 1613 | order = 3 |
|
1522 |
return |
|
|
1614 | return "%.*g %s" % (precision, timespan * scaling[order], units[order]) |
General Comments 0
You need to be logged in to leave comments.
Login now