| """Implementation of code management magic functions. |
| """ |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
| import inspect |
| import io |
| import os |
| import re |
| import sys |
| import ast |
| from itertools import chain |
| from urllib.request import Request, urlopen |
| from urllib.parse import urlencode |
| from pathlib import Path |
|
|
| |
| from IPython.core.error import TryNext, StdinNotImplementedError, UsageError |
| from IPython.core.macro import Macro |
| from IPython.core.magic import Magics, magics_class, line_magic |
| from IPython.core.oinspect import find_file, find_source_lines |
| from IPython.core.release import version |
| from IPython.testing.skipdoctest import skip_doctest |
| from IPython.utils.contexts import preserve_keys |
| from IPython.utils.path import get_py_filename |
| from warnings import warn |
| from logging import error |
| from IPython.utils.text import get_text_list |
|
|
| |
| |
| |
|
|
| |
| class MacroToEdit(ValueError): pass |
|
|
| ipython_input_pat = re.compile(r"<ipython\-input\-(\d+)-[a-z\d]+>$") |
|
|
| |
| range_re = re.compile(r""" |
| (?P<start>\d+)? |
| ((?P<sep>[\-:]) |
| (?P<end>\d+)?)? |
| $""", re.VERBOSE) |
|
|
|
|
| def extract_code_ranges(ranges_str): |
| """Turn a string of range for %%load into 2-tuples of (start, stop) |
| ready to use as a slice of the content split by lines. |
| |
| Examples |
| -------- |
| list(extract_input_ranges("5-10 2")) |
| [(4, 10), (1, 2)] |
| """ |
| for range_str in ranges_str.split(): |
| rmatch = range_re.match(range_str) |
| if not rmatch: |
| continue |
| sep = rmatch.group("sep") |
| start = rmatch.group("start") |
| end = rmatch.group("end") |
|
|
| if sep == '-': |
| start = int(start) - 1 if start else None |
| end = int(end) if end else None |
| elif sep == ':': |
| start = int(start) - 1 if start else None |
| end = int(end) - 1 if end else None |
| else: |
| end = int(start) |
| start = int(start) - 1 |
| yield (start, end) |
|
|
|
|
| def extract_symbols(code, symbols): |
| """ |
| Return a tuple (blocks, not_found) |
| where ``blocks`` is a list of code fragments |
| for each symbol parsed from code, and ``not_found`` are |
| symbols not found in the code. |
| |
| For example:: |
| |
| In [1]: code = '''a = 10 |
| ...: def b(): return 42 |
| ...: class A: pass''' |
| |
| In [2]: extract_symbols(code, 'A,b,z') |
| Out[2]: (['class A: pass\\n', 'def b(): return 42\\n'], ['z']) |
| """ |
| symbols = symbols.split(',') |
|
|
| |
| py_code = ast.parse(code) |
|
|
| marks = [(getattr(s, 'name', None), s.lineno) for s in py_code.body] |
| code = code.split('\n') |
|
|
| symbols_lines = {} |
| |
| |
| |
| |
| end = len(code) |
| for name, start in reversed(marks): |
| while not code[end - 1].strip(): |
| end -= 1 |
| if name: |
| symbols_lines[name] = (start - 1, end) |
| end = start - 1 |
|
|
| |
| |
| |
| |
| blocks = [] |
| not_found = [] |
| for symbol in symbols: |
| if symbol in symbols_lines: |
| start, end = symbols_lines[symbol] |
| blocks.append('\n'.join(code[start:end]) + '\n') |
| else: |
| not_found.append(symbol) |
|
|
| return blocks, not_found |
|
|
| def strip_initial_indent(lines): |
| """For %load, strip indent from lines until finding an unindented line. |
| |
| https://github.com/ipython/ipython/issues/9775 |
| """ |
| indent_re = re.compile(r'\s+') |
|
|
| it = iter(lines) |
| first_line = next(it) |
| indent_match = indent_re.match(first_line) |
|
|
| if indent_match: |
| |
| indent = indent_match.group() |
| yield first_line[len(indent):] |
|
|
| for line in it: |
| if line.startswith(indent): |
| yield line[len(indent) :] |
| elif line in ("\n", "\r\n") or len(line) == 0: |
| yield line |
| else: |
| |
| yield line |
| break |
| else: |
| yield first_line |
|
|
| |
| for line in it: |
| yield line |
|
|
|
|
| class InteractivelyDefined(Exception): |
| """Exception for interactively defined variable in magic_edit""" |
| def __init__(self, index): |
| self.index = index |
|
|
|
|
| @magics_class |
| class CodeMagics(Magics): |
| """Magics related to code management (loading, saving, editing, ...).""" |
|
|
| def __init__(self, *args, **kwargs): |
| self._knowntemps = set() |
| super(CodeMagics, self).__init__(*args, **kwargs) |
|
|
| @line_magic |
| def save(self, parameter_s=''): |
| """Save a set of lines or a macro to a given filename. |
| |
| Usage:\\ |
| %save [options] filename [history] |
| |
| Options: |
| |
| -r: use 'raw' input. By default, the 'processed' history is used, |
| so that magics are loaded in their transformed version to valid |
| Python. If this option is given, the raw input as typed at the |
| command line is used instead. |
| |
| -f: force overwrite. If file exists, %save will prompt for overwrite |
| unless -f is given. |
| |
| -a: append to the file instead of overwriting it. |
| |
| The history argument uses the same syntax as %history for input ranges, |
| then saves the lines to the filename you specify. |
| |
| If no ranges are specified, saves history of the current session up to |
| this point. |
| |
| It adds a '.py' extension to the file if you don't do so yourself, and |
| it asks for confirmation before overwriting existing files. |
| |
| If `-r` option is used, the default extension is `.ipy`. |
| """ |
|
|
| opts,args = self.parse_options(parameter_s,'fra',mode='list') |
| if not args: |
| raise UsageError('Missing filename.') |
| raw = 'r' in opts |
| force = 'f' in opts |
| append = 'a' in opts |
| mode = 'a' if append else 'w' |
| ext = '.ipy' if raw else '.py' |
| fname, codefrom = args[0], " ".join(args[1:]) |
| if not fname.endswith(('.py','.ipy')): |
| fname += ext |
| fname = os.path.expanduser(fname) |
| file_exists = os.path.isfile(fname) |
| if file_exists and not force and not append: |
| try: |
| overwrite = self.shell.ask_yes_no('File `%s` exists. Overwrite (y/[N])? ' % fname, default='n') |
| except StdinNotImplementedError: |
| print("File `%s` exists. Use `%%save -f %s` to force overwrite" % (fname, parameter_s)) |
| return |
| if not overwrite : |
| print('Operation cancelled.') |
| return |
| try: |
| cmds = self.shell.find_user_code(codefrom,raw) |
| except (TypeError, ValueError) as e: |
| print(e.args[0]) |
| return |
| with io.open(fname, mode, encoding="utf-8") as f: |
| if not file_exists or not append: |
| f.write("# coding: utf-8\n") |
| f.write(cmds) |
| |
| if not cmds.endswith('\n'): |
| f.write('\n') |
| print('The following commands were written to file `%s`:' % fname) |
| print(cmds) |
|
|
| @line_magic |
| def pastebin(self, parameter_s=''): |
| """Upload code to dpaste.com, returning the URL. |
| |
| Usage:\\ |
| %pastebin [-d "Custom description"][-e 24] 1-7 |
| |
| The argument can be an input history range, a filename, or the name of a |
| string or macro. |
| |
| If no arguments are given, uploads the history of this session up to |
| this point. |
| |
| Options: |
| |
| -d: Pass a custom description. The default will say |
| "Pasted from IPython". |
| -e: Pass number of days for the link to be expired. |
| The default will be 7 days. |
| """ |
| opts, args = self.parse_options(parameter_s, "d:e:") |
|
|
| try: |
| code = self.shell.find_user_code(args) |
| except (ValueError, TypeError) as e: |
| print(e.args[0]) |
| return |
|
|
| expiry_days = 7 |
| try: |
| expiry_days = int(opts.get("e", 7)) |
| except ValueError as e: |
| print(e.args[0].capitalize()) |
| return |
| if expiry_days < 1 or expiry_days > 365: |
| print("Expiry days should be in range of 1 to 365") |
| return |
|
|
| post_data = urlencode( |
| { |
| "title": opts.get("d", "Pasted from IPython"), |
| "syntax": "python", |
| "content": code, |
| "expiry_days": expiry_days, |
| } |
| ).encode("utf-8") |
|
|
| request = Request( |
| "https://dpaste.com/api/v2/", |
| headers={"User-Agent": "IPython v{}".format(version)}, |
| ) |
| response = urlopen(request, post_data) |
| return response.headers.get('Location') |
|
|
| @line_magic |
| def loadpy(self, arg_s): |
| """Alias of `%load` |
| |
| `%loadpy` has gained some flexibility and dropped the requirement of a `.py` |
| extension. So it has been renamed simply into %load. You can look at |
| `%load`'s docstring for more info. |
| """ |
| self.load(arg_s) |
|
|
| @line_magic |
| def load(self, arg_s): |
| """Load code into the current frontend. |
| |
| Usage:\\ |
| %load [options] source |
| |
| where source can be a filename, URL, input history range, macro, or |
| element in the user namespace |
| |
| If no arguments are given, loads the history of this session up to this |
| point. |
| |
| Options: |
| |
| -r <lines>: Specify lines or ranges of lines to load from the source. |
| Ranges could be specified as x-y (x..y) or in python-style x:y |
| (x..(y-1)). Both limits x and y can be left blank (meaning the |
| beginning and end of the file, respectively). |
| |
| -s <symbols>: Specify function or classes to load from python source. |
| |
| -y : Don't ask confirmation for loading source above 200 000 characters. |
| |
| -n : Include the user's namespace when searching for source code. |
| |
| This magic command can either take a local filename, a URL, an history |
| range (see %history) or a macro as argument, it will prompt for |
| confirmation before loading source with more than 200 000 characters, unless |
| -y flag is passed or if the frontend does not support raw_input:: |
| |
| %load |
| %load myscript.py |
| %load 7-27 |
| %load myMacro |
| %load http://www.example.com/myscript.py |
| %load -r 5-10 myscript.py |
| %load -r 10-20,30,40: foo.py |
| %load -s MyClass,wonder_function myscript.py |
| %load -n MyClass |
| %load -n my_module.wonder_function |
| """ |
| opts,args = self.parse_options(arg_s,'yns:r:') |
| search_ns = 'n' in opts |
| contents = self.shell.find_user_code(args, search_ns=search_ns) |
|
|
| if 's' in opts: |
| try: |
| blocks, not_found = extract_symbols(contents, opts['s']) |
| except SyntaxError: |
| |
| error("Unable to parse the input as valid Python code") |
| return |
|
|
| if len(not_found) == 1: |
| warn('The symbol `%s` was not found' % not_found[0]) |
| elif len(not_found) > 1: |
| warn('The symbols %s were not found' % get_text_list(not_found, |
| wrap_item_with='`') |
| ) |
|
|
| contents = '\n'.join(blocks) |
|
|
| if 'r' in opts: |
| ranges = opts['r'].replace(',', ' ') |
| lines = contents.split('\n') |
| slices = extract_code_ranges(ranges) |
| contents = [lines[slice(*slc)] for slc in slices] |
| contents = '\n'.join(strip_initial_indent(chain.from_iterable(contents))) |
|
|
| l = len(contents) |
|
|
| |
| |
| if l > 200000 and 'y' not in opts: |
| try: |
| ans = self.shell.ask_yes_no(("The text you're trying to load seems pretty big"\ |
| " (%d characters). Continue (y/[N]) ?" % l), default='n' ) |
| except StdinNotImplementedError: |
| |
| ans = True |
|
|
| if ans is False : |
| print('Operation cancelled.') |
| return |
|
|
| contents = "# %load {}\n".format(arg_s) + contents |
|
|
| self.shell.set_next_input(contents, replace=True) |
|
|
| @staticmethod |
| def _find_edit_target(shell, args, opts, last_call): |
| """Utility method used by magic_edit to find what to edit.""" |
|
|
| def make_filename(arg): |
| "Make a filename from the given args" |
| try: |
| filename = get_py_filename(arg) |
| except IOError: |
| |
| |
| if arg.endswith('.py'): |
| filename = arg |
| else: |
| filename = None |
| return filename |
|
|
| |
| opts_prev = 'p' in opts |
| opts_raw = 'r' in opts |
|
|
| |
| class DataIsObject(Exception): pass |
|
|
| |
| lineno = opts.get('n',None) |
|
|
| if opts_prev: |
| args = '_%s' % last_call[0] |
| if args not in shell.user_ns: |
| args = last_call[1] |
|
|
| |
| |
| use_temp = True |
|
|
| data = '' |
|
|
| |
| filename = make_filename(args) |
| if filename: |
| use_temp = False |
| elif args: |
| |
| data = shell.extract_input_lines(args, opts_raw) |
| if not data: |
| try: |
| |
| |
|
|
| |
| data = eval(args, shell.user_ns) |
| if not isinstance(data, str): |
| raise DataIsObject |
|
|
| except (NameError,SyntaxError): |
| |
| filename = make_filename(args) |
| if filename is None: |
| warn("Argument given (%s) can't be found as a variable " |
| "or as a filename." % args) |
| return (None, None, None) |
| use_temp = False |
|
|
| except DataIsObject as e: |
| |
| if isinstance(data, Macro): |
| raise MacroToEdit(data) from e |
|
|
| |
| filename = find_file(data) |
| if filename: |
| if 'fakemodule' in filename.lower() and \ |
| inspect.isclass(data): |
| |
| |
| |
| attrs = [getattr(data, aname) for aname in dir(data)] |
| for attr in attrs: |
| if not inspect.ismethod(attr): |
| continue |
| filename = find_file(attr) |
| if filename and \ |
| 'fakemodule' not in filename.lower(): |
| |
| |
| data = attr |
| break |
| |
| m = ipython_input_pat.match(os.path.basename(filename)) |
| if m: |
| raise InteractivelyDefined(int(m.groups()[0])) from e |
|
|
| datafile = 1 |
| if filename is None: |
| filename = make_filename(args) |
| datafile = 1 |
| if filename is not None: |
| |
| warn('Could not find file where `%s` is defined.\n' |
| 'Opening a file named `%s`' % (args, filename)) |
| |
| |
| if datafile: |
| if lineno is None: |
| lineno = find_source_lines(data) |
| if lineno is None: |
| filename = make_filename(args) |
| if filename is None: |
| warn('The file where `%s` was defined ' |
| 'cannot be read or found.' % data) |
| return (None, None, None) |
| use_temp = False |
|
|
| if use_temp: |
| filename = shell.mktempfile(data) |
| print('IPython will make a temporary file named:',filename) |
|
|
| |
| |
| try: |
| last_call[0] = shell.displayhook.prompt_count |
| if not opts_prev: |
| last_call[1] = args |
| except: |
| pass |
|
|
|
|
| return filename, lineno, use_temp |
|
|
| def _edit_macro(self,mname,macro): |
| """open an editor with the macro data in a file""" |
| filename = self.shell.mktempfile(macro.value) |
| self.shell.hooks.editor(filename) |
|
|
| |
| mvalue = Path(filename).read_text(encoding="utf-8") |
| self.shell.user_ns[mname] = Macro(mvalue) |
|
|
| @skip_doctest |
| @line_magic |
| def edit(self, parameter_s='',last_call=['','']): |
| """Bring up an editor and execute the resulting code. |
| |
| Usage: |
| %edit [options] [args] |
| |
| %edit runs IPython's editor hook. The default version of this hook is |
| set to call the editor specified by your $EDITOR environment variable. |
| If this isn't found, it will default to vi under Linux/Unix and to |
| notepad under Windows. See the end of this docstring for how to change |
| the editor hook. |
| |
| You can also set the value of this editor via the |
| ``TerminalInteractiveShell.editor`` option in your configuration file. |
| This is useful if you wish to use a different editor from your typical |
| default with IPython (and for Windows users who typically don't set |
| environment variables). |
| |
| This command allows you to conveniently edit multi-line code right in |
| your IPython session. |
| |
| If called without arguments, %edit opens up an empty editor with a |
| temporary file and will execute the contents of this file when you |
| close it (don't forget to save it!). |
| |
| |
| Options: |
| |
| -n <number>: open the editor at a specified line number. By default, |
| the IPython editor hook uses the unix syntax 'editor +N filename', but |
| you can configure this by providing your own modified hook if your |
| favorite editor supports line-number specifications with a different |
| syntax. |
| |
| -p: this will call the editor with the same data as the previous time |
| it was used, regardless of how long ago (in your current session) it |
| was. |
| |
| -r: use 'raw' input. This option only applies to input taken from the |
| user's history. By default, the 'processed' history is used, so that |
| magics are loaded in their transformed version to valid Python. If |
| this option is given, the raw input as typed as the command line is |
| used instead. When you exit the editor, it will be executed by |
| IPython's own processor. |
| |
| -x: do not execute the edited code immediately upon exit. This is |
| mainly useful if you are editing programs which need to be called with |
| command line arguments, which you can then do using %run. |
| |
| |
| Arguments: |
| |
| If arguments are given, the following possibilities exist: |
| |
| - If the argument is a filename, IPython will load that into the |
| editor. It will execute its contents with execfile() when you exit, |
| loading any code in the file into your interactive namespace. |
| |
| - The arguments are ranges of input history, e.g. "7 ~1/4-6". |
| The syntax is the same as in the %history magic. |
| |
| - If the argument is a string variable, its contents are loaded |
| into the editor. You can thus edit any string which contains |
| python code (including the result of previous edits). |
| |
| - If the argument is the name of an object (other than a string), |
| IPython will try to locate the file where it was defined and open the |
| editor at the point where it is defined. You can use `%edit function` |
| to load an editor exactly at the point where 'function' is defined, |
| edit it and have the file be executed automatically. |
| |
| - If the object is a macro (see %macro for details), this opens up your |
| specified editor with a temporary file containing the macro's data. |
| Upon exit, the macro is reloaded with the contents of the file. |
| |
| Note: opening at an exact line is only supported under Unix, and some |
| editors (like kedit and gedit up to Gnome 2.8) do not understand the |
| '+NUMBER' parameter necessary for this feature. Good editors like |
| (X)Emacs, vi, jed, pico and joe all do. |
| |
| After executing your code, %edit will return as output the code you |
| typed in the editor (except when it was an existing file). This way |
| you can reload the code in further invocations of %edit as a variable, |
| via _<NUMBER> or Out[<NUMBER>], where <NUMBER> is the prompt number of |
| the output. |
| |
| Note that %edit is also available through the alias %ed. |
| |
| This is an example of creating a simple function inside the editor and |
| then modifying it. First, start up the editor:: |
| |
| In [1]: edit |
| Editing... done. Executing edited code... |
| Out[1]: 'def foo():\\n print("foo() was defined in an editing |
| session")\\n' |
| |
| We can then call the function foo():: |
| |
| In [2]: foo() |
| foo() was defined in an editing session |
| |
| Now we edit foo. IPython automatically loads the editor with the |
| (temporary) file where foo() was previously defined:: |
| |
| In [3]: edit foo |
| Editing... done. Executing edited code... |
| |
| And if we call foo() again we get the modified version:: |
| |
| In [4]: foo() |
| foo() has now been changed! |
| |
| Here is an example of how to edit a code snippet successive |
| times. First we call the editor:: |
| |
| In [5]: edit |
| Editing... done. Executing edited code... |
| hello |
| Out[5]: "print('hello')\\n" |
| |
| Now we call it again with the previous output (stored in _):: |
| |
| In [6]: edit _ |
| Editing... done. Executing edited code... |
| hello world |
| Out[6]: "print('hello world')\\n" |
| |
| Now we call it with the output #8 (stored in _8, also as Out[8]):: |
| |
| In [7]: edit _8 |
| Editing... done. Executing edited code... |
| hello again |
| Out[7]: "print('hello again')\\n" |
| |
| |
| Changing the default editor hook: |
| |
| If you wish to write your own editor hook, you can put it in a |
| configuration file which you load at startup time. The default hook |
| is defined in the IPython.core.hooks module, and you can use that as a |
| starting example for further modifications. That file also has |
| general instructions on how to set a new hook for use once you've |
| defined it.""" |
| opts,args = self.parse_options(parameter_s,'prxn:') |
|
|
| try: |
| filename, lineno, is_temp = self._find_edit_target(self.shell, |
| args, opts, last_call) |
| except MacroToEdit as e: |
| self._edit_macro(args, e.args[0]) |
| return |
| except InteractivelyDefined as e: |
| print("Editing In[%i]" % e.index) |
| args = str(e.index) |
| filename, lineno, is_temp = self._find_edit_target(self.shell, |
| args, opts, last_call) |
| if filename is None: |
| |
| |
| return |
|
|
| if is_temp: |
| self._knowntemps.add(filename) |
| elif (filename in self._knowntemps): |
| is_temp = True |
|
|
|
|
| |
| print('Editing...', end=' ') |
| sys.stdout.flush() |
| filepath = Path(filename) |
| try: |
| |
| |
| quoted = filename = str(filepath.absolute()) |
| if " " in quoted: |
| quoted = "'%s'" % quoted |
| self.shell.hooks.editor(quoted, lineno) |
| except TryNext: |
| warn('Could not open editor') |
| return |
|
|
| |
| |
| if args.strip() == "pasted_block": |
| self.shell.user_ns["pasted_block"] = filepath.read_text(encoding="utf-8") |
|
|
| if 'x' in opts: |
| print() |
| else: |
| print('done. Executing edited code...') |
| with preserve_keys(self.shell.user_ns, '__file__'): |
| if not is_temp: |
| self.shell.user_ns["__file__"] = filename |
| if "r" in opts: |
| source = filepath.read_text(encoding="utf-8") |
| self.shell.run_cell(source, store_history=False) |
| else: |
| self.shell.safe_execfile(filename, self.shell.user_ns, |
| self.shell.user_ns) |
|
|
| if is_temp: |
| try: |
| return filepath.read_text(encoding="utf-8") |
| except IOError as msg: |
| if Path(msg.filename) == filepath: |
| warn('File not found. Did you forget to save?') |
| return |
| else: |
| self.shell.showtraceback() |
|
|